IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Penser en Java 2nde édition - Sommaire |  Préface |  Avant-propos | Chapitre : 1  2  3  4  5  6  7  8  9  10  11  12  13  14  15 |  Annexe : A B C D  | Tables des matières - Thinking in Java

  Chapitre 6 -  Réutiliser les classes

pages : 1 2 3 4 

Pourquoi le transtypage ascendant ? 

La raison de ce terme est historique et basée sur la manière dont les diagrammes d'héritage ont été traditionnellement dessinés :  avec la racine au sommet de la page, et grandissant vers le bas. Bien sûr vous pouvez dessiner vos diagrammes de la manière que vous trouvez le plus pratique. Le diagramme d'héritage pour Wind.java est :  image

Transtyper depuis une classe dérivée vers la classe de base nous déplace vers le haut dans le diagramme, on fait donc communément référence à un transtypage ascendant. Le transtypage ascendant est toujours sans danger parce qu'on va d'un type plus spécifique vers un type plus général. La classe dérivée est un sur-ensemble de la classe de base. Elle peut contenir plus de méthodes que la classe de base, mais elle contient au moins les méthodes de la classe de base. La seule chose qui puisse arriver à une classe pendant le transtypage ascendant est de perdre des méthodes et non en gagner. C'est pourquoi le compilateur permet le transtypage ascendant sans transtypage explicite ou une notation spéciale.

On peut également faire l'inverse du transtypage ascendant, appelé transtypage descendant, mais cela génère un dilemme qui est le sujet du chapitre 12.

Composition à la place de l'héritage revisité

En programmation orienté objet, la manière la plus probable pour créer et utiliser du code est simplement de mettre des méthodes et des données ensemble dans une classe puis d'utiliser les objets de cette classe. On utilisera également les classes existantes pour construire les nouvelles classes avec la composition. Moins fréquemment on utilisera l'héritage. Donc bien qu'on insiste beaucoup sur l'héritage en apprenant la programmation orientée objet, cela ne signifie pas qu'on doive l'utiliser partout où l'on peut. Au contraire, on devrait l'utiliser avec parcimonie, seulement quand il est clair que l'héritage est utile. Un des moyens les plus clairs pour déterminer si on doit utiliser la composition ou l'héritage est de se demander si on aura jamais besoin de faire un transtypage ascendant de la nouvelle classe vers la classe de base. Si on doit faire un transtypage ascendant, alors l'héritage est nécessaire, mais si on n'a pas besoin de faire un transtypage ascendant, alors il faut regarder avec attention pour savoir si on a besoin de l'héritage. Le prochain chapitre (polymorphisme) fournit une des plus excitantes raisons pour le transtypage ascendant, mais si vous vous rappelez de vous demander « Ai-je besoin de transtypage ascendant ? », vous aurez un bon outil pour décider entre composition et héritage.

Le mot clé final

Le mot clé Java final a des sens légèrement différents suivant le contexte, mais en général il signifie « Cela ne peut pas changer ». Vous pourriez vouloir empêcher les changements pour deux raisons : conception ou efficacité. Parce que ces deux raisons sont quelque peu différentes, il est possible de mal utiliser le mot clé final.

Les sections suivantes parlent des trois endroits où le mot clé final peut être utilisé : données, méthodes et classes.

Données finales

Beaucoup de langages de programmation ont un moyen de dire au compilateur que cette donnée est constante. Une constante est utile pour deux raisons:

  1. Elle peut être une constante lors de la compilation qui ne changera jamais ; 
  2. Elle peut être une valeur initialisée à l'exécution qu'on ne veut pas changer.

Dans le cas d'une constante à la compilation, le compilateur inclut « en dur » la valeur de la constante pour tous les calculs où elle intervient  ; dans ce cas, le calcul peut être effectué à la compilation, éliminant ainsi un surcoût à l'exécution. En Java, ces sortes de constantes doivent être des primitives et sont exprimées en utilisant le mot-clé final. Une valeur doit être donnée au moment de la définition d'une telle constante.

Un champ qui est à la fois static et final a un emplacement de stockage fixe qui ne peut pas être changé.

