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 

Notons que les données membres peuvent ou non être final, comme on le choisit. Les mêmes règles s'appliquent à final pour les données membres sans tenir compte du fait que la classe est ou non final. Définir une classe comme final empêche simplement l'héritage - rien de plus. Quoiqu'il en soit, parce que cela empêche l'héritage, toutes les méthodes d'une classe final sont implicitement final, étant donné qu'il n'y a aucun moyen de les surcharger. Donc le compilateur à les mêmes options d'efficacité que si on définissait explicitement une méthode final.

On peut ajouter le modificateur final à une méthode dans une classe final, mais ça ne rajoute aucune signification.

Attention finale

Il peut paraître raisonnable de déclarer une méthode final alors qu'on conçoit une classe. On peut décider que l'efficacité est très importante quand on utilise la classe et que personne ne pourrait vouloir surcharger les méthodes de toute manière. Cela est parfois vrai.

Mais il faut être prudent avec ces hypothèses. En général, il est difficile d' anticiper comment une classe va être réutilisée, surtout une classe générique. Si on définit une méthode comme final, on devrait empêcher la possibilité de réutiliser cette classe par héritage dans les projets d'autres programmeurs simplement parce qu'on ne pourrait pas l'imaginer être utilisée de cette manière.

La bibliothèque standard de Java en est un bon exemple. En particulier, la classe Vector en Java 1.0/1.1 était communément utilisée et pourrait avoir encore été plus utile si, au nom de l'efficacité, toutes les méthodes n'avaient pas été final. Ça parait facilement concevable que l'on puisse vouloir hériter et surcharger une telle classe fondamentale, mais les concepteurs d'une manière ou d'une autre ont décidé que ce n'était pas approprié. C'est ironique pour deux raisons. Premièrement, Stack hérite de Vector, ce qui dit qu'une Stack est un Vector, ce qui n'est pas vraiment vrai d'un point de vue logique. Deuxièmement, beaucoup des méthodes importantes de Vector, telles que addElement( ) et elementAt( ) sont synchronized. Comme nous verrons au chapitre 14, ceci implique un surcoût significatif au niveau des performances qui rend caduque tout gain fourni par final. Ceci donne de la crédibilité à la théorie que les programmeurs sont constamment mauvais pour deviner où les optimisations devraient être faites.Il est vraiment dommage qu'une conception aussi maladroite ait fait son chemin dans la bibliothèque standard que nous utilisons tous. Heureusement, la bibliothèque de collections Java 2 remplace Vector avec ArrayList, laquelle se comporte bien mieux. Malheureusement , il y a encore beaucoup de code nouveau écrit qui utilise encore l'ancienne bibliothèques de collections.

Il est intéressant de noter également que Hashtable, une autre classe importante de la bibliothèque standard, n'a pas une seule méthode final. Comme mentionné à plusieurs endroits dans ce livre, il est plutôt évident que certaines classes ont été conçues par des personnes totalement différentes. Vous verrez que les noms de méthodes dans Hashtable sont beaucoup plus court comparés à ceux de Vector, une autre preuve. C'est précisément ce genre de choses qui ne devrait pas être évident aux utilisateurs d'une bibliothèque de classes. Quand plusieurs choses sont inconsistantes, cela fait simplement plus de travail pour l'utilisateur. Encore un autre grief à la valeur de la conception et de la qualité du code. À noter que la bibliothèque de collection de Java 2 remplaces Hashtable avec HashMap.

Initialisation et chargement de classes

Dans des langages plus traditionnels, les programmes sont chargés tout d'un coup au moment du démarrage. Ceci est suivi par l'initialisation et ensuite le programme commence. Le processus d'initialisation dans ces langages doit être contrôlé avec beaucoup d'attention afin que l'ordre d'initialisation des statics ne pose pas de problème. C++, par exemple, a des problèmes si une static attend qu'une autre static soit valide avant que la seconde ne soit initialisée.

Java n'a pas ce problème parce qu'il a une autre approche du chargement. Parce que tout en Java est un objet, beaucoup d'actions deviennent plus facile, et ceci en est un exemple. Comme vous l'apprendrez plus complètement dans le prochain chapitre, le code compilé de chaque classe existe dans son propre fichier séparé. Ce fichier n'est pas chargé tant que ce n'est pas nécessaire. En général, on peut dire que « le code d'une classe est chargé au moment de la première utilisation ». C'est souvent au moment où le premier objet de cette classe est construit, mais le chargement se produit également lorsqu'on accède à un champ static ou une méthode static.

