1 Cours ENPC, printemps 2001
Java, objets, interfaces et Internet

Premières applications

Créer un répertoire java dans votre répertoire personnel, et des sous-répertoires java/src et java/classes.

Les programmes sources Java figureront dans le répertoire java/src, regroupés par package. Pour ce premier TD, créer un répertoire java/src/td1.

Écrire dans ce répertoire le source Java d'une application dans un fichier f.java ; ce fichier doit commencer par package td1;. Soit M le nom de la classe principale (c'est-à-dire celle contenant la fonction main). Exemples : Hello.java, Echo.java.

Compiler ce fichier par la commande javac f.java. Les fichiers de classe M.class, etc., sont créés dans le répertoire java/classes/td1.

En vous plaçant dans java/classes, exécuter cette application par la commande java td1.M.

Passage d'arguments à une application

La fonction main doit être déclarée ainsi :

public static void main(String[] args)
	

Si l'application doit être appelée avec, par exemple, trois arguments a1, a2 et a3 :

java td1.M a1 a2 a3
	

la fonction main obtient ces trois chaînes de caractères comme args[0], args[1] et args[2], qui sont des références de type String.

Écrire sur la sortie standard

System.out.println("le résultat est" + N + ".");
	

L'opérateur + permet ici de concaténer des chaînes de caractères. La variable N est d'abord convertie en une chaîne de caractères avant la concaténation. L'écriture est terminée par un retour à la ligne.

Pour lire une donnée d'un certain type, par exemple un entier, sur l'entrée standard, voir le chapitre 5.

Classe et instances, constructeurs

Une classe permet de définir des objets par instanciation, en utilisant l'opérateur new et l'un des constructeurs de la classe :

Point p = new Point(2, 3);
Point o = new Point();
	

Les définitions de p et o introduisent ces noms comme des références à des objets de la classe Point. Ces objets sont construits à l'aide de l'opérateur new et d'un constructeur de la classe Point. Un constructeur d'une classe a le même nom que la classe, et un certain nombre de paramètres.

Membres : champs et méthodes

Les membres d'une classe sont ses champs et ses méthodes. Les champs d'une classe sont analogues à ceux d'un struct en C. Les méthodes sont des fonctions qui opérent sur les champs. On accède à un champ d'une instance en suffixant le nom d'un objet par le nom du champ ; on invoque une méthode d'une instance en suffixant le nom d'un objet par le nom de la méthode, suivi par ses arguments entre parenthèses : par exemple, on accède aux champs x et y du point p par p.x et p.y, et on invoque la méthode translater par p.translater(1, 1).

La définition d'une classe comporte la définition de ses champs, de ses constructeurs et de ses méthodes. La définition suivante a deux champs, deux constructeurs et une méthode :

class Point {
 double x, y;

 Point() {}              // x = y = 0.0;

 Point(double x, double y) {
   this.x = x;
   this.y = y;
 }
 // ------------------------------------
 void translater(double dx, double dy) {
   x = x + dx;             // this.x = this.x + dx
   y = y + dy;
 }
}

La référence this

Le nom this, employé dans une méthode, réfère à l'instance courante de la classe. Il permet d'accéder aux membres de l'instance this.x, this.y et this.translater(). Il permet aussi d'employer le nom de champ x comme paramètre du constructeur, dans this.x = x.

Ce nom peut aussi être employé avec une liste d'arguments, mais seulement au début du corps d'un constructeur, pour invoquer un autre constructeur de la classe. Par exemple, le constructeur sans argument pourrait être défini à l'aide du constructeur à deux arguments en employant this :

 Point() {
   this(0, 0);
} 

Dérivation et héritage

À l'exception de la classe Object, toute classe dérive d'une autre classe. La dérivation est spécifiée par le mot-clé extends et permet d'étendre ou de spécialiser une classe. La classe dérivée est une sous-classe ; la classe parente est la sur-classe directe. Les membres de la classe parente peuvent être hérités par la classe dérivée.

Dans la classe PointColore, la dérivation ajoute un champ de type Color à la classe Point ; ses constructeurs commencent par appeler le constructeur de la classe parente, par l'opérateur super ; la méthode translater est héritée :

class PointColore extends Point {
 Color couleur;

 PointColore(double x, double y, Color couleur) {
   super(x, y);
   this.couleur = couleur;
 }
 PointColore() {
   super();
   this.couleur = Color.black;
 }
}

Dans la classe PointDiagonal, la dérivation n'ajoute pas de champ à la classe Point, mais contraint ses deux champs à être égaux ; la méthode translater n'est pas héritée, car elle est redéfinie, et une nouvelle méthode translater à un argument est ajoutée :

class PointDiagonal extends Point {

 PointDiagonal(double x) {
   this.x = this.y = x;
 }

 void translater(double dx, double dy) {
   super.translater(dx, dx);
 }

 void translater(double dx) {
   translater(dx, dx);
 }
}

La référence super