Quand on utilise final avec des objets références plutôt qu'avec des types primitifs la signification devient un peu confuse. Avec un type primitif, final fait de la valeur une constante, mais avec un objet référence, final fait de la « référence » une constante. Une fois la référence liée à un objet, elle ne peut jamais changer pour pointer vers un autre objet. Quoiqu'il en soit, l'objet lui même peut être modifié ; Java ne fournit pas de moyen de rendre un objet arbitraire une constante. On peut quoiqu'il en soit écrire notre classe de manière que les objets paraissent constants. Cette restriction inclut les tableaux, qui sont également des objets.

Voici un exemple qui montre les champs final:

// ! c06:FinalData.java
// L'effet de final sur les champs.

class Value {
  int i = 1;
}

public class FinalData {
  // Peut être des constantes à la compilation
  final int i1 = 9;
  static final int VAL_TWO = 99;
  // Constantes publiques typiques:
  public static final int VAL_THREE = 39;
  // Ne peuvent pas être des constantes à la compilation:
  final int i4 = (int)(Math.random()*20);
  static final int i5 = (int)(Math.random()*20);
  
  Value v1 = new Value();
  final Value v2 = new Value();
  static final Value v3 = new Value();
  // Tableaux:
  final int[] a = { 1, 2, 3, 4, 5, 6 };

  public void print(String id) {
    System.out.println(
      id + " : " + "i4 = " + i4 +
      ", i5 = " + i5);
  }
  public static void main(String[] args) {
    FinalData fd1 = new FinalData();
    // ! fd1.i1++; // Erreur : on ne peut pas changer la valeur
    fd1.v2.i++; // L'objet n'est pas une constante!
    fd1.v1 = new Value(); // OK -- non final
    for(int i = 0; i < fd1.a.length; i++)
      fd1.a[i]++; // L'objet n'est pas une constante!
    // ! fd1.v2 = new Value(); // Erreur : Ne peut pas
    // ! fd1.v3 = new Value(); // changer la référence
    // ! fd1.a = new int[3];

    fd1.print("fd1");
    System.out.println("Creating new FinalData");
    FinalData fd2 = new FinalData();
    fd1.print("fd1");
    fd2.print("fd2");
  }
} ///:~

Etant donné que i1 et VAL_TWO sont des primitives final ayant une valeur à la compilation, elles peuvent être toutes les deux utilisées comme constantes à la compilation et ne sont pas vraiment différentes. VAL_THREE nous montre la manière la plus typique de définir ces constantes : public afin qu'elles puissent être utilisées en dehors du package, static pour souligner qu'il ne peut y en avoir qu'une seulement, et final pour dire que c'est une constante. Notez que les primitives final static avec des valeurs initiales constantes (ce sont des constantes à la compilation) sont nommées avec des lettre capitales par convention, avec des mots séparés par des underscores. Ce sont comme des constantes C, d'où cette convention est originaire. Notons également que i5 ne peut pas être connu à la compilation, donc elle n'est pas en lettres capitales.

Le fait que quelque chose soit final ne signifie pas que sa valeur est connue à la compilation. Ceci est montré par l'initialisation de i4 et i5 à l'exécution en utilisant des nombres générés aléatoirement. La portion de cet exemple montre également la différence entre mettre une valeur final static ou non static. Cette différence n'est visible que quand les valeurs sont initialisées à l'exécution, tandis que les valeurs à la compilation sont traitées de même par le compilateur. Et vraisemblablement optimisées à la compilation. La différence est montrée par la sortie d'une exécution:

fd1 : i4 = 15, i5 = 9
Creating new FinalData
fd1 : i4 = 15, i5 = 9
fd2 : i4 = 10, i5 = 9

Notez que les valeurs de i4 pour fd1 et fd2 sont uniques, mais la valeur de i5 n'est pas changée en créant un second objet FinalData. C'est parce qu'elle est static et initialisée une fois pour toutes lors du chargement et non à chaque fois qu'un nouvel objet est créé.

