Héritage, Classe/sous-classe
(référence: Chap. 4, Chap. 8)
Classe
Une classe est un modèle d'objets qui ont certaines caractéristiques en commun.
Une classe contient un ensemble d'objets possédant ces caractéristiques.
Dans une classe, on peut définir:
Forme typique de déclaration d'une classe: (pp. 140):
class <nom_classe> { <Déclaration des données> <Constructeurs> <Déclaration des méthodes> }
Exemple, voir la class Rectangle, pp. 142.
Sous-classe
Une sous-classe correspond à un modèle plus spécifique. En général, ce modèle plus spécifique est obtenu en ajoutant des attributs/méthodes.
Une sous-classe contient un sous-ensemble d'objets.
Remarque: Un objet qui appartient à une sous-classe appartient aussi à sa super-classe.
Syntaxe (pp. 287):
class <child-class> extends <parent-class> { <constructeurs> <déclaratyion des attributs/méthodes supplémentaires> }
Héritage
= mécanisme de transfère des propriétés d'une super-classe vers une sous-classe.
Un objet d'une sous-classe hérite les propriétés de la super-classe (sauf les constructeurs).
Exemple, pp. 290-291, super-classe Book et sous-classe Dictionary.
Différence entre héritage et utilisation.
Dans l'exemple de Book et Dictionary, il y a une relation d'héritage. A la page 291, la classe Words2 "utilise" la classe Dictionary. Il n'existe pas d'héritage entre Dictionary et Words2.
Accessibilité des attributs et des méthodes.
L'accessibilité peut dépendre de la notion de package. Il y aura une présentation plus tard sur cette notion. Pour l'instant, on peut simplement comprendre qu'un package regroupe un ensemble de classes qui traitent des problèmes du même genre. Pour le moment, on se contente de traiter l'accessibilité dans le même package.
On distingue deux cas.
1. Accès aux éléments de la super-classe à partir d'une sous-classe
modificateur |
||||
Où est la sous-classe? |
public |
protected |
(par défaut) |
private |
Dans le même package |
Oui |
Oui |
Oui |
Non |
Dans différents packages |
Oui |
Oui |
Non |
Non |
2. Accès en dehors d'une sous-classe
Même package |
Oui |
Oui |
Oui |
Non |
Différents packages |
Oui |
Non |
Non |
Non |
L'accès à partir d'une sous-classe correspond au cas où on veut accéder aux éléments de la super-classe dans la déclaration d'une sous-classe. Par exemple:
class C1 { int a; protected int b; private int c; } class C2 extends C1 { int d; C2() { a=0; b=0; // c=0; est interdit d=0; } }
Dans la déclaration de C2, on essaie d'accéder aux attributs a, b, et c de la super-classe. Cela correspond aux cas du premier tableau (dans une sous-classe).
Pour un autre exemple:
class C3 extends C1 { int d; C1 c_1; C3() { c_1 = new C1(); c_1.a = 0; c_1.b = 0; // c_1.c = 0; est interdit. } }
Les accès aux attributs a, b et c à partir de c_1 correspond aux cas d'accès en dehors d'une sous-classe parce qu'on essaie de le faire à partir d'une instance "utilisée" dans cette classe (même si on est encore dans la déclaration de la sous-classe C3).
Prenons l'exemple de School et Student, pp. 294-296.
Dans la classe Grad_Student, le constructeur
public Grad_Student( …)
utilise super(student_name, classes) pour faire appel au constructeur de la super-classe, (CAD: public Student (…) de la classe Student). L'exécution de ce constructeur a pour effet d'instancier les attributs "name" et "num_courses", déclarés dans la classe Student.
Questions de réflexion:
1. Peut-on ne pas utiliser super(…) dans cet exemple, mais utiliser directement les attributs "name" et "num_courses" comme suit:
name = student_name; num_courses = classes;
2. Dans main() …, on a:
sammy.info(); pete.info(); pete.support();
Peut-on aussi mettre les lignes suivantes à cet endroit?
System.out.println(sammy.name); System.out.println(pete.source);
3. Supposons une classe suivante en plus:
class Phd_Student extends Grad_Student { private String thesis_title; public Phd_Student (String student_name; int classes, String support_source, double hourly_rate, String title) { super(student_name, classes, support_source, hourly_rate); thesis_title = title; } }
Peut-on remplacer super(…) par les suivantes?
name = student_name; num_courses = classes; source = support_source; rate = hourly_rate;
La bonne façon de programmation O-O: On déclare autant que possible les variables locales comme private. Cela a pour effet de protéger ces variables des modifications et des accès non contrôlés de l'extérieur. Afin de permettre à un accès "contrôlé", on crée des méthodes d'accès, par exemple, get_… et set_….
Exemple:
class Student { private String name; private int num_courses; public Student(...) {...} public void info() {...} public String get_name() (return name; } public void set_name(String a_name) {name = a_name; } public int get_num_courses() {return num_courses;} public void set_num_courses(int classes) {num_courses = classes;} ... }
Il est alors possible d'ajouter certain contrôle sur les accès aux attributs. Par exemple, il serait possible d'interdire set_num_courses(6) si le règlement n'autorise pas un étudiant d'inscrire en même temps à 6 cours…
Constructeur
<nom_classe>() {}
Overloading / Overriding
Overloading réfère au phénomène que plusieurs version d'une méthode co-existent, mais ces méthodes ont des signatures différentes. La signature est une identification de méthode interne. Elle est créée; en fonction du nom de la méthode et les paramètres. Ainsi, il n'y a pas de conflit entre des méthodes de même nom avec des signatures différentes.
Un exemple de Overloading est la déclaration de plusieurs constructeurs dans une classe, mais avec des paramètres différents.
Overriding réfère au phénomène qu'une nouvelle version d'une méthode avec la même signature a été redéfinie dans une sous-classe. Par exemple:
class C1 { public int a; public C1(int x) { a = x; } public void print() { System.out.println("Version1" + a); } } class C2 extends C1 { public int b; public C2 (int x, int y) { super(x); b = y; } public void print() { System.out.println("Version2" + b); } }
Si on les utilise comme suit:
... main(...) { C1 c_1 = new C1(1); C2 c_2 = new C2(2, 3); c_1.print(); // donne "Version 1 1" c_2.print(); // donne "Version 2 3" ... }
Pour l'instance référée par c_2, la seule version de print() disponible est celle déclarée dans C2. L'ancienne version de C1 a été remplacée (overridden).
Polymorphisme
Cela réfère au phénomène que plusieurs déclarations (versions) des attributs co-existent dans un même objet. L'attribut utilisé dépend comment l'évocation est faite.
Par exemple:
class C1 { public int a; public C1(int x) { a = x; } } class C2 extends C1 { public int a; public int b; public C2 (int x, int y) { super(0); a = x; b = y; } }
Il y a une redéfinition de "int a" dans la classe C2. Voyons comment ces deux variables "a" peuvent être réféées.
C1 c_1; C2 c_2; c_1 = new C1(1); // c_1.a -> 1 c_2 = new C2(2,3); //c_2.a -> 2 c_1 = new C2(2,3); // c_1.a -> 0, la valeur 0 est mis dans a de classe C1 par super(0); // c_2 = new C1(1); est interdit.En réalité, une instance de C2 correspond à une structure de données comme suit:
qui contient deux déclarations de "a". Ces deux "a" correspondent à des places mémoire différentes. La "version" de "a" accédée est déterminée par la classe de la référence. Si on accède à partir d'une référence de la classe C1, alors on accède à la version déclarée dans C1 (c'est le cas de c_1.a). Si l'accès est fait à partir d'une référence de classe C2, alors on accède à la version de C2.
Casting
Étant donné que les deux versions co-existent, on peut donc manipuler les références pour les accéder. Il y a encore la façon de "casting" qui permet de modifier la classe d'une référence comme (C2) c_1 dans l'exemple qui suit:
c_1 = new C2(2,3); // c_1.a donne 0; Mais ((C2) c_1).a donne 2;
De même, on peut aussi faire l'affectation suivant (qui est interdit sans le casting:
c_2 = (C2) c_1; // cela est possible via casting. Mais une autre condition est que l'objet référé par c_1 soit réellement de la classe C2.
La condition pour "caster" une référence à une autre classe (qui peut être une sous-classe ou super-classe de la référence) est que l'objet référé soit de cette classe.
Une comparaison avec Overriding: Dans le cas de méthode, on a seulement une version de méthode disponible, contrairement au cas des attributs. La différence vient du fait que les méthodes sont implicitement "static", donc une seule copie par classe. Quand une sous-classe redéfinit une méthode, l'ancienne méthode n'existe plus (physiquement) pour les objets de cette sous-classe. Tandis que chaque objet possède sa propre copie des attributs non statiques.
Donc, dans l'exemple utilisé pour illustrer Overriding, même si on appelle ((C1) c_2).print() en utilisant du casting, la méthode est toujours celle définie dans C2.
This et super.
Ces deux "références" sont utilisées dans la déclaration des classes/sous-classes. "this" réfère à l'objet courant. Super réfère à la classe supérieure.
Exemple:
class date { int year; void setYear (int year) { this.year = year; } }
Dans la déclaration d'une classe, on peut avoir besoin de référer à des attributs/méthodes de cette classe. Comme il n'y a pas encore de référence créée, il est impossible d'utiliser <référence>.<attribut> pour cela. Mais on peut utiliser "this" pour référer à l'objet courant. Ainsi, this.
"super" peut être utilisé dans la déclaration d'une sous-classe de deux manières:
Exemple:
class C1 { String nom; print() { System.out.println(nom);} } class C2 extends C1 { String prenom; print() { super.print(); // appel à print() de C1 System.out.println(prenom); }
Package (Section 9.3)
Un package regroupe un ensemble de classes qui ont des fonctionnalités similaires. Par exemple, on peut regrouper toutes les classes qui traitent des objets graphiques. C'est un package déjà défini dans Java - le package awt. L'utilité principale de cette regroupement est de permettre à importer très rapidement un ensemble d'outils, ainsi d'établir un bon environnement pour un programme.
Définition d'un package
Un package contient seulement des classes. (Donc, pas de classe avec "main" dedans. ) Les classes qui sont à inclure dans un package doit être mises dans un fichier dont la première ligne spécifie le package comme suit:
package <nom_package>;
public class C1 {...}
Après compilation, la classe C1 sera mise dans le package <nom_package>.
Organisation des packages
Il existe aussi une hiérarchie dans les packages. La racine des packages prédéfinis en Java est "java". Quelques sous-packages dans "java" sont "lang", "awt", "util", etc. On peut voir ceci comme un arbre:
Dans cette figure, un nom en Majuscule correspond à une classe, et en minuscule à un package. Ainsi, on peut voir qu'un package peut contenir des sous-packages, et des classes.
Si on veut définir sa propre hiérarchie de packages, on peut également les organiser dans un autre arbre. Mais cette organisation doit correspondre à l'organisation physique des classes dans les répertoires. Par exemple, si on veut créer la hiérarchie de packages suivante:
Il faut que l'organisation des répertoires corresponde à la même hiérarchie. On doit avoir un répertoire "maRacine", dans lequel il y a 3 sous-répertoires "package1", "package2" et "package3", et ainsi de suite. Les classes incluses dans chaque package doivent être mises dans le répertoire correspondant. Par exemple, si la classe "C2" est dans le package2, le fichier C2.class (le résultat de compilation de cette classe) doit se trouver dans le répertoire "package2".
Utilisation d'un package
Dans un programme, on peut importer une classe ou toutes les classes d'un package comme suit:
import <package>.C1;
import <package>.*;
La première ligne importe juste la classe C1 du package, tandis que la seconde importe toutes les classes du package.
Pour importer un package, il faut que le package soit visible du compilateur (à l'endroit où le programme est compilé). Pour rendre un package visible partout, on peut inclure le chemin de ce répertoire dans une variable d'environnement CLASSPATH. On peut ajouter un chemin dans cette variable (qui se trouve dans le fichier .cshrc de votre compte) comme suit:
setenv CLASSPATH .:/u/nie/maRacine
Les chemins sont séparés par ":". En général, le répertoire courant "." est ajouté dans CLASSPATH. Dans cet exemple, je suppose que le répertoire "maRacine" se trouve dans mon compte "/u/nie/".
De cette façon, "maRacine" devient visible par tous les programmes Java. Pour importer un package de cette hiérarchie, il faut que la commande "import" spécifie le chemin pour arriver au package à partir du point qui est visible. Par exemple, dans un programme qui se trouve dans mon compte à la racine "/u/nie/", si je veux importer les classe du package11, il faut que j'écrive comme suit:
import package1.package11.*;
où "
package1.package11" spécifie le chemin pour trouver le package11. Dans cet exemple, je n'ai pas à spécifier "maRacine" parce que ce répertoire est déjà mis dans CLASSPATH, donc visible. Le compilateur fera une recherche dans "maRacine" pour trouver "package1", et ensuite "package11".Si un packages n'est pas mis dans CLASSPATH (ni dans une hiérarchie dont la racine est dans CLASSPATH), il est visible seulement dans le même répertoire. C'est-à-dire, si un programme essaie de l'importer, ce programme doit être compilé dans ce même répertoire que le package (car les packages sont visibles dans le même répertoire par défaut).