Le nom super, employé dans une méthode, réfère à l'instance courante, en tant qu'objet de la classe parente. Il permet d'accéder aux membres (champs ou méthodes) définis dans la classe parente, même s'ils sont cachés ou redéfinis dans la classe contenant cette utilisation de super : c'est le cas de super.translater(dx, dx).

Ce nom peut aussi être employé avec une liste d'arguments, pour invoquer un constructeur de la classe parente. C'est le cas de super(x, y) invoqué dans le constructeur PointColore(double, double, Color). Cette invocation éventuelle de super doit toujours figurer au début du corps du constructeur (retenir qu'avant de créer un objet, il faut d'abord créer son « parent »).

Surcharge

De façon analogue aux constructeurs, il est fréquent que plusieurs méthodes d'une classe portent le même nom, chacune ayant soit un nombre d'arguments distinct, soit le même nombre, mais des types distincts ; c'est le cas de translater. On parle dans ce cas de surcharge du nom. Le compilateur est capable de déterminer laquelle de ces méthodes homonymes doit être invoquée, en se basant sur le nombre et le type des arguments.

Sous-typage et héritage

Un objet d'une classe dérivée d'une classe A peut être employé partout où un objet de classe A est attendu :

PointColore pointRose = new PointColore(2, 3, Color.pink);

Les méthodes de la classe parente qui ne sont pas redéfinies sont héritées par la classe dérivée :

  pointRose.x ...
  pointRose.translater(3, 4); ...

Les champs de la classe parente sont hérités par la classe dérivée, mais peuvent être masqués par un autre champ de même nom.

L'héritage est un des mécanismes caractéristiques de la programmation orientée objet.

Typage statique et liaison tardive

Un langage admet un typage statique s'il est possible de vérifier à la compilation la cohérence des types de toutes les expressions, et ainsi d'éliminer la possibilité d'erreurs à l'exécution dues à des opérations qui ne respecteraient pas les contraintes de type. Le typage statique est un critère de sécurité essentiel. La plupart des langages de programmation contemporains admettent un typage statique : C, CAML, Java. Il existe cependant des langages non-typés comme Lisp et Prolog.

Le mécanisme de liaison est celui qui associe un objet à un nom. Cette association est déterminée d'une part par le texte du programme, (notamment par la portée lexicale des noms dans les blocs, comme en C), et d'autre part par l'environnement d'exécution.

C'est ainsi qu'un nom déclaré d'un certain type peut désigner une instance d'une classe dérivée de ce type :

Point pointRose = new PointColore(2, 3, Color.pink);
Point pointUnite = new PointDiagonal(1);

Ces affectations sont correctes, car de la forme « type = sous-type » : le type du nom pointRose est Point, tandis que le type de l'objet référé par pointRose est PointColore.

La méthode translater(double, double) de la classe Point est redéfinie dans la classe PointDiagonal. L'évaluation de l'expression pointUnite.translater(1, 2) consiste à invoquer la méthode translater(double, double) définie dans la classe PointDiagonal, non celle de Point. La liaison du nom translater à la méthode de PointDiagonal ne peut être réalisée en général qu'à l'exécution, quand le type réel de pointUnite est connu (car le new doit être exécuté). Il s'agit d'une forme de liaison dynamique, dite liaison tardive, qui est une des particularités des langages orientés objets. Cette liaison est propre aux méthodes et ne s'applique pas aux champs.

Classes abstraites

Une méthode abstraite est une méthode déclarée abstract et dont le corps est remplacé par un « ; ». Une classe abstraite est une classe qui a au moins une méthode abstraite (éventuellement héritée d'une classe parente abstraite). Les classes abstraites sont donc des classes partiellement implémentées ; elles doivent être déclarées abstract. Une classe abstraite ne peut pas avoir de constructeur et n'a pas d'instance.

Cependant, les classes abstraites sont des types. On peut déclarer une variable ou un paramètre d'une méthode d'un type abstrait. Le mécanisme de liaison tardive permettra d'invoquer la méthode correcte une fois que la variable ou le paramètre désignera une instance d'une classe concrète dérivée.

abstract class Arbre {

  abstract boolean estVide();
}

class AVide extends Arbre {

  AVide(){}

  boolean estVide() {
    return true;
  }
}

class ACons extends Arbre {
  int label;
  Arbre gauche, droite;

  ACons(int label, Arbre gauche, Arbre droite) {
   this.label = label;
   this.gauche = gauche;
   this.droite = droite;
  }

  boolean estVide() {
    return false;
  }
}
Une classe d'application pourra déclarer une variable a de type abstrait Arbre, et pourra l'initialiser à l'aide des constructeurs des classes concrètes :
    Arbre a = 
      new ACons(1,
		new ACons(2, new AVide(), new AVide()),
		new ACons(3, new AVide(), new AVide()));

L'exécution de l'appel a.estVide() conduit à invoquer la méthode estVide() de ACons, qui renvoie false.


[Cours Java] [Notes 2]

Mise à jour :