Les variables v1 jusqu'à v4 montre le sens de références final. Comme on peut le voir dans main(), le fait que v2 soit final ne signifie pas qu'on ne peut pas changer sa valeur. Quoiqu'il en soit, on ne peut pas réaffecter un nouvel objet à v2, précisément parce qu'il est final. C'est ce que final signifie pour une référence. On peut également voir que ce sens reste vrai pour un tableau, qui est une autre sorte de référence. Il n'y a aucun moyen de savoir comment rendre les références du tableau elle-mêmes final. Mettre les références final semble moins utile que mettre les primitives final.

Finals sans initialisation

Java permet la création de finals sans initialisation, qui sont des champs déclarés final, mais n'ont pas de valeur d'initialisation. Dans tous les cas, un final sans initialisation doit être initialisé avant d'être utilisé, et le compilateur doit s'en assurer. Quoiqu'il en soit, les finals sans initialisation fournissent bien plus de flexibilité dans l'usage du mot-clé final depuis que, par exemple, un champ final à l'intérieur d'une classe peut maintenant être différent pour chaque objet tout en gardant son caractère immuable. Voici un exemple:

// ! c06:BlankFinal.java
// Les membres des données final sans initialisation

class Poppet { }

class BlankFinal {
  final int i = 0; // Final initialisé
  final int j; // Final sans initialisation
  final Poppet p; // Référence final sans initialisation
  // Les finals doivent être initialisés
  // dans le constructeur:
  BlankFinal() {
    j = 1; // Initialise le final sans valeur initiale
    p = new Poppet();
  }
  BlankFinal(int x) {
    j = x; // Initialise le final sans valeur initiale
    p = new Poppet();
  }
  public static void main(String[] args) {
    BlankFinal bf = new BlankFinal();
  }
} ///:~

Vous êtes forcés d'initialiser un final soit avec une expression au moment de la définition, soit dans chaque constructeur. De cette manière il est garanti que le champ final sera toujours initialisé avant son utilisation.

Arguments final

Java permet de définir les arguments final en les déclarant comme tels dans la liste des arguments. Cela signifie qu'à l'intérieur de la méthode on ne peut pas changer ce vers quoi pointe l'argument:

// ! c06:FinalArguments.java
// Utilisation de « final » dans les arguments d'une méthode.

class Gizmo {
  public void spin() {}
}

public class FinalArguments {
  void with(final Gizmo g) {
    // ! g = new Gizmo(); // Illégal -- g est final
  }
  void without(Gizmo g) {
    g = new Gizmo(); // OK -- g n'est pas final
    g.spin();
  }
  // void f(final int i) { i++; } // Ne peut pas changer
  // On peut seulement lire depuis une primitive final:
  int g(final int i) { return i + 1; }
  public static void main(String[] args) {
    FinalArguments bf = new FinalArguments();
    bf.without(null);
    bf.with(null);
  }
} ///:~

A noter qu'on peut encore affecter une référence null à un argument qui est final sans que le compilateur ne l'empêche, comme on pourrait le faire pour un argument non-final.

Les méthodes f( ) et g( ) montre ce qui arrive quand les arguments primitifs sont final: on peut lire l'argument, mais on ne peut pas le changer.

Méthodes final

Les méthodes final ont deux raisons d'être. La première est de mettre un « verrou » sur la méthode pour empêcher toute sous-classe de la redéfinir. Ceci est fait pour des raisons de conception quand on veut être sûr que le comportement d'une méthode est préservé durant l'héritage et ne peut pas être redéfini.

La deuxième raison est l'efficacité. Si on met une méthode final, on permet au compilateur de convertir tout appel à cette méthode en un appel incorporé. Quand le compilateur voit un appel à une méthode final, il peut à sa discrétion éviter l'approche normale d'insérer du code pour exécuter l'appel de la méthode (mettre les arguments sur la pile, sauter au code de la méthode et l' exécuter, revenir au code courant et nettoyer les arguments de la pile, s'occuper de la valeur de retour) et à la place remplacer l'appel de méthode avec une copie du code de cette méthode dans le corps de la méthode courante. Ceci élimine le surcoût de l'appel de méthode. Bien entendu, si une méthode est importante, votre code commencera alors à grossir et vous ne verrez plus le gain de performance dû au code « incorporé », parce que toute amélioration sera cachée par le temps passé à l'intérieur de la méthode. Ceci implique que le compilateur Java est capable de détecter ces situations et de choisir sagement si oui ou non il faut « incorporer » une méthode final. Quoiqu'il en soit, il est mieux de ne pas faire confiance à ce que peut faire le compilateur et de mettre une méthode final seulement si elle est plutôt petite ou si on veut explicitement empêcher la surcharge.

