Troisième partie

publicité
U.F.R Mathématiques de la Décision
Programmation objet
Partie II
André Casadevall
Mai 2001
p. 2
AJ.C – 12 juin 2001
Table des matières
3 Héritage
3.1 Introduction - L’héritage, qu’est-ce que c’est ? . . . . . . . . . .
3.2 Retour sur les constructeurs et sur l’utilisation de this . . . .
3.2.1 Les constructeurs . . . . . . . . . . . . . . . . . . . . . .
3.2.2 Une autre utilisation de this . . . . . . . . . . . . . . .
3.3 Méthodes et héritage . . . . . . . . . . . . . . . . . . . . . . . .
3.3.1 Méthodes héritées - méthodes propres . . . . . . . . . .
3.3.2 Sur-classes, sous-classes . . . . . . . . . . . . . . . . . .
3.3.3 Redéfinition des méthodes d’instance . . . . . . . . . . .
3.3.4 Appel d’une méthode d’instance héritée et/ou redéfinie
3.3.5 Redéfinition des méthodes de classe . . . . . . . . . . .
3.3.6 Utilisation de super . . . . . . . . . . . . . . . . . . . .
3.4 Surcharge des méthodes . . . . . . . . . . . . . . . . . . . . . .
3.4.1 Surcharge v.s redéfinition . . . . . . . . . . . . . . . . .
3.4.2 Sélection et exécution d’une méthode . . . . . . . . . .
3.5 Variables d’instance et héritage . . . . . . . . . . . . . . . . . .
3.5.1 Accès aux variables d’instance . . . . . . . . . . . . . .
3.5.2 Des constructeurs pour les classes-fille - super( ) . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
5
5
6
6
10
11
11
12
13
16
18
19
22
22
25
28
28
32
4 Packages et règles d’accès
4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2 Utilisation et création de packages . . . . . . . . . . . . . . . .
4.2.1 Un peu de vocabulaire . . . . . . . . . . . . . . . . . . .
4.2.2 Accès aux classes d’un package . . . . . . . . . . . . . .
4.2.3 La variable d’environnement CLASSPATH . . . . . . . . .
4.2.4 Créer un package - Ajouter une classe à un package . .
4.3 Accès aux éléments d’une classe . . . . . . . . . . . . . . . . . .
4.3.1 Modificateurs usuels . . . . . . . . . . . . . . . . . . . .
4.4 Contrôle de la redéfinition et de l’héritage : final et abstract
4.4.1 Utilisation de final . . . . . . . . . . . . . . . . . . . .
4.4.2 Classes et méthodes abstraites . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
33
33
35
35
36
38
39
39
40
40
40
41
5 Exceptions
5.1 Des exceptions, pourquoi faire ? . . . . . . . . . .
5.1.1 Objectif des exceptions . . . . . . . . . . .
5.1.2 Cadre général de l’utilisation des exceptions
5.2 Les classes d’exception de java . . . . . . . . . . .
5.3 Exceptions et héritage . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
43
43
43
45
46
47
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
TABLE DES MATIÈRES
5.3.1
5.3.2
Erreurs multiples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Méthodes redéfinies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
6 Flux et fichiers
6.1 Introduction . . . . . . . . . . . . . . . . . . .
6.1.1 Flux de données . . . . . . . . . . . .
6.1.2 Fichiers . . . . . . . . . . . . . . . . .
6.2 Quelques classes de java.io . . . . . . . . . .
6.3 Exemples d’utilisation des classes de java.io
6.3.1 Fichiers de caractères . . . . . . . . .
6.3.2 Fichiers d’octets . . . . . . . . . . . .
6.3.3 Utilisation de IOException . . . . . .
6.3.4 Les flux in et out . . . . . . . . . . .
6.3.5 Fichiers d’objets . . . . . . . . . . . .
p. 4
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
49
49
49
50
52
53
53
54
55
56
57
AJ.C – 12 juin 2001
3
Héritage
Sommaire
3.1
3.2
Introduction - L’héritage, qu’est-ce que c’est ? . . . . . .
Retour sur les constructeurs et sur l’utilisation de this
3.2.1 Les constructeurs . . . . . . . . . . . . . . . . . . . . . . .
3.2.2 Une autre utilisation de this . . . . . . . . . . . . . . . .
3.3 Méthodes et héritage . . . . . . . . . . . . . . . . . . . . .
3.3.1 Méthodes héritées - méthodes propres . . . . . . . . . . .
3.3.2 Sur-classes, sous-classes . . . . . . . . . . . . . . . . . . .
3.3.3 Redéfinition des méthodes d’instance . . . . . . . . . . . .
3.3.4 Appel d’une méthode d’instance héritée et/ou redéfinie .
3.3.5 Redéfinition des méthodes de classe . . . . . . . . . . . .
3.3.6 Utilisation de super . . . . . . . . . . . . . . . . . . . . .
3.4 Surcharge des méthodes . . . . . . . . . . . . . . . . . . .
3.4.1 Surcharge v.s redéfinition . . . . . . . . . . . . . . . . . .
3.4.2 Sélection et exécution d’une méthode . . . . . . . . . . .
3.5 Variables d’instance et héritage . . . . . . . . . . . . . . .
3.5.1 Accès aux variables d’instance . . . . . . . . . . . . . . .
3.5.2 Des constructeurs pour les classes-fille - super( ) . . . .
3.1
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
. . . . . . .
. . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . .
. . . . . . . .
. . . . . . . .
5
6
6
10
11
11
12
13
16
18
19
22
22
25
28
28
32
Introduction - L’héritage, qu’est-ce que c’est ?
L’héritage est la propriété permet à une classe d’utiliser des ressources, des méthodes en particulier, qui sont définies dans une classe existante. La nouvelle classe est appelée classe-fille de la
classe existante (on dit aussi qu’elle hérite, qu’elle étends ou encore qu’elle dérive de cette dernière).
La classe initiale est naturellement appelée classe-mère de la nouvelle classe.
Nous avons rencontré dans les chapitres précédents des méthodes,equals ou toString par
exemple, qu’il est possible d’utiliser dans une classe alors même qu’elles n’y sont pas explicitement
définies. Ces deux méthodes sont en fait définies dans la classe Object : en java toutes les classes
dérivent de la classe Object.
L’héritage ne se limite pas à la seule réutilisation de méthodes. L’héritage est aussi un moyen
pour étendre le comportement d’une classe donnée en lui associant des classes-filles plus spécialisées :
– soit en ajoutant de nouvelles méthodes ;
– soit en modifiant certaines méthodes de la classe-mère ;
CHAPITRE 3. HÉRITAGE
– soit en ajoutant de nouvelles variables d’instance (ce qui aura une incidence sur les constructeurs de ces classes) ;
– soit enfin en combinant ces trois approches.
L’héritage est enfin, on le verra plus loin, un moyen de mettre en oeuvre la notion de type et
de la compléter : une classe-fille qui hérite d’une classe-mère partage avec celle-ci ses méthodes
(donc son interface) et ainsi que ses variables d’instance. Ce chapitre sera également l’occasion
d’approfondir la notion de surcharge qui bien qu’indépendante de la notion d’héritage, intervient
lorsqu’on met en oeuvre ce concept.
3.2
3.2.1
Retour sur les constructeurs et sur l’utilisation de this
Les constructeurs
Examinons la classe suivante :
Exemple 3.2.1 :
1
2
3
public class Personne {
private int age ;
private String nom ;
4
public String toString() {
return nom + " - " + age + " ans" ;
}
5
6
7
8
1
2
3
4
5
6
}
public class Test {
public static void main(String[] args) {
Personne a = new Personne() ;
System.out.println(a) ;
}
}
Remarquons tout d’abord que la classe Personne ne possède pas de constructeur. L’exécution
de Test produit l’affichage :
null - 0 ans
Commentons ce résultat :
– À la ligne 3 on a créé un objet Personne.
– La ligne 4 affiche les valeur des des variables variables d’instance de l’objet créé :
– age est affectée de la valeur 0 ;
– non est affectée de la valeur null.
Règle :
En l’absence de constructeur explicite, java utilise un constructeur générique qui affecte des
valeurs par défaut aux variables d’instance de l’objet créé :
0
types numériques
f alse
boolean
char
caractère 0 u\00000
null
référence à des objets
p. 6
AJ.C – 12 juin 2001
3.2. RETOUR SUR LES CONSTRUCTEURS ET SUR L’UTILISATION DE THIS
Exemple 3.2.2 :
Modifions le constructeur à la classe Personne :
1
2
3
public class Personne {
private int age ;
private String nom ;
4
public Personne(int n, String s) {
age = n ;
nom = s ;
}
public String toString() {
return nom + " - " + age + " ans" ;
}
5
6
7
8
9
10
11
12
}
Exécutons à nouveau Test : une erreur est alors signalée à la ligne 3 :
No constructor matching Personne() found in class Personne
Règle :
Dès qu’un constructeur explicite est défini, le mécanisme du constructeur générique est supprimé.
Exemple 3.2.3 :
Remplaçons les lignes 3 et 4 de Test par :
Personne b = new Personne(7,"Bozo") ;
System.out.println(b) ;
3
4
Exécutons Test à nouveau : on obtient alors le résultat attendu : Bozo - 7 ans.
Réintroduisons dans la classe Personne le constructeur non paramétré défini précédemment :
1
2
3
public class Personne {
private int age ;
private String nom ;
4
public Personne() {
}
public Personne(int n, String s) {
age = n ;
nom = s ;
}
public String toString() {
return nom + " - " + age + " ans" ;
}
5
6
7
8
9
10
11
12
13
14
}
AJ.C – 12 juin 2001
p.7
CHAPITRE 3. HÉRITAGE
Exécutons la classe Test modifiée :
1
2
3
4
public class Test {
public static void main(String[] args) {
Personne a = new Personne() ;
System.out.println(a) ;
5
Personne b = new Personne(7,"Bozo") ;
System.out.println(b) ;
6
7
}
8
9
}
On obtient :
null - 0 ans
Bozo - 7 ans
Signature d’un constructeur, d’une méthode
Ainsi que le montrent les exemples précédents, une classe peut comporter plusieurs constructeurs. Ils se distinguent alors par la liste des type des paramètres utilisés lors de la définition
du constructeur ou paramètres formels. Cette liste est la signature du constructeur. Plus
précisément, la signature d’une méthode (et en particulier celle d’un constructeur) est constituée du nom de la méthode suivie de la liste ordonnée des types des paramètres formels utilisés
lors de sa définition. La signature des deux constructeurs de la classe Personne est respectivement
Personne () et Personne (int, String).
On ne peut avoir simultanément deux constructeurs non-paramétrés puisqu’ils auraient la même
signature. Par contre les deux constructeurs suivants :
public Personne(int n, String s) {
age = n ;
nom = s ;
}
public Personne(String s, int n) {
age = n ;
nom = s ;
}
peuvent coexister puisque la signature du premier est Personne (int, String), alors que la signature du second est Personne (String, int).
Remarquons de plus que le constructeur non-paramétré Personne() tel qu’il a été défini à la
ligne 5, joue un rôle semblable à celui du constructeur générique : les variables nom et age ont été
affectées des valeurs null et 0. Ce comportement était parfaitement prévisible, le corps de ce constructeur ne comportant aucune affectation (explicite ou implicite) de valeur aux variables
nom ou age.
Règle :
– Les constructeurs d’une classe doivent différer par leur signature.
– Lorsqu’il n’y a pas d’affectation de valeur (explicite ou implicite) à une variable d’instance,
celle-ci est initialisée avec pour valeur par défaut :
p. 8
AJ.C – 12 juin 2001
3.2. RETOUR SUR LES CONSTRUCTEURS ET SUR L’UTILISATION DE THIS
types numériques
0
f alse
boolean
caractère 0 u\00000
char
null
référence à des objets
Exemple 3.2.4 :
Dans le contexte de la classe Personne voici des exemples qui illustrent l’ensemble des situations
possibles :
– Pas de paramètre, pas d’affectation (donc valeurs par défaut) :
public Personne() {
System.out.println("pouic") ;
}
La séquence
Personne a = new Personne() ;
System.out.println(a) ;
affiche alors :
pouic
null - 0 ans
– Pas de paramètre, mais affectation d’une valeur (nécessairement) constante :
public Personne() {
nom = "Bozo" ;
}
La séquence
Personne a = new Personne() ;
System.out.println(a) ;
affiche alors :
Bozo - 0 ans
La valeur par défaut null a été remplacée par Bozo qui devient la valeur d’initialisation de
la variable nom.
– Un paramètre ou des paramètres :
public Personne(String s) {
nom = s ;
}
La séquence
Personne a = new Personne("Toto") ;
System.out.println(a) ;
affiche alors :
Toto - 0 ans
La variable age n’est pas initialisée, elle prend donc la valeur null.
– Le cas où le constructeur serait paramétré, mais où il n’y aurait pas d’affectation de valeur
aux variables d’instances n’a pas beaucoup d’intérêt . . .
AJ.C – 12 juin 2001
p.9
CHAPITRE 3. HÉRITAGE
3.2.2
Une autre utilisation de this
On a déjà vu que this remplace l’objet appelant pour lever une ambiguı̈té qui pourrait conduire
à une erreur. En voici un exemple :
Exemple 3.2.5 :
Le constructeur Personne(int n, String s) défini ci-dessus aurait pu être écrit sous la forme :
public Personne(int age, String nom) {
this.age = age ;
this.nom = nom ;
}
pour distinguer la variable locale nom ou age de la variable d’instance homonyme.
Une autre utilisation de this est liée à la définition des constructeurs comme le montre l’exemple
suivant :
Exemple 3.2.6 :
1
2
3
public class Personne {
private int age ;
private String nom ;
4
public Personne() {
}
public Personne(String s) {
nom = s ;
}
public Personne(int n, String s) {
this(s) ;
age = n ;
}
public String toString() {
return nom + " - " + age + " ans" ;
}
5
6
7
8
9
10
11
12
13
14
15
16
17
}
Commentons l’utilisation de this dans Personne(int n, String s) (ligne 11) ; this(s) cherche
à appliquer un constructeur (s’il l’en existe un) de la classe Personne qui soit paramétré par un
String (autrement dit, un constructeur de signature Personne (String)). Cette propriété est celle
du constructeur Personne(String s) ; this(s) crée donc un objet Personne dont la variable nom
est initialisée avec la valeur de s et dont la variable age vaut 0.
Dans ce contexte, this(s) est en quelque sorte équivalent à :
this = new Personne(s)
(this ne peut bien sur être utilisé ainsi). Cette “équivalence” a pour conséquence qu’il n’est pas
possible d’inverser l’ordre des deux lignes 11 et 12 ; en effet this(s) recréerait un nouvel objet qui
écraserait l’objet en cours de création.
De même, this() appliquerait à l’objet appelant le constructeur de la classe Personne de la forme
Personne(), créant ainsi un objet Personne dont la variable nom serait initialisée à null et la
variable age à 0.
Règle :
p. 10
AJ.C – 12 juin 2001
3.3. MÉTHODES ET HÉRITAGE
Utilisation de this :
– this, sans paramètre, remplace une variable faisant référence à l’objet appelant ;
– lorsqu’il apparaı̂t toujours à la premiere ligne d’un constructeur d’une classe Cc,
this(liste de paramètres )
applique (s’il existe) le constructeur
Cc(liste de paramètres )
3.3
3.3.1
Méthodes et héritage
Méthodes héritées - méthodes propres
Exemple 3.3.1 :
Dans cet exemple on définit les classes Nom, NomPlus qui se déduit de Nom en utilisant l’héritage, et
une classe TestNom qui met en oeuvre les deux premières.
1
2
3
4
5
public class Nom {
public String affiche() {
return "Bozo" ;
}
}
6
7
8
9
10
11
public class NomPlus extends Nom {
public String affichePlus() {
return "Oum" ;
}
}
Dans l’en-tête de la classe NomPlus, extends Nom indique que la classe NomPlus est une classe-fille
de Nom.
Exécutons la classe TestNom :
1
public class TestNom {
2
public static void main(String[] args) {
Nom n = new Nom() ;
System.out.println(n.affiche()) ;
NomPlus m = new NomPlus() ;
System.out.println(m.affichePlus()) ;
System.out.println(m.affiche()) ;
// System.out.println(n.affichePlus()) ;
n = m ;
// m = n ; provoque une erreur
}
3
4
5
6
7
8
9
10
11
12
13
provoque une erreur
}
AJ.C – 12 juin 2001
p.11
CHAPITRE 3. HÉRITAGE
On obtient l’affichage :
Bozo
Oum
Bozo
Cet exemple montre que les objets NomPlus peuvent utiliser la méthode affiche de la classe Nom.
Cette méthodes est une méthode héritée de la classe-mère Nom. Par contre les objets de la classe
Nom ne peuvent utiliser la méthode affichePlus (ligne 9) qui est une méthode propre de la classe
NomPlus.
On constate également que la référence à des objets NomPlus peut être affectée à des objets Nom
la réciproque (ligne 11) conduisant à une erreur. C’est une situation que nous avons déjà rencontrée :
Nomplus définit un sous-type de Nom. Mais contrairement à ce qui se passe pour les interfaces, une
classe ne peut être la classe-fille que d’une seule classe.
Règles :
– extends Cc dans l’en-tête d’une classe indique que cette classe est une classe-fille de la classe
Cc.
– Une classe ne peut ne peut être la classe-fille que d’une seule classe (classe-mère).
– Les méthodes de la classe-mère sont accessibles aux objets de la classe-fille (méthodes
héritées).
– Les méthodes de la classe-fille ne sont pas accessibles aux objets de la classe-mère (méthodes
propres à la classe-fille).
– Une classe-fille définit un sous-type de la classe-mère. On peut affecter la référence d’un
objet de la classe-fille, à une variable dont le type est celui de la classe-mère.
Lorsque Bb est une classe-fille de Aa on dit que Bb étend Aa, que Bb dérive de Aa ou encore que
Bb hérite de Aa.
3.3.2
Sur-classes, sous-classes
Créons une nouvelle classe NomPlusPlus qui hérite de NomPlus
Exemple 3.3.2 :
1
2
3
4
5
public class NomPlusPlus extends NomPlus {
public String affichePlusPlus() {
return "Gus" ;
}
}
et complétons TestNom :
1
public class TestNom {
2
3
4
5
public static void main(String[] args) {
Nom n = new Nom() ;
System.out.println(n.affiche()) ;
6
NomPlus m = new NomPlus() ;
7
p. 12
AJ.C – 12 juin 2001
3.3. MÉTHODES ET HÉRITAGE
8
9
System.out.println(m.affichePlus()) ;
System.out.println(m.affiche()) ;
10
11
12
13
14
15
16
NomPlusPlus p = new NomPlusPlus() ;
System.out.println(p.affichePlusPlus()) ;
System.out.println(p.affichePlus()) ;
System.out.println(p.affiche()) ;
m = p ;
n = p ;
Cette nouvelle version de TestNom affiche :
Bozo
Oum
Bozo
Gus
Oum
Bozo
Tout ceci montre que la classe NomPlusPlus peut utiliser les méthodes de la classe NomPlus mais
aussi les méthodes de la classe Nom. De même la référence à des objets NomPlusPlus peut être
affectée à des objets Nom et NomPlus.
Règle :
La relation “Bb dérive de Aa” est une une relation transitive.
– Les classes-filles des classes-filles . . . des classes-filles d’une classe Cc constituent les sousclasses de Cc.
– Les classes-mères des classe-mères . . . des classes-mères de Cc constituent les sur-classes de
Cc.
Puisqu’une classe ne peut avoir qu’une seule classe-mère, l’ensemble des sur-classes et des sousclasses d’une classe Cc donnée possède une structure d’arbre
Les règles suivantes étendent par transitivité les propriétés des classes-mères et des classes-filles
aux sur-classes et aux sous-classes d’une classe donnée.
Règle :
Soit Cc une classe :
– Les méthodes de toute sur-classe de Cc sont accessibles aux objets de Cc (méthodes héritées).
– Les objets de Cc ne peuvent accéder à aucune des méthodes des sous-classes de Cc.
– Les sous-classes de Cc définissent des sous-types du type défini par Cc.
3.3.3
Redéfinition des méthodes d’instance
Nous allons voir qu’il est possible de redéfinir dans une classe Cc une méthode déjà définie dans
une sur-classe de Cc, les deux versions ayant la même signature. Ceci est en fait parfaitement
légal puisque Cc et sa sur-classe sont deux classes distinctes.
AJ.C – 12 juin 2001
p.13
CHAPITRE 3. HÉRITAGE
sur-classe
sous-classe
Object
Cc
Fig. 3.1: Arbre des classes
Exemple 3.3.3 :
Créons une nouvelle version de NomPlusPlus dans laquelle la méthode affiche est redéfinie c’est à
dire que la méthode affiche déjà définie dans la classe Nom est définie à nouveau dans la sous-classe
NomPlusPlus :
1
2
3
4
5
public class Nom {
public String affiche() {
return "Bozo" ;
}
}
6
7
8
9
10
11
12
13
14
15
16
public class NomPlus extends Nom {
public String affichePlus() {
return "Oum" ;
}
}
public class NomPlusPlus extends NomPlus {
public String affiche() {
return "Toto" ;
}
}
p. 14
AJ.C – 12 juin 2001
3.3. MÉTHODES ET HÉRITAGE
Exécutons TestNom :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TestNom {
public static void main(String[] args) {
Nom n = new Nom() ;
System.out.println(n.affiche()) ;
NomPlus m = new NomPlus() ;
System.out.println(m.affichePlus()) ;
System.out.println(m.affiche()) ;
NomPlusPlus p = new NomPlusPlus() ;
System.out.println(p.affichePlus()) ;
System.out.println(p.affiche()) ;
n = p ;
// System.out.println(n.affichePlus()) ;
System.out.println(n.affiche()) ;
System.out.println(((Nom)p).affiche()) ;
}
}
On obtient l’affichage suivant :
Bozo
Oum
Bozo
Oum
Toto
Toto
Toto
Commentons cet affichage :
– À la ligne 3 on crée un objet Nom. La méthode affiche utilisée à la ligne 4 est celle de la
classe Nom, d’où l’affichage de Bozo.
– À la ligne 5 on crée un objet NomPlus. La méthode affichePlus utilisée ligne 6 est celle de
la classe NomPlus, d’où l’affichage de Oum.
– À la ligne 7 la méthode affiche est la méthode héritée de la classe Nom, d’où l’affichage de
Bozo.
– À la ligne 8 on crée un objet NomPlusPlus. La méthode affichePlus utilisée ligne 9 est la
méthode héritée de la classe NomPlus, d’où l’affichage de Oum.
– À la ligne 10 la méthode affiche est la méthode redéfinie dans NomPlusPLus, d’où l’affichage
de Toto.
– La ligne 12 provoque une erreur de compilation :
Error
: Method affichePlus() not found in class Nom.
TestNom.java line 12
System.out.println(n.affichePlus()) ;
En effet n est une variable de type Nom alors que affichePlus d”finie dans NomPlus n’est pas
une méthode accessible aux variables de la classe Nom.
– La ligne 13 est syntaxiquement exacte puisque n est une variable de type Nom et que la méthode
affiche est définie dans cette classe. Cependant n fait référence à un objet NomPlusPlus et
c’est alors la méthode affiche redéfinie dans NomPlusPlus qui est utilisée, d’où l’affichage
de Toto.
AJ.C – 12 juin 2001
p.15
CHAPITRE 3. HÉRITAGE
– La ligne 14 est un essai infructueux pour forcer l’utilisation de la méthode affiche de Nom
par un objet de la classe NomPlusPlus. La méthode utilisée dépend bien de l’objet appelant
et non pas du type de la variable qui référence cet objet.
L’utilisation d’un cast dans ce contexte provoque un message d’attention :
Warning : The cast from NomPlusPlus to Nom is possibly unnecessary.
TestNom.java line 14
System.out.println(((Nom)p).affiche()) ;
On vera plus loin que la “bonne méthode” consiste en l’utilisation de super.
Méthodes d’instance accessibles
En java toute variable et plus généralement toute expression, possède un type qui est un type
fondamental ou celui d’une classe ou d’une interface. Nous ne nous intéresserons qu’aux variables
(ou aux expressions) dont le type est celui d’une classe ou d’une interface puisque ce sont celles qui
peuvent apparaı̂tre comme préfixe dans l’appel d’une méthode d’instance.
On dira qu’une méthode d’instance est accessible à une variable (ou une expression) lorsque le
type de la variable (ou de l’expression) est :
– le type de la classe ou de l’interface où est définie la méthode d’instance ;
– un sous-type de ce dernier.
Réciproquement on dira qu’une variable (ou une expression) et une méthode sont compatibles
lorsque la méthode est accessible à cette variable (ou à cette expression). Par abus de langage on
dira parfois “méthode accessible à un objet o, . . . ” au lieu de “méthode accessible à la variable
qui fait référence à o, . . . ”.
Exemple 3.3.4 :
Reprenons les classes de l’exemple précédent. Elles définissent trois types :
– Nom - la seule méthode accessible est affiche()
– NomPlus qui est un sous-type de Nom - les méthodes accessibles sont
– affiche() héritée de Nom
– affichePlus()
– NomPlusPlus qui est un sous-type de NomPlus - les méthodes accessibles sont
– affichePlus() héritée de NomPlus
– affichePlus() redéfinie dans NomPlusPlus().
A la ligne 12 on définit n comme variable de type Nom et seule affiche() est accessible, d’où
l’erreur provoquée par la ligne 13. A la ligne 14 (Nom)p est une expression de type Nom et
((Nom)p).affiche() ne provoque pas d’erreur, il en serait de même pour l’appel de méthode
(new NomPlus()).affiche() (qui retourne Bozo).
3.3.4
Appel d’une méthode d’instance héritée et/ou redéfinie
Avant de voir comment java sélectionne la méthode qui sera effectivement utilisée, précisons
le mécanisme utilisé par java lors de l’appel d’une méthode.
Paramètres-effectifs
Appeler une méthode c’est fournir aux paramètres-formels (les paramètres qui ont servi à la
définition de la méthode) les valeurs nécessaires à l’exécution des instructions qui forment le corps
de la méthode. Ces valeurs sont appelées paramètres-effectifs ou arguments de l’appel.
p. 16
AJ.C – 12 juin 2001
3.3. MÉTHODES ET HÉRITAGE
Exemple 3.3.5 :
Complétons la classe Nom par la méthode printN :
1
2
3
4
5
6
7
public void printN(int n, String s) {
String result = s ;
for (int i = 0 ; i<n ; i++)
result += s ;
return result ;
System.out.println(result) ;
}
Dans la classe TestNom on ajoutera la ligne :
8
n.printN(3, "abc")
– les paramètres formels de printN sont n et s,
– sa signature est printN(int, String).
– lors de l’appel de cette méthode (ligne 8) les paramètres-effectifs ou arguments de l’appel sont
3 et "abc".
Appel de méthode
Un appel de méthode est le texte constitué par le nom de la méthode suivi de la liste des
paramètres effectifs : printN(3, "abc") dans l’exemple ci-dessus.
La signature d’un appel de méthode est formée du nom de la méthode suivi par la liste des
types des paramètres-effectifs : printN(int, String) dans le même exemple.
Compatibilité d’un appel de méthode
Lorsqu’une méthode est appelée (supposons pour l’instant que cette méthode existe), le compilateur effectue les taches suivantes :
– il vérifie que le nombre des arguments de l’appel est identique au nombre des paramètresformels ;
– ensuite, si ce nombre est strictement positif, il cherche à associer à chaque argument un (et un
seul) paramètre-formel : le mode d’association est simplement fondé sur l’ordre d’apparition
des paramètres : le premier argument est associé au premier paramètre-formel, le deuxième
argument est associé au deuxième paramètre-formel, . . . (dans l’exemple, 3 est associé à i,
"abc" est associé à s) ;
– la valeur de chaque argument est alors transmise au paramètre-formel correspondant par une
opération (implicite à l’appel de la méthode) qui en java est l’affectation de valeur :
paramètre-formel = argument
Sous-typage et promotion numérique étendent la validité de cette affectation aussi il est nécessaire
de préciser les conditions suivant lesquelles un appel de méthode est compatible avec une méthode,
plus précisément avec la signature d’une méthode. Un appel de méthode est compatible avec la
signature d’une une méthode lorsque :
– le nom figurant dans l’appel est identique au le nom apparaissant dans la signature de la
méthode ;
– le nombre des arguments est égal au nombre des types de paramètres apparaissant dans la
signature ;
AJ.C – 12 juin 2001
p.17
CHAPITRE 3. HÉRITAGE
– lorsque ce nombre est strictement positif, chaque argument doit être compatible pour l’affectation avec le paramètre-formel auquel il est associé :
– si les paramètres appartiennent à des types numériques, la promotion est possible (dans
l’exemple, la méthode printN de signature printN (int, String) peut être appelée
avec un argument byte) ;
– si les paramètres sont des références à des objets, le type de l’argument doit être un
sous-type du type du paramètre-formel.
Sélection d’une méthode d’instance héritée et/ou redéfinie
Considérons une suite de classes C0 , C1 , · · · , CN telles que pour tout i (1 ≤ i ≤ N ), Ci dérive
de Ci−1 . Supposons qu’une méthode d’instance soit définie dans C0 et soit éventuellement redéfinie
dans certaines des classes Ci . La exécution et le résultat de l’appel de cette méthode dépend des
deux étapes successives suivantes :
1. typage statique : Le compilateur détermine le type de la variable (ou de l’expression)
préfixant l’appel de méthode :
– il existe parmi les méthodes accessibles par cette variable (ou cette expression) une
méthode dont la signature est compatible avec l’appel de méthode, alors le processus se
continue à l’étape 2 ;
– sinon la compilation s’arrête avec un message d’erreur.
2. exécution dynamique : La variable (ou l’expression) préfixant l’appel de méthode fat
référence à un objet (objet appelant) dont le compilateur détermine la classe :
– si la méthode appelée est définie dans cette classe, elle est exécutée,
– sinon java cherche dans la classe-mère de la classe, puis éventuellement dans la classemère de la classe-mère . . . en remontant dans l’arbre des classes jusqu’à trouver la
méthode qui sera exécutée ; cette méthode est la méthode “la plus proche” parmi
les méthodes de même signature définies dans l’ensemble des sur-classes de la classe de
l’objet appelant.
3.3.5
Redéfinition des méthodes de classe
Examinons maintenant le cas des méthodes de classe (caractérisées par le mot static).
Modifions les classesNom, NomPlus et TestNom en remplaçant les méthodes d’instance affiche et
affichePlus par des méthodes de classe :
Exemple 3.3.6 :
1
2
3
4
5
public class Nom {
public static String msg() {
return "Bozo" ;
}
}
6
7
8
9
10
public class NomPlus extends Nom {
public static String msg() {
return "Oum" ;
}
p. 18
AJ.C – 12 juin 2001
3.3. MÉTHODES ET HÉRITAGE
Exécutons la classe TestNom :
1
2
3
4
5
6
public class TestNom {
public static void main(String[] args) {
System.out.println(Nom.msg()) ;
System.out.println(NomPlus.msg()) ;
}
}
On obtient l’affichage :
Bozo
Oum
Le processus de sélection des méthodes de classe est particulièrement simple : le compilateur
cherche dans la classe dont le nom préfixe l’appel de méthode une méthode de classe dont la
signature est compatible avec l’appel de méthode :
– si cette méthode existe elle est exécutée ;
– sinon la compilation s’arrête avec un message d’erreur.
3.3.6
Utilisation de super
La sélection de la méthode à utiliser est plus simple pour les méthodes de classe que pour les
méthodes d’instance ; nom de la classe dans laquelle est définie la méthode apparaı̂t explicitement
dans l’appel d’une méthode de classe. Pour les méthodes d’instance super permet d’orienter le
choix du compilateur afin utiliser la méthode définie dans une sur-classe à la place de celle définie
dans la classe considérée.
super s’utilise d’une façon assez proche de celle de this. Modifions une fois encore les classes Nom,
NomPlus et NomPlusPlus.
Exemple 3.3.7 :
1
2
3
4
5
6
7
8
public class Nom {
public String affiche() {
return "Bozo" ;
}
public String printN() {
return "BOZO" ;
}
}
9
10
11
12
13
14
15
16
17
public class NomPlus extends Nom {
public String affiche() {
return "Oum" ;
}
public String affiche1() {
return super.affiche() ;
}
}
18
19
20
public class NomPlusPlus extends NomPlus {
public String printN() {
AJ.C – 12 juin 2001
p.19
CHAPITRE 3. HÉRITAGE
return "GUS" ;
}
public String affiche2() {
return super.printN() ;
}
21
22
23
24
25
26
}
Exécutions la classe TestNom :
1
public class TestNom {
2
public static void main(String[] args) {
Nom n = new Nom() ;
System.out.println(n.affiche()) ;
System.out.println(n.printN()) ;
3
4
5
6
7
NomPlus m = new NomPlus();
System.out.println(m.affiche1()) ;
8
9
10
NomPlusPlus p = new NomPlusPlus();
System.out.println(p.affiche2()) ;
11
12
}
13
14
}
on obtient :
Bozo
BOZO
Bozo
BOZO
Commentons ces résultats :
– Le premier Bozo résulte de println(n.affiche()) de la ligne 5.
– Le deuxième BOZO résulte de println(n.printN()) de la ligne 6.
– Le Bozo suivant résulte de println(m.affiche1()) de la ligne 9. Dans affiche1, le résultat
montre que super.affiche() (ligne 15 de NomPlus) fait référence à la méthode affiche()
de la classe Nom et non à celle de la classe NomPlus. Tout se passe donc comme si la ligne 15
avait été écrite :
Nom n = transformeEnNom(this) ; return n.affiche() ;
en forçant le compilateur à considérer l’objet appelant comme un objet de la classe Nom et non
pas comme un objet de la classe NomPlus. Bien évidemment, la méthode transformeEnNom
n’existe pas sous cette forme ; c’est super qui réalise cette transformation.
Remarquons enfin que si on avait remplacé cette ligne 15 par :
Nom n = (Nom)this ; return n.affiche() ;
le résultat aurait été Oum en raison de l’aspect dynamique de l’exécution d’une méthode (c.f.
Règles de sélection des méthodes d’instance).
– Le dernier affichage résulte de println(p.affiche2()) et p est un objet NomPlusPlus.
super.printN() (ligne 24 de NomPlusPlus) fait référence à la méthode printN() de la classe
Nom et non à celle de la classe NomPlusPlus et retourne BOZO
p. 20
AJ.C – 12 juin 2001
3.3. MÉTHODES ET HÉRITAGE
Dans la suite nous verrons une autre utilisation de super en relation avec les constructeurs.
Règle :
Utilisation de super dans le corps d’une méthode d’instance :
– Lorsqu’une classe Cc contient une méthode m( . . . ) qui redéfinit une méthode de la classemère de Cc, super.m( . . . ) applique à l’objet appelant la méthode m( . . . ) définie dans la
classe-mère et non la méthode redéfinie dans Cc.
– Si la méthode m( . . . ) n’existe pas dans la classe-mère, java chercher à appliquer la méthode
de même signature “la plus proche” définie dans l’ensemble des sur-classes de Cc
Remarque :
!!!
Méthodes utilisant des méthodes héritées
Modifions très légèrement la classe Nom, les autres classes restant inchangées.
Exemple 3.3.8 :
1
2
3
4
5
6
7
8
9
public class Nom {
public String affiche() {
return "Bozo" ;
}
public String printN() {
return this.affiche().toUpperCase() ;
// en remplacement de return "BOZO" ;
}
}
10
11
12
13
14
15
16
17
18
public class NomPlus extends Nom {
public String affiche() {
return "Oum" ;
}
public String affiche1() {
return super.affiche() ;
}
}
19
20
21
22
23
24
25
26
27
public class NomPlusPlus extends NomPlus {
public String printN() {
return "GUS" ;
}
public String affiche2() {
return super.printN() ;
}
}
Exécutons la même classe testNom :
AJ.C – 12 juin 2001
p.21
CHAPITRE 3. HÉRITAGE
1
2
3
4
5
public class TestNom {
public static void main(String[] args) {
Nom n = new Nom() ;
System.out.println(n.affiche()) ;
System.out.println(n.printN()) ;
6
NomPlus m = new NomPlus();
System.out.println(m.affiche1()) ;
7
8
9
NomPlusPlus p = new NomPlusPlus();
System.out.println(p.affiche2()) ;
10
11
}
12
13
}
On obtient :
Bozo
BOZO
Bozo
GUS
Commentons ces résultats :
– Le premiers et troisième affichages sont identiques à ceux obtenus dans l’exemple précédent ;
ceci est bien normal puisque nous n’avons pas modifié la méthode affiche() de la classe Nom.
– Le second affichage, BOZO, résulte de this.affiche().toUpperCase() La chaı̂ne de
caractères Bozo retournée par this.affiche() est transformée en majuscules par
toUpperCase().
– Arrêtons nous sur le dernier affichage qui lui est modifié : on obtient GUS au lieu du BOZO
obtenu dans l’exemple précédent bien que la classe NomPlusPlus n(ait pas été modifiée :
– p.affiche2() fait référence à la méthode printN() de la classe Nom puisque printN
n’est pas définie dans NomPlus. Java exécute donc (p.affiche()).toUpperCase().
Mais puisque p fait référence à un objet de la classe NomPlusPlus, c’est la méthode
affiche définie dans NomPlusPlus qui est exécutée ; p.affiche() retourne GUS. Finalement p.affiche2() retourne GUS.
3.4
3.4.1
Surcharge des méthodes
Surcharge v.s redéfinition
On parle de surcharge lorsque deux méthodes portant le même nom (méthodes homonymes),
sont définies dans la même classe et ne diffèrent que par leur signature (nom de la méthode suivi
de la liste ordonnée des types des paramètres utilisés pour sa définition). On parle de redéfinition
lorsque deux méthodes portant le même nom sont définies dans deux classes distinctes dont
l’une est une sur-classe de l’autre, et ont la même liste de paramètres formels.
() Remarquons que nous avons déjà rencontré ce problème à propos des constructeurs qui pour une
classe donnée portent nécessairement le même nom et ne se distinguent que par leur signature.
p. 22
AJ.C – 12 juin 2001
3.4. SURCHARGE DES MÉTHODES
Exemple 3.4.1 :
Modifions les classes Nom et NomPlus de la section précédente :
1
2
3
4
5
public class Nom {
public void affiche() {
System.out.println( "Bozo") ;
}
}
6
7
8
9
10
11
public class NomPlus extends Nom {
// la méthode affiche est redéfinie
public void affiche() {
System.out.println( "Oum");
}
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// les méthodes print sont surchargées
public void print() {
System.out.println( "Gus") ;
}
public String print(String s) {
return ("String : "+s) ;
}
public void print(double i, String s) {
System.out.println( "double first : " + i) ;
}
public void print(String s, double i) {
System.out.println("String first : "+s) ;
}
}
Exécutons la classe Test suivante :
1
2
3
4
5
6
7
8
9
10
public class Test {
public static void main(String[] args) {
NomPlus p = new NomPlus() ;
p.affiche() ;
p.print() ;
System.out.println(p.print( "Toto")) ;
p.print("aa", 1) ;
p.print(2, "bb") ;
}
}
On obtient :
Oum
Gus
String : Toto
String first : aa
double first : 2
AJ.C – 12 juin 2001
p.23
CHAPITRE 3. HÉRITAGE
Commentons :
– p est un objet NomPlus, p.affiche() utilise donc la méthode redéfinie affiche de la classe
NomPlus.
– Les affichages suivants sont le résultat de l’appel des différentes versions de la méthode surchargée print. Elles diffèrent par leur signature qui dans l’ordre de définition des méthodes,
sont :
– print()
– print(String)
– print(double, String)
– print(String, double)
On notera que le type de la valeur renvoyée par la méthode n’intervient pas dans le processus
de sélection de la méthode print utilisée ; seuls sont intervenu le type, le nombre et l’ordre
des paramètres.
Surcharge simple - Surcharge implicite
Le phénomène de surcharge peut apparaı̂tre de façon implicite (cachée) en conjonction avec
l’héritage comme dans l’exemple suivant :
Exemple 3.4.2 :
Ajoutons une méthode print à la classe Nom de la section précédente :
1
2
3
4
5
6
7
8
public class Nom {
public void affiche() {
System.out.println( "Bozo") ;
}
public String print(int i) {
return String.valueOf(i) ;
}
}
Complétons la classe Test pour pouvoir appeler cette nouvelle version de print par :
System.out.println(p.print(123)) ;
A l’exécution on obtient l’affichage d’une ligne supplémentaire :
123
En effet, les méthodes print accessibles pour les variables de type NomPlus sont maintenant les
suivantes :
– print()
– print(String)
– print(double, String)
– print(String, double)
– print(int) héritée de la classe Nom
L’appel p.print(123) a sélectionné la signature print(int) et la méthode correspondante a été
utilisée.
p. 24
AJ.C – 12 juin 2001
3.4. SURCHARGE DES MÉTHODES
On dira qu’il y a :
– surcharge simple lorsque les différentes méthodes surchargées sont définies dans la même
classe ;
– surcharge implicite lorsque certaines méthodes accessibles sont définies dans les sur-classes
de la classe considérée.
Le processus de sélection de la méthode exécutée dans le cas de la surcharge simple :
3.4.2
Sélection et exécution d’une méthode
Lors d’un appel de méthode la sélection puis l’exécution d’une méthode constituent un problème
délicat dans la mesure ou il est nécessaire de tenir en compte de trois facteurs :
– l’héritage,
– la surcharge,
– les conséquences du sous-typage ou de la promotion des arguments.
Sélection d’une signature
Cette étape est fondée sur la notion de type. En présence d’un appel de méthode :
(expr).nomDeMethode (liste d’arguments )
java va analyser la signature de l’appel et la comparer à la signature des méthodes dont le nom est
nomDeMethode. En fait java va sélectionner une signature de méthode et non pas une
méthode. Rappelons ici que toutes les méthodes définies dans une même classe différent par
leur signature. Si l’appel est ambiguë et que java ne parvient pas à dégager une signature unique
la compilation s’arrête avec un message d’erreur du type :
Error : Reference to \textit{nomDeMethode} is ambiguous.
It is defined in .... and .....
Les étapes principales de ce processus de sélection sont les suivantes :
– Méthode d’instance ou méthode de classe
java analyse la variable ou l’expression préfixant l’appel de méthode :
– si elle ne fait pas référence à un objet, seules les signatures des méthodes de classe
(déclarées static) accessibles seront candidates,
– sinon la recherche se poursuit parmi les signatures des méthodes d’instance accessibles
à la variable (ou à l’expression) préfixant l’appel de méthode. Ceci ne fait intervenir que
le type de la variable (ou de l’expression) préfixant l’appel de méthode (cf. §3.3.3)
L’accessibilité générale des classes et des méthodes sera étudié au prochain chapitre, mais on
peut retenir dès à présent que les classes et les méthodes publiques i.e. précédées du mot
public sont accessibles depuis toute classe.
– Compatibilité des paramètres
Parmi les signatures sélectionnée par le premier test, java ne va considérer que les signatures
compatibles avec les arguments de l’appel (cf. §3.3.4). En raison du sous-typage et de la
promotion numérique, il se peut qu’à ce stade il reste encore plusieurs signatures concurrentes.
– Choix de la signature la plus spécifique
Une signature S1 est plus spécifique pour un appel de méthode donné, qu’une signature S2
lorsque :
– les signatures S1 et S2 sont toutes deux valide pour cet appel de méthode ;
– la classe dans laquelle la méthode correspondant à S1 est définie est le même ou est une
sous-classe de celle dans laquelle est S2 définie ;
AJ.C – 12 juin 2001
p.25
CHAPITRE 3. HÉRITAGE
– Chaque type de paramètre de S1 est le même type ou un sous-type du type de paramètre
correspondant de S2 .
Exemple 3.4.3 :
On a deja défini dans la classe NomPlus une méthode de signature
print(String, double)
Supposons que l’on définisse également les méthodes de signature :
print(String, float)
print(double, float)
print(float, double)
Pour l’appel de signature print(String, int), la signature print(String, float)
est plus spécifique que print(String, double). Par contre, pour l’appel de signature print(float, float) aucune des deux signatures print(double, float) et
print(float, double) n’est plus spécifique que l’autre. Il en résultera un message
d’erreur lors de la compilation.
Sauf dans le cas d’une méthode de classe, la méthode effectivement exécutée dépend de l’objet
auquel la variable (ou l’expression) préfixant l’appel de méthode fait référence cet objet est appelé
objet appelant. Ce type est défini dynamiquement lors de l’exécution.
Exécution de la méthode correspondant à la signature sélectionnée
Supposons qu’une signature ait été sélectionnée ; l’étape qui suit et que nous allons détailler
intervient lors de l’exécution de la classe qui contient l’appel de méthode.
– Si la signature sélectionnée est celle d’une méthode de classe, la méthode correspondante est
exécutée..
– Si c’est celle d’une méthode d’instance, java détermine la classe de l’objet appelant et cherche
une méthode dont la signature correspond à la signature sélectionnée :
– dans cette classe
– si une telle méthode n’existe pas, la recherche continue dans la classe-mère de la classe
de l’objet appelant, puis éventuellement dans les sur-classes de celle-ci.
Une exception à cette règle est le cas où la méthode a été préfixée par super : la recherche commence alors par la classe-mère de la classe de l’objet appelant et se poursuit éventuellement
dans les sur-classes de celle-ci.
Exemple 3.4.4 :
Modifions les classes Nom et NomPlus de la section précédente :
1
2
3
4
5
public class Nom {
public void print(String s) {
System.out.println("String : "+s) ;
}
}
6
7
8
9
10
11
public class NomPlus extends Nom {
public void print(String s) {
System.out.println ("String : "+s.toUpperCase()) ;
}
public void print(int i, String s) {
p. 26
AJ.C – 12 juin 2001
3.4. SURCHARGE DES MÉTHODES
System.out.println( i
}
public void print(double
System.out.println( i
}
public void print(double
System.out.println( i
}
public void print(int i,
System.out.println( i
}
12
13
14
15
16
17
18
19
20
21
22
23
+ " + "+ s) ;
i, String s) {
+ " - "+ s) ;
i, StringBuffer s) {
+ " * "+ s) ;
Object o) {
+ " + "+ o) ;
}
Exécutons la séquence d’instruction :
1
2
3
4
5
6
7
NomPlus p = new NomPlus() ;
p.print("Bozo") ;
p.print(2, "Oum") ;
p.print(2.0, "Gus") ;
StringBuffer sb = new StringBuffer("Toto") ;
p.print(2.5, sb) ;
// p.print(2, sb) ; // conduit à une erreur
On obtient :
String : BOZO
2 + Oum
2.0 - Gus
2.5 * Toto
– String : BOZO est le résultat de l’appel de la méthode print(String s) de la classe
NomPlus. NomPlus est plus spécifique que la méthode de même signature de Nom (point 3) ;
– 2 + Oum est le résultat de l’appel de la méthode print(int i, String s) ; deux
méthodes sont compatibles avec l’appel print(2, "Oum") : print(int i, String s)
et print(doublet i, String s), mais print(int i, String s) est plus spécifique que
print(int i, String s) (point 2) ;
– 2.0 - Gus est le résultat de l’appel de la méthode print(double i, String s), seule
méthode compatibles avec l’appel print(2.0, "Gus") ;
– 2.5 * Toto est le résultat de l’appel de la méthode print(int i, StringBuffer s), seule
méthode compatibles avec l’appel print(2.5, sb) ;
– enfin, lorsqu’on exécute la dernière instruction on obtient une erreur :
Error : Reference to print is ambiguous. It is defined in void print(int,
java.lang.Object) and void print(double, java.lang.StringBuffer).
qui montre que java ne peut pas choisir entre les deux méthodes print(int i, Object o)
et print(double i, StringBuffer s). L’appel p.print(2, sb) est valide pour les deux
méthodes, mais aucune d’elles n’est plus spécifique que l’autre.
AJ.C – 12 juin 2001
p.27
CHAPITRE 3. HÉRITAGE
Exemple 3.4.5 :
Un autre exemple, plus surprenant, extrait de l’ouvrage JAVA Language Reference - Mark Grand
- O’REILLY ed. est le suivant :‘
1
2
3
4
class
class
class
class
A
B
C
D
{}
extends A {}
extends B {}
extends C {}
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class W {
void foo(D d)
}
class X extends
void foo(A a)
void foo(B b)
}
class Y extends
void foo(B b)
}
class Z extends
void foo(C c)
}
{System.out.println("D");}
W {
{System.out.println("A");}
{System.out.println("X.B");}
X {
{System.out.println("Y.B");}
Y {
{System.out.println("C");}
19
20
21
22
23
24
25
public class CallSelection {
public static void main(String [] argv) {
Z z = new Z() ;
((X)z).foo(new C());
}
}
La méthode foo est appelée avec une expression ((X)z) de type X. La sélection de signature se fait
donc parmi les méthodes foo accessibles pour les variables de ce type :
– foo(D) héritée de W
– foo(A) définie dans X
– foo(B) définie dans X
La signature de l’appel foo(C) exclu la signature foo(D), un objet de la classe C ne pouvant être
assigné à une variable de type D. java doit donc choisir entre les signatures foo(A) et foo(B).
Cette dernière est plus spécifique puisque B est un sous-type de A. La signature sélectionnée est
donc foo(B).
Lors de l’exécution, l’objet appelant est z de la classe Z. La recherche de la méthode foo de signature
foo(B) commence dans Z. Une telle méthode n’existe pas dans Z. La recherche se poursuit dans la
classe Y, classe-mère de Z. Il existe dans Y une méthode de signature foo(B) ; c’est celle-ci qui est
exécutée.
3.5
3.5.1
Variables d’instance et héritage
Accès aux variables d’instance
Reprenons la classe Personne définie au début de cee chapitre. Elle est complétée par une classe
Eleve qui ajoute une variable d’instance cursus.
p. 28
AJ.C – 12 juin 2001
3.5. VARIABLES D’INSTANCE ET HÉRITAGE
Exemple 3.5.1 :
1
2
3
public class Personne {
private int age ;
private String nom ;
4
public Personne() {
nom = "???" ;
}
public Personne(int n, String s) {
age = n ;
nom = s ;
}
public String toString() {
return nom + " - " + age + " ans" ;
}
5
6
7
8
9
10
11
12
13
14
15
}
16
17
18
public class Eleve extends Personne {
private String cursus ;
19
public void setCursus(String c) {
cursus = c ;
}
public String toString() {
return nom + " - " + age +" ans" + " - " + cursus ;
}
20
21
22
23
24
25
26
}
A la compilation on obtient un message d’erreur pour return nom + " - " + age + " ans" :
Error : variable nom in class Personne not accessible for class Eleve
variable age in class Personne not accessible for class Eleve
Le statut privé des variables nom et age n’est donc pas modifié par l’héritage.
L’utilisation de super va nous permettre d’écrire proprement la méthode toString de la classe
Eleve. Remplaçons les lignes 23 à 25 par :
23
24
25
public String toString() {
return super.toString() + " - " + cursus ;
}
Ainsi que nous l’avons déjà vu, super.toString() applique à l’objet appelant le méthode
toString() de la classe-mère Personne. Exécutons la classe Test :
1
2
3
4
5
6
public class Test {
public static void main(String[] args) {
Eleve e = new Eleve() ;
System.out.println(e) ;
e.setCursus("ne sait pas") ;
System.out.println(e) ;
AJ.C – 12 juin 2001
p.29
CHAPITRE 3. HÉRITAGE
// Eleve f = new Eleve(5,"Mimi") ;
// System.out.println(f) ;
7
8
}
9
10
}
On obtient l’affichage
??? - 0 ans - null
??? - 0 ans - ne sait pas
Commentons ce résultat
– A la ligne 3 on crée un objet Eleve. En l’absence de constructeur explicite, java utilise
un constructeur générique : java cherche d’abord s’il existe un constructeur non paramétré
dans la classe-mère. C’est donc Personne() qui est utilisé ce qui est cohérent avec le premier
affichage.
– Le second affichage est la conséquence de e.setCursus("ne sait pas") qui permet de
modifier la variable cursus de l’objet e.
– Les lignes 7 et 8 si elles étaient exécutées conduirait à une erreur wrong number of
parameters in . . . . Ceci montre bien que le mécanisme du constructeur générique est limité
aux constructeurs non paramétrés.
Règles :
Accès aux variables d’instance
Considérons une classe Bb sous-classe d’une classe Aa.
– les variables d’instance (private) définies dans Aa ne sont pas accessibles depuis Bb ;
– il faut donc prévoir lors de la construction de la classe-mère des méthodes d’accès du type
getX . . . ( . . . ) ou setX . . . ( . . . )
!!! Remarque :
Variables d’instance redéfinies - On peut se poser la question suivante : que se passe-t-il si on
définit dans une classe Cc une variable d’instance déjà définie dans une sur-classe Aa de la classe
Cc considérée ?
java accepte de genre de redéfinition. Dans la classe Cc on accède normalement à la variable.
Si dans Cc on veut accéder à la variable primitive, il faut que lors de sa définition (dans la surclasse Aa de Cc) cette variable ne soit pas déclarée private et si c désigne un objet de Cc, on
utilisera la notation ((Aa)c).nom de la variable . On reviendra plus complètement sur le rôle
des modificateurs d’accès (private, public, ... dans le prochain chapitre.
Exemple 3.5.2 :
On ajoute des méthodes d’accès à la classe Nom :
1
2
3
public class Personne {
private int age ;
private String nom ;
4
p. 30
AJ.C – 12 juin 2001
3.5. VARIABLES D’INSTANCE ET HÉRITAGE
public Personne() {
nom = "???" ;
}
public Personne(int n, String s) {
age = n ;
nom = s ;
}
public int getAge() {
return age ;
}
public void setAge(int n) {
age = n ;
}
public String getNom() {
return nom ;
}
public void setNom(String n) {
nom = n ;
}
public String toString() {
return nom + " - " + age + " ans" ;
}
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
}
On est alors en mesure d’écrire des constructeurs pour la classe Eleve :
1
2
3
4
5
6
7
8
public Eleve() {
}
public Eleve(int n, String s, String c) {
this() ;
setAge(n) ;
setNom(s) ;
setCursus(c) ;
}
Le constructeur Eleve() est indispensable pour pouvoir utiliser this(), qui on l’a vu est ”symboliquement” équivalent à this = new Eleve(). On fixe ensuite la valeur des variables age, nom
et cursus. Exécutons à nouveau la classe Test :
1
2
3
4
5
6
7
8
public class Test {
public static void main(String[] args) {
Eleve e = new Eleve() ;
System.out.println(e) ;
Eleve f = new Eleve(7,"Bozo","Clown") ;
System.out.println(f) ;
}
}
AJ.C – 12 juin 2001
p.31
CHAPITRE 3. HÉRITAGE
On obtient :
??? - 0 ans - null
Bozo - 7 ans - Clown
Mais ce genre de construction ne doit pas nous empêcher de nous poser la “bonne question” :
comment écrire des constructeurs pour les classes-fille ?
3.5.2
Des constructeurs pour les classes-fille - super( )
Réécrivons le constructeur Eleve(int n, String s, String c) en utilisant super( . . .
:
Exemple 3.5.3 :
1
2
3
4
public Eleve(int n, String s, String c) {
super(n,s) ;
setCursus(c) ;
}
super(n, s) fait référence à un constructeur d’une sur-classe de Eleve dont la liste des paramètresformels serait (int,String). Un tel constructeur existe, c’est Personne(int n, String s).
super(n, s) est en quelque sorte équivalent à :
this = transformeEnEleve(new Personne(int n, String s))
Bien entendu, this ne peut être utilisé ainsi et la méthode transformeEnEleve n’existe pas. Tout
comme this( . . . ), super( . . . ) doit apparaı̂tre à la première ligne du corps du constructeur.
Règles :
– Lorsqu’il apparaı̂t à la premiere ligne d’un constructeur d’une classe Cc, super(liste
de paramètres ) fait référence (si ce constructeur existe) au constructeur d’une sur-classe de
Cc possédant la même liste de paramètres.
p. 32
AJ.C – 12 juin 2001
4
Packages et règles d’accès
Sommaire
4.1
4.2
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Utilisation et création de packages . . . . . . . . . . . . . . . . . . . . . .
4.2.1 Un peu de vocabulaire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2.2 Accès aux classes d’un package . . . . . . . . . . . . . . . . . . . . . . . . .
4.2.3 La variable d’environnement CLASSPATH . . . . . . . . . . . . . . . . . . . .
4.2.4 Créer un package - Ajouter une classe à un package . . . . . . . . . . . . .
4.3 Accès aux éléments d’une classe . . . . . . . . . . . . . . . . . . . . . . .
4.3.1 Modificateurs usuels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.4 Contrôle de la redéfinition et de l’héritage : final et abstract . . . . .
4.4.1 Utilisation de final . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.4.2 Classes et méthodes abstraites . . . . . . . . . . . . . . . . . . . . . . . . .
4.1
33
35
35
36
38
39
39
40
40
40
41
Introduction
Les packages fournissent un moyen entièrement pris en compte au niveau du langage java pour :
– regrouper des classes ou des interfaces participant aux mêmes centres d’intérêt ;
– mieux gérer la visibilité des classes : deux classes portant le même nom sont distinguées dès
qu’elles appartiennent à des packages différents ;
– définir un certain niveau de protection pour les classes et leurs éléments.
Les packages ne sont pas totalement indépendants de la structure des répertoires induite par le
système d’exploitation présent sur la machine utilisée. Le mécanisme qui lie packages et répertoires
sera développé dans la rubrique consacrée à la variable d’environnement CLASSPATH.
La notion de package est également différente ce celle de projet que l’on rencontre dans certains
environnements de développement (IDE) comme KAWA : une classe peut appartenir successivement
à plusieurs projets sans qu’il soit nécessaire de la modifier, alors qu’elle ne peut appartenir qu’à un
seul package.
La bibliothèque standard de java est elle-même structurée en packages (et sous-packages) pour
en faciliter la compréhension. La liste suivante indique les packages et les sous-packages les plus
fréquemment utilisés :
Le package java contient :
– java.lang qui fournit les classes et interfaces fondamentales de la programmation en java
qui contient les sous-packages :
CHAPITRE 4. PACKAGES ET RÈGLES D’ACCÈS
– java.lang.ref
– java.lang.reflect
– java.applet qui fournit les classes et interfaces nécessaires à la création d’applets (applications lancées depuis une page du web) ;
– java.awt qui fournit les classes et interfaces nécessaires à la création d’interfaces graphiques
et à la manipulation d’images, contient les sous-packages
– java.awt.color
– java.awt.dnd
– java.awt.event
– java.awt.font
– java.awt.geom
– java.awt.im
– java.awt.image qui contient :
– java.awt.image.renderable
– java.awt.print
– java.beans qui fournit les classes et interfaces permettant la création de java-beans (sorte
de ”briques logicielles” dont l’agrégation permet de créer des applications) qui contient :
– java.beans.beancontext
– java.io qui fournit les classes et interfaces relatives à la gestion des entrée/sorties ;
– java.math qui fournit des classes complémentaires pour le calcul numérique (la classe math
fait partie du package java.lang) ;
– java.net qui fournit les classes et interfaces relatives aux réseaux ;
– java.util qui fournit des classes utilitaires (date, heure, générateur de nombre aléatoires,
collections) ; elle contient :
– java.util.jar
– java.util.zip
Le package javax contient les classes du package swing qui permet la réalisation d’interfaces utilisateur plus sophistiquées que awt. Il a la structure suivante :
– javax.swing contient les sous-packages :
– javax.swing.border
– javax.swing.colorchooser
– javax.swing.event
– javax.swing.filechooser
– javax.swing.plaf
– javax.swing.plaf.basic
– javax.swing.plaf.metal
– javax.swing.plaf.multi
– javax.swing.table
– javax.swing.text
– javax.swing.text.html
p. 34
AJ.C – 12 juin 2001
4.2. UTILISATION ET CRÉATION DE PACKAGES
– javax.swing.text.html.parser
– javax.swing.text.rtf
– javax.swing.tree
– javax.swing.undo
À la lecture de cet exemple, on devine facilement la syntaxe utilisée pour désigner les packages et
sous-packages : java.awt.color désigne le sous-package color du package awt qui est lui-même
un sous-package de java ; java.awt.color est le nom complet de ce package.
En pratique, les classes prédéfinies des packages java et javax apparaissent souvent sous forme
d’un unique fichier compressé classes.zip qui est décompressé “au vol” lorsque cela est nécessaire.
Règles :
– Les noms des packages sont des identificateurs construits selon les règles usuelles.
– Un package contient des classes, des interfaces et éventuellement des sous-packages. L’ensemble formé par un package et ses sous-packages possède une structure d’arbre :
nom1.nom2. . . . .nomp
désigne le package nomp , sous-package de . . . , sous-package de nom2 , lui même sous-package
de nom1 ; nom1.nom2. . . . .nomp est le nom complet du package.
– À chaque package (ou sous-package) doit correspondre un répertoire formé des fichiers des
classes et interfaces du package : une structure arborescente de répertoires correspondant
alors à la structure arborescente des packages.
Il faut bien comprendre que les packages proposent une organisation logique des classes et des
interfaces alors que les répertoires qui regroupent les fichiers des classes et des interfaces, fournissent
une organisation physique de celles-ci.
4.2
Utilisation et création de packages
Les packages regroupent des classes. Une question vient immédiatement à l’esprit : comment
utiliser ces classes qui sont enfermées dans une structure particulière ?
4.2.1
Un peu de vocabulaire
On va définir le vocabulaire utilisé pour caractériser les classes dont le contenu est
(éventuellement) utilisable par d’autres classes.
Classe visible depuis une autre classe
Une classe Cc est visible (ou accessible) depuis une autre classe, lorsque certaines de ses variables
ou de ses méthodes, précisément celles précédées du modificateur public, peuvent être utilisées
depuis la classe Cc.
Exemple 4.2.1 :
Reprenons un exemple du chapitre précédent :
1
2
3
4
5
6
7
public class Personne {
private int age ;
private String nom ;
public String toString() {
return nom + " - " + age + " ans" ;
}
}
AJ.C – 12 juin 2001
p.35
CHAPITRE 4. PACKAGES ET RÈGLES D’ACCÈS
1
2
3
4
5
6
public class Test {
public static void main(String[] args) {
Personne a = new Personne() ;
System.out.println(a) ;
}
}
Nous avons vu que la classe Text s’exécute sans erreurs particulières (même si le résultat n’est
pas extraordinaire . . . ). Ceci montre que la classe Nom est visible depuis la classe Test puisque la
méthode toString de Nom a pu être utilisée dans System.out.println(a).
Classes publiques
Une classe publique est une classe dont l’en-tête contient le modificateur public Le modificateur private n’est pas utilisé dans le contexte des classes usuelles. Il est réservé à des classes qui
sont définies à l’intérieur d’une classe. Dans l’exemple précédent, les deux classes Nom et Test sont
des classes publiques.
Lorsque plusieurs classes sont enregistrées dans le même fichier, il ne peut y avoir qu’une seule
classe publique. C’est alors nécessairement la classe qui contient la méthode main. Cette
classe est appelée classe principale et le fichier porte nécessairement son nom.
Exemple 4.2.2 :
Si on souhaite enregistrer les deux classes précédentes dans un même fichier, la classe principale
sera Test et le fichier Test.java s’écrira alors (on notera la suppression du modificateur public) :
1
2
3
4
5
6
7
1
2
3
4
5
6
class Personne {
private int age ;
private String nom ;
public String toString() {
return nom + " - " + age + " ans" ;
}
}
public class Test {
public static void main(String[] args) {
Personne a = new Personne() ;
System.out.println(a) ;
}
}
4.2.2
Accès aux classes d’un package
Package par défaut
Lorsque l’on n’a pas défini de package, java définit implicitement un package associé au
répertoire de travail ; c’est le package par défautt. On peut donc admettre que toutes les classes
que l’on est susceptible d’utiliser appartiennent à un package.
p. 36
AJ.C – 12 juin 2001
4.2. UTILISATION ET CRÉATION DE PACKAGES
Règles :
Règles d’accès aux classes :
– Depuis le package lui-même
Toutes les classes ou interfaces d’un package sont visibles depuis n’importe quelle classe du
package (en particulier, les classes qui ne pas qualifiées par public). Cet accès est appelé
l’“accès package”.
– Depuis un autre package
Pour qu’une classe Cc appartenant à un package autre que celui d’une classe donnée soit
visible par celle-ci, il faut que :
– la classe Cc soit publique ;
– la classe Cc ait été importée.
– Les classes publiques du package java.lang. sont visibles directement depuis toute classe.
Importation d’une classe - import
La directive import placée juste avant l’en-tête d’une classe :
import nom complet du package.nom de la classe ;
indique à java que cette classe est susceptible d’utiliser la classe désignée.
Exemple 4.2.3 :
Si la classe Personne appartient au package projects, pour pouvoir l’utiliser en dehors de ce
package, dans la classe test par exemple, on écrira :
1
2
3
4
5
import projects.Personne ;
public class Test {
public static void main(String[] args) {
Personne a = new Personne() ;
. . .
Il faut noter que import n’ajoute pas la classe importée à la classe en cours d’utilisation, mais
permet simplement à celle-ci d’utiliser des ressources contenues dans la classe importée ; import
n’a donc aucune incidence sur la taille du code généré. Si on souhaite importer toutes les classes
d’un package on écrira :
import nom complet du package.* ;
Exemple 4.2.4 :
1
2
3
4
5
import projects.* ;
public class Test {
public static void main(String[] args) {
Personne a = new Personne() ;
. . .
La directive import ... .* ne permet que l’importation des classes et interfaces du package considéré, mais pas les éléments des sous-packages éventuels inclus dans le package ; on doit
écrire import ... .* autant de fois qu’il y a de package ou de sous-package à importer. Par
exemple, si on veut importer simultanément les classes et interfaces des packages java.awt et
java.awt.color on écrira :
AJ.C – 12 juin 2001
p.37
CHAPITRE 4. PACKAGES ET RÈGLES D’ACCÈS
Exemple 4.2.5 :
1
2
3
4
5
import java.awt.* ;
import java.awt.color.* ;
public class MyFrame extends Frame {
Canvas toile ;
. . .
!!! Remarque :
Le package java.lang est automatiquement importé, sans qu’il soit nécessaire d’écrire la directive
import java.lang correspondante.
4.2.3
La variable d’environnement CLASSPATH
La variable d’environnement CLASSPATH est une variable pré-déclarée, liée à l’implantation de
la machine virtuelle java, qui renferme la liste des répertoires contenant les packages susceptibles
d’être importés. La valeur de cette variable peut être affiché par :
System.out.println( System.getProperty("java.class.path")) ;
L’affichage résultant est souvent de la forme suivante (la syntaxe dépend étroitement du système
d’exploitation utilisé) :
/java/classes.zip : /java/projects : /java/exemples/exPack
– /java/classes.zip désigne le répertoire où sont stockés les packages standards de java ;
– Le package par défaut qui correspond au répertoire de travail (ici /java/projects/exPack)
est souvent représenté par un point.
Lorsque un import ... apparaı̂t, java transforme le nom du package en un chemin relatif, puis
ajoute ce chemin aux différents noms apparaissant dans la variable CLASSPATH, puis éventuellement
cherche le package parmi les packages standards de java. Si cette recherche échoue, java envoie
un message d’erreur.
Exemple 4.2.6 :
Supposons que CLASSPATH ait la valeur :
/java/classes.zip : /java/projects : /java/exemples/exPack
et que le package importé soit myPackage.graph. la chaı̂ne de caractères myPackage.graph est
transformée en /myPackage/graph. Ensuite le package myPackage.graph est recherché
– dans le répertoire /java/exemples/exPack/myPackage/graph ;
– puis dans le répertoire /java/projects/myPackage/graph ;
– puis éventuellement dans les packages standards /java/classes.zip ;
– si alors myPackage.graph n’a pas été trouvé, on obtient une erreur.
Dans la plupart des IDE le fait d’ajouter une classe à un projet ajoute implicitement le chemin
d’accès au fichier de cette classe, à la variable CLASSPATH de telle sorte que la compilation se déroule
sans problème.
p. 38
AJ.C – 12 juin 2001
4.3. ACCÈS AUX ÉLÉMENTS D’UNE CLASSE
4.2.4
Créer un package - Ajouter une classe à un package
Pour créer un package, il suffit de créer un répertoire portant le nom du package. Pour un souspackage, le répertoire doit être créé dans le répertoire associé au package-père du sous-package. La
valeur de la variable CLASSPATH sera en suite (éventuellement) mise à jour pour que java puisse
accéder à ce nouveau package..
Exemple 4.2.7 :
Supposons que CLASSPATH ait toujours la valeur :
/java/classes.zip : /java/projects : /java/exemples/exPack
et l’on veuille créer un package projects.graph. Pour cela il suffit de créer le répertoire
/java/projects/graph. Ici, il n’est pas nécessaire de mettre à jour la variable CLASSPATH
puisqu’elle contient déjà le répertoire /java/projects.
Pour ajouter une classe ou une interface à un package (dont on suppose l’existence), il suffit
que la première ligne de cette classe ou interface, avant les directives import éventuelles, soit :
package nom complet du package
Exemple 4.2.8 :
Pour ajouter la classe MyFrame au package projects.graph, on ajoute package projects.graph ;
dans MyFrame :
1
2
3
4
5
6
package projects.graph ;
import java.awt.* ;
import java.awt.color.* ;
public class MyFrame extends Frame {
Canvas toile ;
. . .
4.3
Accès aux éléments d’une classe
Les éléments (variables et méthodes) d’une classe sont toujours accessible depuis cette classe.
Pour accéder à ces mêmes éléments depuis une autre classe, il est tout d’abord nécessaire que
la classe soit visible depuis cette autre classe. De plus, des modificateurs d’accès permettent
d’accorder ou de supprimer l’accès à un élément.
AJ.C – 12 juin 2001
p.39
CHAPITRE 4. PACKAGES ET RÈGLES D’ACCÈS
4.3.1
Modificateurs usuels
private
dénomination
accès “privé”
accès “package”
protected
public
Modificateurs usuels
classes
non utilisé pour les classes
usuelles
accès limité au package
accès “protégé”
non utilisé
accès “public”
accès possibles depuis toute
classe qui l’importe
abstract
classe non instanciable
final
classe non dérivable
static
non utilisé
méthodes ou variables
accès limité à la classe de
définition
accès limité au package
accès limité au package
et à toute sous-classe
de la classe de définition
accès possible depuis toute
classe pour laquelle la classe
de définition est visible
méthode sans corps
½
methode qui ne peut être redéfinie
variable à valeur constante
méthode ou variable de classe
On peut classer les modificateurs en trois familles :
– private, protected, public qui qualifient l’accès aux classes et à leurs éléments, par défaut,
en l’absence de l’un de ces modificateurs, l’accès est l’accès “package” ;
– abstract et final qui permettent de contrôler la redéfinition des méthodes et l’héritage ;
– static qui qualifie les méthodes et les variables attachées à une classe et non à un objet, par
défaut, en l’absence de modificateur, les variables et méthodes sont des variables et méthodes
d’instance.
Au sein d’une même famille ils sont exclusifs : une méthode ne peut-être simultanément public et
private ou simultanément abstract et final. Par contre, il est tout à fait possible qu’une méthode
ou une variable soit qualifiée par plusieurs modificateurs appartenant à des familles distinctes :
public static void main(String[] args)
4.4
4.4.1
Contrôle de la redéfinition et de l’héritage : final et abstract
Utilisation de final
Le modificateur final s’applique à des classes, des méthodes ou des variables :
– Une classe qualifiée par final est une classe qui ne peut avoir de classe-fille. C’est par exemple,
le cas des classes String, StringBuffer ou encore des classes Byte, Double, Float, Integer,
Long et Short.
– La valeur d’une variable qualifiée par final ne peut être modifiée ; son comportement est alors
celui d’une constante ; java utilise cette possibilité pour définir certaines constantes : par
exemple, la constante Math.PI est définie comme public static final double signifiant
ainsi que c’est une variable de classe (static), accessible depuis toute classe puisque la classe
Math est une classe publique du package java.lang et que cette variable est qualifiée par
public, et dont la valeur ne peut être modifiée. Il est d’usage que l’identificateur de telles
variables soit écrit en majuscules.
– Les méthodes qualifiées par final sont des méthodes qui ne peuvent être redéfinies dans une
classe dérivée.
p. 40
AJ.C – 12 juin 2001
4.4. CONTRÔLE DE LA REDÉFINITION ET DE L’HÉRITAGE : FINAL ET ABSTRACT
4.4.2
Classes et méthodes abstraites
Une méthode abstraite est une méthode dont le corps est remplacé par un simple pointvirgule. Une telle méthode n’est donc pas définie ; on laisse le soin à une classe dérivée d’en compléter
la définition. Son en-tête est alors nécessairement qualifiée par abstract.
Une classe qui contient une classe abstraite est elle-même une classe abstraite et son en-tête
contient nécessairement le modificateur abstract.
Exemple 4.4.1 :
On complète la classe Personne par une méthode imprime qui sera programmée dans une sousclasse de Personne en fonction du contexte dans lequel cette nouvelle classe sera effectivement
utilisée. La classe Personne devient alors une classe abstraite.
1
2
3
public abstract class Personne {
private int age ;
private String nom ;
4
public abstract void imprime() ;
5
6
public String toString() {
return nom + " - " + age + " ans" ;
}
7
8
9
10
}
Il est à noter qu’une classe abstraite ne peut être instanciée (c’est à dire utilisée pour créer
des objets) puisque certaines de ses méthodes sont incomplètes. Les classes abstraites apparaissent
donc comme un intermédiaire entre les interfaces et les classes instanciables. C’est en particulier le
cas de la classe Number qui est une classe abstraite, classe-mère des classes Byte, Double, Float,
Integer, Long et Short.
AJ.C – 12 juin 2001
p.41
CHAPITRE 4. PACKAGES ET RÈGLES D’ACCÈS
p. 42
AJ.C – 12 juin 2001
5
Exceptions
Sommaire
5.1
Des exceptions, pourquoi faire ? . . . . . . .
5.1.1 Objectif des exceptions . . . . . . . . . . . .
5.1.2 Cadre général de l’utilisation des exceptions .
5.2 Les classes d’exception de java . . . . . . . .
5.3 Exceptions et héritage . . . . . . . . . . . . .
5.3.1 Erreurs multiples . . . . . . . . . . . . . . . .
5.3.2 Méthodes redéfinies . . . . . . . . . . . . . .
5.1
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
. . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . .
. . . . . . .
. . . . . . . .
. . . . . . . .
43
43
45
46
47
47
48
Des exceptions, pourquoi faire ?
Les exception fournissent un moyen complètement intégré au langage java pour gérer des
situations imprévues pouvant conduire à une interruption inopinée d’un programme. Si la plupart
des erreurs de syntaxe sont détectées lors de la compilation, d’autres erreurs sont liées aux objets
intervenant dans le programme : division par zéro, dépassement des valeurs permises pour les bornes
d’un tableau, classe introuvable, . . . et n’apparaı̂ssent que lors de l’exécution du programme. Ces
erreurs sont rassemblées sous le nom d’erreurs d’exécution. Ce sont ces erreurs là qui sont
modélisées par les exceptions.
5.1.1
Objectif des exceptions
L’objectif et l’intérêt des exceptions est de donner :
– un cadre systématique pour traiter les erreurs en les modélisant par des objets et en fournissant
un ensemble de classes couvrant un certain nombre de situations “classiques” : division par
zéro, dépassement des valeurs permises pour les bornes d’un tableau . . . ;
– un mécanisme qui permet de séparer le diagnostic d’une erreur (d’une exception) de son
traitement en le déléguant à une méthode spécifique.
Exemple 5.1.1 :
Voici un petit programme qui saisit des nombres et qui calcule leur moyenne (tronquée) :
1
2
3
4
public class Excp {
public static void main(String[] args) {
StandardInput.startConsole() ;
System.out.println( "Combien de nombres ?" ) ;
CHAPITRE 5. EXCEPTIONS
int n = StandardInput.readInt() ;
int s = 0 ;
for(int i = 0 ; i<n ; i++) {
System.out.println( "Entrez un entier" ) ;
int m = StandardInput.readInt() ;
s+=m ;
}
System.out.println("moyenne"+s/n ) ;
5
6
7
8
9
10
11
12
}
13
14
}
Malheureusement lorsqu’on entre la valeur zéro à la question Combien de nombres ?, on obtient :
Exception Occurred:
java.lang.ArithmeticException: / by zero
at Excp.main(Compiled Code) ...
Une erreur est survenue, le programme ne fournit pas de réponse et java nous en prévient par le
message ci-dessus. Une façon d’éviter cette situation serait de modifier la saisie en remplaçant les
lignes 4 et 5 par :
int n ;
do {
System.out.println( "Combien de nombres ?" ) ;
n = StandardInput.readInt() ;
if(n <=0)
System.out.println("Le nombre doit ^
etre positif") ;
}
while(n<=0) ;
1
2
3
4
5
6
7
8
Cette démarche appelle plusieurs remarques :
– on a analysé les causes d’erreur possibles et on a prévu un traitement en conséquence : faire
saisir à nouveau la valeur de n ;
– mais ce mode de traitement est pas toujours possible : tous les programmes ne sont pas
toujours aussi simples et il n’est pas toujours possible modifier les données . . .
– enfin, la méthode main ci-dessus n’est pas un exemple de bonne programmation puisque tout
est mélangé saisie, calcul, gestion des erreurs.
Le concept d’exception proposé par java va permettre de distinguer la cause initiale de l’erreur
(dans l’exemple ci-dessus, la saisie), l’instruction qui provoque l’erreur (la division par zéro) et le
traitement de la cause initiale :
1
2
3
4
5
6
7
8
class NbNegException extends Exception {
private int nbErr ;
public NbNegException(int n) {
nbErr = n ;
}
public String toString() {
return( "le nb entré, " + nbErr + ",
}
p. 44
ne convient pas") ;
AJ.C – 12 juin 2001
5.1. DES EXCEPTIONS, POURQUOI FAIRE ?
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
}
public class Excp1 {
public static int saisie () throws NbNegException {
System.out.println( "Combien de valeurs ?" ) ;
int n = StandardInput.readInt() ;
if(n <= 0)
throw new NbNegException(n) ;
else return n ;
}
public static void main(String[] args) {
StandardInput.startConsole() ;
try {
int n = saisie() ;
int s = 0 ;
for(int i = 0 ; i<n ; i++) {
System.out.println( "Entrez un entier" ) ;
int m = StandardInput.readInt() ;
s+=m ;
}
System.out.println("moyenne : "+s/n ) ;
} catch (NbNegException e) {
System.out.println(e) ;
}
}
}
Cette dernière version est est apparemment plus complexe, mais :
– Elle sépare les différentes fonctions : saisie puis traitement.
– Le test de non-positivité do . . . while a été remplacé par throw . . . . Le rôle de throws
avec un ’s’ dans l’en-tête de certaines méthodes sera explicité plus loin.
– La protection contre la division par zéro et le traitement de l’erreur éventuelle est assurée par
le couple try . . . catch . . .
5.1.2
Cadre général de l’utilisation des exceptions
La lecture de l’exemple précédent permet de distinguer les étapes suivantes dans la mise en
oeuvre des exceptions :
– Analyse et modélisation des erreurs possibles
Ceci se traduira par l’utilisation d’une classe d’exception prédéfinie dans java ou comme
dans l’exemple, par la définition d’une classe dérivée de la classe Exception. dans l’exemple
ci-dessus, la classe NbNegException. En général, il suffit de définir un constructeur et la
méthode toString.
– Diagnostic de l’erreur et génération d’une exception// Pour générer une exception,
dans le jargon informatique, on dit souvent lever une exception (en anglais throw ), il suffit
de sélectionner le cas conduisant à une erreur et de créer une nouvelle exception au moyen de
l’instruction throw et du constructeur de la classe d’exception choisie :
if(n <= 0)
throw new NbNegException(n) ;
AJ.C – 12 juin 2001
p.45
CHAPITRE 5. EXCEPTIONS
Le throw provoque la sortie de la méthode dans lequel il apparaı̂t et le retour au point d’appel
de celle-ci (au même titre qu’un return).
– Traitement de l’erreur
Il est réalisé par l’instruction try . . . catch qui permet d’isoler au sein d’une méthode les
parties de code susceptibles de générer des exception :
– soit parce que dans la définition de ces parties on a prévu de générer une exception ;
– soit parce que cette possibilité a été prévue par java et ce fait est signalé dans l’API de
java (c’est en particulier le cas de la plupart des méthodes ayant un lien avec la gestion
des entrées /sorties) :
et d’“attraper” l’exception lorsqu’elle se présente afin de sortir “sans degas” de la partie
litigieuse et d’effectuer dans ce cas le traitement approprié.
try {
int n = saisie() ;
<-- saisie() peut lever une exception NbNegException
. . . . . .
ce qui provoque un saut à l’instruction
. . . . . .
catch (NbNegException e)
}
catch (NbNegException e) { <-- Si une exception NbNegException est générée
System.out.println(e) ;
l’exécution se poursuit ici
}
avec un traitement approprié
A défaut d’un traitement effectif de l’erreur, il est possible de propager, de “laisser passer”
(throws en anglais) l’exception, le traitement étant délégué aux instructions utilisant cette
partie de code. Ce passage de relais se fait en ajoutant le mot throws suivi du nom de la classe
d’exception, dans la signature de la méthode, après la liste des paramètres. En particulier une
méthode qui lève une exception doit pouvoir transmettre l’exception aux parties de code qui
utilisent cette méthode et doit comporter nécessairement un throws dans sa signature :
public static int saisie () throws NbNegException {
5.2
Les classes d’exception de java
Les classes d’exception dérivent de la classe Throwable qui possède deux sous-classes :
– La classe Error qui modélise des erreurs internes à la machine virtuelle.
– La classe Exception comporte deux constructeurs Exception() et Exception(String s) et
une méthode toString() qui donne un bref aperçu de la nature de l’exception. Elle est la
classe-mère de nombreuses classes d’exceptions parmi les quelles :
– La
classe
RunTimeException
modélise
les
erreurs
rencontrées
lors
de l’exécution des classes. Elle possède un grand nombre de sousclasses
:
ArtthmeticException, ClassCastException, IllegalArgument
Exception, ArrayIndexOutOfBoundsException, NegativeArraySizeException,
NoSuchException, NullPointerException, . . . . Les RuntimeException ont une
caractéristique particulière : il n’est pas nécessaire ni de les traiter ni de les propager,
c’est à dire qu’il n’est pas nécessaire d’encadrer une partie de code qui génère une
RuntimeException dans une construction tri . . . catch ni de compléter la signature
par un throws. En général on ne lève pas de RunTimeException.
– La classe IOException comporte également un grand nombre de sous-classes. Elle
modélise les exceptions correspondant à les événements anormaux liés à l’environnement
extérieur du programme : fichiers, flux, sockets, . . . . Ces exceptions peuvent être levées
et doivent être propagées ou être traitées.
p. 46
AJ.C – 12 juin 2001
5.3. EXCEPTIONS ET HÉRITAGE
O bject
Throw able
Error
Exception
Runtim eException
ClassN otFoundException
IO Exception
A rithm eticException
ClassCastException
Fig. 5.1: Arborescence des exceptions en java
5.3
Exceptions et héritage
L’héritage intervient de façon permanente puisque toutes les classes d’exceptions, qu’elles soient
prédéfinies dans java ou qu’elles soient définies par l’utilisateur dans le cadre d’un programme,
héritent de la classe Exception. Les deux points développés ci dessous méritent une certaine attention.
5.3.1
Erreurs multiples
Examinons la classe suivante :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Toto {
public static void test () {
int [] t = {0, 1, 2, 3] ;
try (
System.out.println(t[4]) ;
} catch (RuntimeException i) {
System.out.println("RuntimeException : " + i) ;
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("OutOfBoundsException : " + e) ;
}
}
public static void main (Sting[] args) {
test() ;
}
}
AJ.C – 12 juin 2001
p.47
CHAPITRE 5. EXCEPTIONS
La compilation de cette classe provoque une erreur et on obtient l’affichage
Toto.java : 9: catch not reached.
} catch (ArrayIndexOutOfBoundsException e) {
1 error
En effet ArrayIndexOutOfBoundsException est une sous-classe de RuntimeException. Si une telle
exception est générée, elle sera capturée par le premier catch qui est plus général.
Règle :
Il est possible de traiter des erreurs multiples au moyen d’autant de catch que de type d’exceptions.
Mais les différents catch doivent être écrits dans l’ordre inverse de celui de l’héritage.
5.3.2
Méthodes redéfinies
Lorsqu’une méthode propageant une exception (throws ...) est redéfinie des règles particulières s’appliquent :
Règle :
Lorsqu’une méthode propageant une exception est redéfinie dans une sous-classe, l’ensemble des exceptions propagées par la méthode redéfinie doit être un sous-ensemble de l’ensemble des exceptions
propagées par la méthode initiale. La méthode redéfinie ne doit pas propager plus d’exceptions que
la méthode homologue de la sur-classe.
p. 48
AJ.C – 12 juin 2001
6
Flux et fichiers
Sommaire
6.1
Introduction . . . . . . . . . . . . . . . . . . .
6.1.1 Flux de données . . . . . . . . . . . . . . . .
6.1.2 Fichiers . . . . . . . . . . . . . . . . . . . . .
6.2 Quelques classes de java.io . . . . . . . . . .
6.3 Exemples d’utilisation des classes de java.io
6.3.1 Fichiers de caractères . . . . . . . . . . . . .
6.3.2 Fichiers d’octets . . . . . . . . . . . . . . . .
6.3.3 Utilisation de IOException . . . . . . . . . .
6.3.4 Les flux in et out . . . . . . . . . . . . . . .
6.3.5 Fichiers d’objets . . . . . . . . . . . . . . . .
6.1
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
. . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . .
. . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
49
49
50
52
53
53
54
55
56
57
Introduction
Pour les programmeurs I/O est l’acronyme des mots entrée et sortie, input and output en anglais.
Les entrées désignent les flux (stream en anglais) de données provenant de l’environnement et dirigés
vers le programme, allant du clavier vers le programme par exemple. Les sorties désignent les flux
de données venant du programme et dirigés vers l’environnement, vers l’écran pour un affichage
par exemple.
La majorité des classes qui en java permettent la gestion des entrées et des sorties appartiennent
au package java.io ; il faudra donc importer ce package en plaçant import java.io.* en tête des
classes qui l’utilisent.
6.1.1
Flux de données
Lorsque un flux de données a pour source le programme, il est appelé flux de sortie, output
stream en anglais. Lorsque le programme est au contraire le destinataire du flux, le flux est appelle
flux d’entrée ou output stream.
Pour modéliser ces flux, java propose quatre classes abstraites :
– pour les flux d’octets :
– java.io.InputStream pour les flux d’entrée ;
– java.io.OutputStream pour les flux de sortie ;
item pour les flux de caractères :
– java.io.Reader pour les flux d’entrée ;
CHAPITRE 6. FLUX ET FICHIERS
– java.io.Writer pour les flux de sortie ;
Les classes que nous utiliserons héritent de ces quatre classes. Deux critères de classement apparaissent déjà :
– octet versus caractère ;
– entrée ou lecture versus sortie ou écriture ;
L’avantage principal de cette approche est de donner un cadre général pour traiter les échange
d’information entre un programme et son environnement : clavier, écran, fichier sur disque, mais
aussi serveur, réseau etc.
6.1.2
Fichiers
Les fichiers dont nous allons traiter ici sont des fichiers possédant un support physique, un disque
en général. Ils constituent un moyen de rendre l’information persistante. En effet l’information liée
à un programme est volatile puisqu’elle cesse d’être accessible dès la fin de l’exécution de celui-ci.
Notre objectif est de pouvoir écrire dans un tel fichier depuis un programme java et de pouvoir ,
toujours depuis un programme java en récupérer le contenu.
Fichiers sur disque
Les fichiers sur disque ont une organisation interne qui dépend du système d’exploitation, OS
en anglais, présent sur la machine utilisée pour exécuter le programme (Linux, Solaris, Windows,
MacOS . . . ). De façon globale ils sont caractérisés par :
– Un nom
Plus que le nom strict du fichier, il est important de connaı̂tre le chemin d’accès au fichier :
nom strict précédé de la liste des répertoires qui séparent la racine du disque du fichier. Les
règles de formation du nom strict et du chemin d’accès sont propres à chaque système. item
Un contenu
Le contenu d’un fichier sur disque est constitué de “grains d’information élémentaires” que
l’on peut voir comme des bits, c’est à dire comme des 0 et des 1. Pour donner un sens au
contenu, on regroupe ces bits en octets ou en caractères (deux octets). Pour cela on distingue
deux types de fichiers :
– les fichiers d’octets appelés également fichiers binaires ;
– les fichiers de caractères appelés aussi fichiers-text, fichiers de texte ou fichiers ascii.
Opérations sur les fichiers
Le modèle usuel des fichiers sur disque est celui des fichiers séquentiels, où, comme dans une
LinkedList la lecture se fait à partir du début du fichier. Les opérations sur ce type de fichiers sont
les suivantes (vous remarquerez qu’elles font nécessairement intervenir le système d’exploitation de
la machine) :
– Créer un fichier ou ouverture en écriture
Cette opération a pour but de préparer à l’écriture d’information sur le disque, aussi on
préférera le terme d’ouverture en lecture ; schématiquement, cette opération établit un flux
entre le programme et le disque,, ajoute le nom du fichier sur le disque et réserve une certaine
quantité d’espace sur le disque. Par défaut, si un fichier portant le même nom est présent
sur le disque, il sera effacé (on peut envisager une opération plus complexe qui permettrait
d’ouvrir un fichier existant pour ajouter (append en anglais) de l’information à la fin du
fichier).
– Écriture
Cette opération suppose que le fichier est ouvert en écriture et utilise le flux établi entre le
programme et le disque pour envoyer de l’information (octets ou caractères) sur le disque.
p. 50
AJ.C – 12 juin 2001
6.1. INTRODUCTION
– Ouverture en lecture
Cette opération a pour but de préparer à la lecture de d’information contenue dans le fichier
sur le disque ; cette opération établit un flux entre le programme et le disque, s’assure de la
présence du fichier sur le disque et positionne la tête de lecture en debut de fichier.
– Lecture
Cette opération suppose que le fichier est ouvert en lecture et utilise le flux établi entre le
disque et le programme pour envoyer de l’information (octets ou caractères) vers le programme.
– Suppression et changement de nom
Fichiers et java
Une difficulté de l’utilisation de fichiers depuis un programme est que l’on doit garder à l’esprit
qu’un langage java par exemple, ne connaı̂t que des entités virtuelles, variables et objets, alors
que le fichier sur disque est bien réel et est de plus dépendant d’un second acteur, le système
d’exploitation. Pour tenir compte de cette dualité, tous les langages procèdent de la même façon :
on associe une variable (et un objet) qui fait référence au fichier physique. Un fichier aura donc
deux noms : le nom du fichier physique et le nom de la variable qui lui est associée et qui seule
permet de l’utiliser depuis le programme.
En java, la classe File du package java.io est utilisée à cette fin. Elle modélise en fait les chemins
d’accès aux fichiers physiques. De cette classe on utilisera principalement :
– Un constructeur
File(String) dont l’argument est le nom du fichier physique auquel on veut faire référence :
File f = new File("lettre.tex") ;
Si le fichier physique lettre.tex existe, f fait référence à ce fichier et permettra de l’utiliser
depuis le programme. Si le fichier physique n’existe pas, il n’est pas créé, mais la variable f
sera utilisée, comme nous le verrons dans la suite, pour créer lettre.tex.
– La méthodes d’instance toString()
Elle revoie le nom du fichier physique :
File f = new File("lettre.tex") ;
System.out.println(f) ;
affiche lettre.tex.
– La méthodes d’instance exists()
Elle renvoie true si le fichier physique existe, false sinon.
– La méthodes d’instance length()
Elle renvoie si le fichier physique existe, la longueur en octets du fichier et 0 sinon.
– La méthode d’instance delete
Cette méthode booléenne supprime la référence vers le fichier physique. Elle retourne true
le fichier existe et si l’opération s’est effectivement déroulée , false sinon. Dans la partie de
code suivante on suppose que la fichier lettre.tex existe.
File f = new File("lettre.tex") ;
System.out.println(f.exists()) ;
f.delete() ;
System.out.println(f.exists()) ;
affiche successivement true puis false ; la référence au fichier lettre.tex a été supprimée.
AJ.C – 12 juin 2001
p.51
CHAPITRE 6. FLUX ET FICHIERS
– La méthodes d’instance renameTo()
Cette méthode booléenne permet de renommer un fichier physique. Elle retourne true si le
fichier existe et si l’opération s’est effectivement déroulée , false sinon.
La classe File possède deux variables d’instance de type String
– pathSeparator qui contient le caractère utilisé pour séparer les noms de fichier dans une liste
de nom de fichiers ( : pour UNIX, ; pour windows)
– separator qui contient le caractère utilisé pour séparer les noms de répertoires dans un
chemin d’accès (/ pour UNIX, /as pour windows)
6.2
Quelques classes de java.io
Les classes dérivant des classe abstraites (donc non-instanciables) java.io.InputStream et
java.io.OutputStream pour les flux d’octets, et java.io.Reader etjava.io.Writer pour les flux
de caractères, sont nombreuses. Chacune apporte des fonctionnalités supplémentaires à la classe
dont elle dérive. Le tableau ci-dessous présente celles que nous utiliserons dans les exemples se
trouvant dans la section suivante.
Fonction
Flux de caractères
Flux d’octets
Ouverture en lecture d’un fichier
FileReader
FileInputStream
Ouverture en écriture d’un fichier
FileWriter
FileOutputStream
Lecture bufférisée
BufferedReader
BufferedInputStream
Écriture bufférisée
BufferedWriter
BufferedOutputStream
Conversion octet/char (lecture)
InputStreamReader
Conversion octet/char (écriture) OutputStreamWriter
Lecture des types de base
DataInputStream
Écriture des types de base
DataOutputStream
Écriture an format texte
PrintWriter
PrintStream
Lecture d’objets
ObjectInputStream
ObjectOutputStream
Écriture d’objets
Les classes apparaissant dans les deux premières lignes du tableau permettent de construire des
flux depuis (lecture) ou vers (écriture) un fichier. Ces quatre classes possèdent peu de fonctionnalités. Pour pouvoir lire ou écrire de façon efficace on utilise ce flux primaire comme argument du
constructeur d’un flux instance d’une classe possédant les caractéristiques souhaitées.
Par exemple, pour écrire dans le fichier "lettre.tex" on a besoin de la méthode println, la
même que celle utilisée par le flux System.out pour écrire à l’écran. Elle appartient à la classe
PrintWriter. On souhaite de plus que ce flux soit bufférisé (c’est-à-dire que l’écriture (ou la lecture) ne se fait pas caractère par caractère, ou octet par octet dans le cas d’un flux d’octets, mais
par paquets de caractères ou d’octets ce qui améliore considérablement la vitesse de transfert).
On va donc créer un flux de type FileWriter qui servira à construire un flux BufferedWriter,
qui servira lui-même à construire un flux PrintWriter. Ce flux servira à écrire dans le fichier eu
utilisant la méthode println :
File f = new File("lettre.tex") ;
FileWriter fw = new FileWriter(f) <-- flux primaire
BufferedWriter bw = new BufferedWriter(fw) <-- flux bufférisé
PrintWriter pw = new PrintWriter(bw) ; <-- flux bufférisé pouvant écrire
pw.println("hello le disque") ;
pw.close() ;
On peut compacter l’écriture des lignes 2 à 4 en :
p. 52
AJ.C – 12 juin 2001
6.3. EXEMPLES D’UTILISATION DES CLASSES DE JAVA.IO
File f = new File("lettre.tex") ;
PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(f))) ;
pw.println("hello le disque") ;
pw.close() ;
Toutes les classes de java.io modélisant des flux contiennent une méthode close. Cette méthode
permet de fermer le flux appelant (dans l’exemple pw). Cette opération est indispensable pour deux
raisons :
– un flux consomme des ressources systèmes ;
– la fermeture du flux vide le tampon, buffer en anglais, associé au flux : tous les caractères
ou tous les octets en attente dans le tampon sont transmis ; si le tampon n’était pas vidé le
dernier paquet de caractères ou d’octets pourrait être perdu.
6.3
6.3.1
Exemples d’utilisation des classes de java.io
Fichiers de caractères
Les fichiers de caractères ou fichiers-text peuvent être créés et lus par n’importe quel éditeur
de texte ce qui leur confère un aspect universel. Ils doivent être privilégiés pour les échanges
d’information.
Écriture d’un fichier de caractères
La plupart des méthodes des classes du package java.io génèrent des IOException. Pour
des raisons de simplicité, dans les exemples ci-dessous, on se contente de propager les exceptions
éventuelles, à défaut de la traiter.
Écriture d’un fichier de caractères
1
2
import java.io.* ;
public class TestFile1 {
3
public static void main(String[] args) throws IOException {
System.out.println("Écriture ... ") ;
File f = new File("essai.txt") ;
PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(f))) ;
pw.println("Hello le disque") ;
pw.println("c’est un PrintWriter qui ecrit") ;
pw.println(123) ;
pw.println("FIN") ;
pw.close() ;
}
4
5
6
7
8
9
10
11
12
13
14
}
Lecture d’un fichier de caractères
La classe BufferedReader possède une méthode readLine qui permet de lire un fichier-text
ligne par ligne. La lecture du fichier se fait par l’intermédiaire d’une boucle while. Lorsque la fin
du fichier est dépassée, readLine retourne null.
1
2
import java.io.* ;
public class TestFile1a {
3
AJ.C – 12 juin 2001
p.53
CHAPITRE 6. FLUX ET FICHIERS
public static void main(String[] args) throws IOException {
System.out.println("Lecture ...");
File f1 = new File("essai.txt") ;
BufferedReader br = new BufferedReader(new FileReader(f1)) ;
String ligne ;
while ((ligne = br.readLine()) != null) {
System.out.println(ligne);
}
br.close() ;
}
4
5
6
7
8
9
10
11
12
13
14
}
6.3.2
Fichiers d’octets
Les fichiers d’octets ou fichiers binaires peuvent contenir des nombres, des caractères, des valeurs
booléennes, des chaı̂nes de caractères et même des objets. La lecture d’un fichier d’octets peut poser
des problèmes si on ne connaı̂t pas le type correspondant aux octets lus.
Écrire dans un fichier d’octets
On utilise la classe DataOutputStream pour écrire la valeur de variables de type caractère,
booléen ou numérique et de chaı̂nes de caractères.
1
2
import java.io.* ;
public class TestFile2 {
3
public static void main(String[] args) throws IOException {
File f = new File("essai.data") ;
DataOutputStream ds = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream (f)) ;
ds.writeUTF("DEBUT") ;
ds.writeInt(123) ;
ds.writeDouble(12.56);
ds.writeBoolean(true) ;
ds.writeUTF("FIN") ;
ds.close() ;
}
4
5
6
7
8
9
10
11
12
13
14
15
}
La méthodewriteUTF écrit les chaı̂nes de caractères (en octets), la méthodewriteInt écrit des
entiers en octets etc.
Lecture d’un fichier d’octets
La classe DataInputStream contient les méthodes readxxx() symétriques des méthodes
readxxx, qui permettent de lire dans un fichier des séquences d’octets et de les traduire en caractères, nombres, valeurs booléennes, chaı̂nes de caractères.
1
2
import java.io.* ;
public class TestFile2a {
3
4
public static void main(String[] args) throws IOException {
p. 54
AJ.C – 12 juin 2001
6.3. EXEMPLES D’UTILISATION DES CLASSES DE JAVA.IO
File f = new File("essai.data") ;
DataInputStream ds = new DataInputStream(
new BufferedInputStream(new FileInputStream (f)) ;
System.out.println(ds.readUTF()) ;
System.out.println(ds.readInt()) ;
System.out.println(ds.readDouble()) ;
System.out.println(ds.readBoolean()) ;
System.out.println(ds.readUTF()) ;
ds.close() ;
5
6
7
8
9
10
11
12
13
}
14
15
}
Lors que l’exécution de cette classe on obtient
DEBUT
123
12.56
true
FIN
Tout se passe donc bien, mais on connaissait la structure du fichier. Pour s’en persuader exécutons
la classe suivante :
1
2
import java.io.* ;
public class TestFile2b {
3
public static void main(String[] args) throws IOException {
File f = new File("essai.data") ;
BufferedReader br = new BufferedReader (
new InputStreamReader(new FileInputStream(f))) ;
String input ;
while ((input = br.readLine()) != null) {
System.out.println(input);
}
br.close() ;
}
4
5
6
7
8
9
10
11
12
13
14
}
Commentons le texte de cette classe. Le flux créé possède le caractéristiques suivantes :
– il lit des octets
– ce flux est transformés en un flux de caractères par ( new InputStreamReader)
– il est structuré en lignes
enfin, la lecture se fait ligne par ligne par br.readLine. On obtient ainsi une sorte de lecteur
universel de caractères : tout ce qui peut être compris comme un caractère est affiché.
L’affichage obtenu est pour le mois curieux : DEBUT FIN ; ce qui montre bien que la connaissance
du contenu du fichier binaire est absolument nécessaire.
6.3.3
Utilisation de IOException
Les exceptions liées aux classes du package java.io sont nombreuses, mais deux plus particulièrement peuvent être utilisées avec profit :
AJ.C – 12 juin 2001
p.55
CHAPITRE 6. FLUX ET FICHIERS
– FileNotFoundException
Comme l’indique son nom, cette exception est générée lorsque le fichier que l’on souhaite
utiliser est absent
– EOFException
Cette exception est générée lorsqu’une lecture après la fin du fichier (EOF est l’acronyme de
End Of File, fin du fichier en français) est tentée.
Dans l’exemple suivant, on va montrer
– comment sécuriser l’ouverture d’un fichier ;
– comment on peut utilisant l’exception EOFException lire un fichier d’entiers numbers.data
dont on ne connaı̂t pas le nombre d’éléments.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.io.*;
public class TestFile2c {
public static void main(String[] args) {
try {
DataInputStream ds = new DataInputStream(
new FileInputStream("numbers.data")) ;
System.out.println("Lecture du fichier numbers.data") ;
int n ;
try {
while (true) {
n = ds.readInt() ;
System.out.println(n) ;
}
}
catch(EOFException e) {
System.out.println("Fin du fichier.") ;
}
ds.close()
}
catch(FileNotFoundException e) {
System.out.println("numbers.data introuvable") ;
}
catch(IOException e) {
System.out.println("Problemes avec numbers.data") ;
}
}
}
Le premier try est associé aux deux catch des lignes 20 et 22. On notera que l’ordre suivant lequel
sont attrapées les exceptions FileNotFoundException et IOException ne peut pas être modifié ;
en effet la casse FileNotFoundException est une sous-classe de IOException. Le second try est
associé au catch de la ligne 15 qui en attrapant les EOFException, achève la lecture du fichier dès
qu’on tente une lecture au delà de la fin du fichier.
6.3.4
Les flux in et out
Les flux in et out sont deux flux prédéfinis de la classe System :
– System.out est un PrintStream associé à l’écran (ou à la sortie standard) ; cette classe
possède les deux méthodes print et println maintes fois utilisées ;
p. 56
AJ.C – 12 juin 2001
6.3. EXEMPLES D’UTILISATION DES CLASSES DE JAVA.IO
– System.in est un InputStream associé au clavier (ou à l’entrée standard) ;
Nous allons voir comment saisir des données au clavier en utilisant System.in. La méthode
readLine de la classe BufferedReader permet la lecture de chaı̂nes de caractères mais pas celle de
valeurs numériques ou booléennes. On va donc lire les données comme des objets String que l’on
convertira ensuite :
1
2
3
4
5
6
7
8
9
10
11
12
public static String readLine( ) {
BufferedReader stdin = new BufferedReader(
new InputStreamReader(System.in));
String str="";
try {
str = stdin.readLine();
}
catch( IOException e) {
System.out.println("I/O error");
}
return str;
}
13
14
15
16
17
18
19
20
21
22
23
public static int readInt() {
String str = readLine() ;
try (
int i = Integer.parseInt(str) ;
}
catch(NumberFormatException) {
System.out.println("Format illegal") ;
}
return i ;
}
24
25
26
27
28
29
30
31
32
33
34
public static double readDouble() {
String str = readLine() ;
try (
double x = Double.parseDouble(str) ;
}
catch(NumberFormatException) {
System.out.println("Format illegal") ;
}
return x ;
}
6.3.5
Fichiers d’objets
Les classes ObjectOutputStream et ObjectInputStream permettent de créer des flux permettent respactivement de gérer des fichiers d’objets. Les oclesses d’objets qu’on peut placer dans
un fichier doivent implanter l’interface Serialisable. Cette interface est un peu particulière
puisqu’elle ne contient aucune signature de méthode. Il suffit donc de compéter la signature de
la classe dont on souhaite enregistrer les instances par implements Serializable.
1
2
import java.io.*;
class MyObject implements Serializable {
AJ.C – 12 juin 2001
p.57
CHAPITRE 6. FLUX ET FICHIERS
double x = 2.5 ;
int n = 1 ;
public String toString() {
return("x = " + x + " n = " + n) ;
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
}
class SerialTest2 {
public static void main(String[] arg) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("essai.dat")) ;
oos.writeObject(new MyObject()) ;
int[] tab1 = {10, 11, 12 ;
oos.writeObject(tab1) ;
Integer[] tab2 = {new Integer(110), new Integer(111)};
oos.writeObject(tab2) ;
oos.close();
18
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("essai.dat")) ;
MyObject o = (MyObject)ois.readObject() ;
System.out.println(o) ;
int[] tableau1 = (int[])ois.readObject() ;
System.out.println(tableau3[1]) ;
Integer[] tableau2 = (Integer[])ois.readObject() ;
System.out.println(tableau4[0]) ;
ois.close() ;
19
20
21
22
23
24
25
26
27
}
28
29
}
p. 58
AJ.C – 12 juin 2001
Téléchargement