Le point de première utilisation est également là où l'initialisation des statics a lieu. Tous les objets static et le bloc de code static sera initialisé dans l'ordre textuel (l'ordre dans lequel ils sont définis dans la définition de la classe) au moment du chargement. Les statics, bien sûr, ne sont initialisés qu'une seule fois.

Initialisation avec héritage

Il est utile de regarder l'ensemble du processus d'initialisation, incluant l'héritage pour obtenir une compréhension globale de ce qui se passe. Considérons le code suivant:

// ! c06:Beetle.java
// Le processus complet d'initialisation.

class Insect {
  int i = 9;
  int j;
  Insect() {
    prt("i = " + i + ", j = " + j);
    j = 39;
  }
  static int x1 =
    prt("static Insect.x1 initialisé");
  static int prt(String s) {
    System.out.println(s);
    return 47;
  }
}

public class Beetle extends Insect {
  int k = prt("Beetle.k initialisé");
  Beetle() {
    prt("k = " + k);
    prt("j = " + j);
  }
  static int x2 =    prt("static Beetle.x2 initialisé");
  public static void main(String[] args) {
    prt("Constructeur Beetle");
    Beetle b = new Beetle();
  }
} ///:~

La sortie de ce programme est:

static Insect.x1 initialisé
static Beetle.x2 initialisé
Constructeur Beetle
i = 9, j = 0
Beetle.k initialisé
k = 47
j = 39

La première chose qui se passe quand on exécute Beetle en Java est qu'on essaye d'accéder à Beetle.main( ) (une méthode static), donc le chargeur cherche et trouve le code compilé pour la classe Beetle (en général dans le fichier appelé Beetle.class). Dans le processus de son chargement, le chargeur remarque qu'elle a une classe de base (c'est ce que le mot-clé extends veut dire), laquelle est alors chargée. Ceci se produit qu'on construise ou non un objet de la classe de base. Essayez de commenter la création de l'objet pour vous le prouver.

Si la classe de base a une classe de base, cette seconde classe de base sera à son tour chargée, etc. Ensuite, l'initialisation static dans la classe de base racine (dans ce cas, Insect) est effectuée, ensuite la prochaine classe dérivée, etc. C'est important parce que l'initialisation static de la classe dérivée pourrait dépendre de l'initialisation correcte d'un membre de la classe de base.

À ce point, les classes nécessaires ont été chargées, donc l'objet peut être créé. Premièrement, toutes les primitives dans l'objet sont initialisées à leurs valeurs par défaut et les références objets sont initialisées à null - ceci se produit en une seule passe en mettant la mémoire dans l'objet au zéro binaire. Ensuite le constructeur de la classe de base est appelé. Dans ce cas, l'appel est automatique, mais on peut également spécifier le constructeur de la classe de base (comme la première opération dans le constructeur Beetle( )) en utilisant super. La constructeur de la classe de base suit le même processus dans le même ordre que le constructeur de la classe dérivée. Lorsque le constructeur de la classe de base a terminé, les variables d'instance sont initialisées dans l'ordre textuel. Finalement le reste du corps du constructeur est exécuté.

Résumé

L'héritage et la composition permettent tous les deux de créer de nouveaux types depuis des types existants. Typiquement, quoiqu'il en soit, on utilise la composition pour réutiliser des types existants comme partie de l'implémentation sous-jacente du nouveau type, et l'héritage quand on veut réutiliser l'interface. Étant donné que la classe dérivée possède l'interface de la classe de base, on peut faire un transtypage ascendant vers la classe de base, lequel est critique pour le polymorphisme, comme vous le verrez dans le prochain chapitre.

En dépit de l'importance particulièrement forte de l'héritage dans la programmation orienté objet, quand on commence une conception on devrait généralement préférer la composition durant la première passe et utiliser l'héritage seulement quand c'est clairement nécessaire. La composition tend à être plus flexible. De plus, par le biais de l'héritage, vous pouvez changer le type exact de vos objets, et donc, le comportement, de ces objets membres à l'exécution. Par conséquent, on peut changer le comportement d'objets composés à l'exécution.