final et private

Toutes les méthodes private sont implicitement final. Parce qu'on ne peut pas accéder à une méthode private, on ne peut pas la surcharger (même si le compilateur ne donne pas de messages d'erreur si on essaye de la redéfinir, on ne redéfinit pas la méthode, on a simplement créé une nouvelle méthode). On peut ajouter le mot-clé final à une méthode private, mais ça n'apporte rien de plus.

Ce problème peut rendre les choses un peu confuses, parce que si on essaye de surcharger une méthode private qui est implicitement final ça semble fonctionner:

// ! c06:FinalOverridingIllusion.java
// C'est seulement une impression qu'on peut
// surcharger une méthode private ou private final.

class WithFinals {
  // Identique à « private » tout seul:
  private final void f() {
    System.out.println("WithFinals.f()");
  }
  // Également automatiquement « final »:
  private void g() {
    System.out.println("WithFinals.g()");
  }
}

class OverridingPrivate extends WithFinals {
  private final void f() {
    System.out.println("OverridingPrivate.f()");
  }
  private void g() {
    System.out.println("OverridingPrivate.g()");
  }
}

class OverridingPrivate2
  extends OverridingPrivate {
  public final void f() {
    System.out.println("OverridingPrivate2.f()");
  }
  public void g() {
    System.out.println("OverridingPrivate2.g()");
  }
}

public class FinalOverridingIllusion {
  public static void main(String[] args) {
    OverridingPrivate2 op2 =
      new OverridingPrivate2();
    op2.f();
    op2.g();
    // On peut faire un transtypage ascendant:
    OverridingPrivate op = op2;
    // Mais on ne peut pas appeler les méthodes:
    // ! op.f();
    // ! op.g();
    // Idem ici:
    WithFinals wf = op2;
    // ! wf.f();
    // ! wf.g();
  }
} ///:~

« Surcharger » peut seulement arriver si quelque chose fait partie de l'interface de la classe de base. On doit être capable de faire un transtypage ascendant vers la classe de base et d'appeler la même méthode. Ce point deviendra clair dans le prochain chapitre. Si une méthode est private, elle ne fait pas partie de l'interface de la classe de base. C'est simplement du code qui est caché à l'intérieur de la classe, et il arrive simplement qu'elle a ce nom, mais si on définit une méthode public, protected ou « amies » dans la classe dérivée, il n'y a aucune connexion avec la méthode de même nom dans la classe de base. Étant donné qu'une méthode private est inatteignable et effectivement invisible, elle ne sert à rien d'autre qu'à l'organisation du code dans la classe dans laquelle elle est définie.

Classes final

Quand on dit qu'une classe entière est final (en faisant précéder sa définition par le mot-clé final) on stipule qu'on ne veut pas hériter de cette classe ou permettre à qui que ce soit de le faire. En d'autres mots, soit la conception de cette classe est telle qu'on n'aura jamais besoin de la modifier, soit pour des raisons de sûreté ou de sécurité on ne veut pas qu'elle soit sous-classée. Ou alors, on peut avoir affaire à un problème d'efficacité, et on veut s'assurer que toute activité impliquant des objets de cette classe sera aussi efficace que possible.

Ce livre a été écrit par Bruce Eckel ( télécharger la version anglaise : Thinking in java )
Ce chapitre a été traduit par Olivier Thomann ( groupe de traduction )
télécharger la version francaise (PDF) | Commandez le livre en version anglaise (amazon) | télécharger la version anglaise
pages : 1 2 3 4 
Penser en Java 2nde édition - Sommaire |  Préface |  Avant-propos | Chapitre : 1  2  3  4  5  6  7  8  9  10  11  12  13  14  15 |  Annexe : A B C D  | Tables des matières - Thinking in Java