Bien que la réutilisation du code à travers la composition et l'héritage soit utile pour un développement rapide, on voudra généralement concevoir à nouveau la hiérarchie de classes avant de permettre aux autres programmeurs d'en devenir dépendant. Votre but est une hiérarchie dans laquelle chaque classe a un usage spécifique et n'est ni trop grosse (englobant tellement de fonctionnalités qu'elle en est difficile à manier pour être réutilisée) ni trop ennuyeusement petite (on ne peut pas l'utiliser par elle-même ou sans ajouter de nouvelles fonctionnalités).

Exercices

Les solutions aux exercices sélectionnés peuvent être trouvées dans le document électronique The Thinking in Java Annotated Solution Guide, disponible pour un faible coût depuis www.BruceEckel.com.

  1. Créer deux classes, A et B, avec des constructeurs par défaut (liste d'arguments vide) qui s'annoncent eux-même. Faire hériter une nouvelle classe C de A, et créer une classe membre B à l'intérieur de C. Ne pas créer un constructeur pour C. Créer un objet d'une classe C et observer les résultats.
  2. Modifier l'exercice 1 afin que A et B aient des constructeurs avec arguments au lieu de constructeurs par défaut. Écrire un constructeur pour C et effectuer toutes les initialisations à l'intérieur du constructeur de C.
  3. Créer une simple classe. À l'intérieur d'une seconde classe, définir un champ pour un objet de la première classe. Utiliser l'initialisation paresseuse pour instancier cet objet.
  4. Hériter une nouvelle classe de la classe Detergent. Redéfinir scrub( ) et ajouter une nouvelle méthode appelée sterilize( ).
  5. Prendre le fichier Cartoon.java et enlever le commentaire autour du constructeur de la classe Cartoon. Expliquer ce qui arrive.
  6. Prendre le fichier Chess.java et enlever le commentaire autour du constructeur de la classe Chess. Expliquer ce qui se passe.
  7. Prouver que des constructeurs par défaut sont créés pour vous par le compilateur.
  8. Prouver que les constructeurs de la classe de base sont (a) toujours appelés et (b) appelés avant les constructeurs des classes dérivées.
  9. Créer une classe de base avec seulement un constructeur qui ne soit pas un constructeur par défaut, et une classe dérivée avec à la fois une constructeur par défaut et un deuxième constructeur. Dans les constructeurs de la classe dérivée, appeler le constructeur de la classe de base.
  10. Créer une classe appelée Root qui contient une instance de chaque classe (que vous aurez également créé) appelées Component1, Component2, et Component3. Dériver une classe Stem de Root qui contienne également une instance de chaque « component ». Toutes les classes devraient avoir un constructeur par défaut qui affiche un message au sujet de cette classe.
  11. Modifier l'exercice 10 de manière à ce que chaque classe ait des constructeurs qui ne soient pas des constructeurs par défaut.
  12. Ajouter une hiérarchie propre de méthodes cleanup( ) à toutes les classes dans l'exercice 11.
  13. Créer une classe avec une méthode surchargée trois fois. Hériter une nouvelle classe, ajouter une nouvelle surcharge de la méthode et montrer que les quatre méthodes sont disponibles dans la classe dérivée.
  14. Dans Car.java ajouter une méthode service( ) à Engine et appeler cette méthode dans main( ).
  15. Créer une classe à l'intérieur d'un package. Cette classe doit contenir une méthode protected. À l'extérieur du package, essayer d'appeler la méthode protected et expliquer les résultats. Maintenant hériter de cette classe et appeler la méthode protected depuis l'intérieur d'une méthode de la classe dérivée.
  16. Créer une classe appelée Amphibian. De celle-ci, hériter une classe appelée Frog. Mettre les méthodes appropriées dans la classe de base. Dans main( ), créer une Frog et faire un transtypage ascendant Amphibian, et démontrer que toutes les méthodes fonctionnent encore.
  17. Modifier l'exercice 16 de manière que Frog redéfinisse les définitions de méthodes de la classe de base (fournir de nouvelles définitions utilisant les mêmes signatures des méthodes). Noter ce qui se passe dans main( ).
  18. Créer une classe avec un champ static final et un champ final et démontrer la différence entre les deux.
  19. Créer une classe avec une référence final sans initialisation vers un objet. Exécuter l'initialisation de cette final sans initialisation à l'intérieur d'une méthode (pas un constructeur) juste avant de l'utiliser. Démontrer la garantie que le final doit être initialisé avant d'être utilisé, et ne peut pas être changé une fois initialisé.
  20. Créer une classe avec une méthode final. Hériter de cette classe et tenter de redéfinir cette méthode.
  21. Créer une classe final et tenter d'en hériter.
  22. Prouver que le chargement d'une classe n'a lieu qu'une fois. Prouver que le chargement peut être causé soit par la création de la première instance de cette classe, soit par l'accès à un membre static.
  23. Dans Beetle.java, hériter un type spécifique de coccinelle de la classe Beetle, suivant le même format des classes existantes. Regarder et expliquer le flux de sortie du programme.

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