VIII. Interfaces et classes internes▲
Les interfaces et les classes internes sont des manières plus sophistiquées d'organiser et de contrôler les objets du système construit.
C++, par exemple, ne propose pas ces mécanismes, bien que le programmeur expérimenté puisse les simuler. Le fait qu'ils soient présents dans Java indique qu'ils furent considérés comme assez importants pour être intégrés directement grâce à des mots-clefs.
Dans le chapitre 7, nous avons vu le mot-clef abstract, qui permet de créer une ou plusieurs méthodes dans une classe qui n'ont pas de définition - on fournit une partie de l'interface sans l'implémentation correspondante, qui est créée par ses héritiers. Le mot-clef interface produit une classe complètement abstraite, qui ne fournit absolument aucune implémentation. Nous verrons qu'une interface est un peu plus qu'une classe abstraite poussée à l'extrême, puisqu'elle permet d'implémenter « l'héritage multiple » du C++ en créant une classe qui peut être transtypée en plus d'un type de base.
Les classes internes ressemblent au premier abord à un simple mécanisme de dissimulation de code : on crée une classe à l'intérieur d'autres classes. Cependant, les classes internes font plus que cela - elles connaissent et peuvent communiquer avec la classe principale - ; sans compter que le code produit en utilisant les classes internes est plus élégant et compréhensible, bien que ce soit un concept nouveau pour beaucoup. Cela prend un certain temps avant d'intégrer les classes internes dans la conception.
VIII-A. Interfaces▲
Le mot-clef interface pousse le concept abstract un cran plus loin. On peut y penser comme à une classe « purement » abstract. Il permet au créateur d'établir la forme qu'aura la classe : les noms des méthodes, les listes d'arguments et les types de retours, mais pas les corps des méthodes. Une interface peut aussi contenir des données membres, mais elles seront implicitement static et final. Une interface fournit un patron pour la classe, mais aucune implémentation.
Une interface déclare : « Voici ce à quoi ressembleront toutes les classes qui implémenteront cette interface ». Ainsi, tout code utilisant une interface particulière sait quelles méthodes peuvent être appelées pour cette interface, et c'est tout. Une interface est donc utilisée pour établir un « protocole » entre les classes (certains langages de programmation orientés objet ont un mot-clef protocol pour réaliser la même chose).
Pour créer une interface, utilisez le mot-clef interface à la place du mot-clef class. Comme pour une classe, on peut ajouter le mot-clef public devant le mot-clef interface (mais seulement si l'interface est définie dans un fichier du même nom) ou ne rien mettre pour lui donner le statut « amical » afin qu'elle ne soit utilisable que dans le même package.
Le mot-clef implements permet de rendre une classe conforme à une interface particulière (ou à un groupe d'interfaces). Il dit en gros : « L'interface spécifie ce à quoi la classe ressemble, mais maintenant on va spécifier comment cela fonctionne ». Sinon, cela s'apparente à de l'héritage. Le diagramme des instruments de musique suivant le montre :
Vous pouvez constater d'après les classes Woodwind et Brass qu’une fois une interface implémentée, cette implémentation devient une classe ordinaire qui peut être étendue d'une façon tout à fait classique.
On peut choisir de déclarer explicitement les méthodes d'une interface comme public. Mais elles sont public même sans le préciser. C'est pourquoi il faut définir les méthodes d'une interface comme public quand on implémente une interface. Autrement elles sont « amicales » par défaut, impliquant une réduction de l'accessibilité d'une méthode durant l'héritage, ce qui est interdit par le compilateur Java.
On peut le voir dans cette version modifiée de l'exemple Instrument. Notons que chaque méthode de l'interface n'est strictement qu'une déclaration, la seule chose que le compilateur permette. De plus, aucune des méthodes d'Instrument n'est déclarée comme public, mais elles le sont automatiquement.
//: c08:music5:Music5.java
// Interfaces.
import
java.util.*;
interface
Instrument {
// Constante compilée :
int
i =
5
; // static & final
// Définitions de méthodes interdites :
void
play
(
); // Automatiquement public
String what
(
);
void
adjust
(
);
}
class
Wind implements
Instrument {
public
void
play
(
) {
System.out.println
(
"Wind.play()"
);
}
public
String what
(
) {
return
"Wind"
; }
public
void
adjust
(
) {}
}
class
Percussion implements
Instrument {
public
void
play
(
) {
System.out.println
(
"Percussion.play()"
);
}
public
String what
(
) {
return
"Percussion"
; }
public
void
adjust
(
) {}
}
class
Stringed implements
Instrument {
public
void
play
(
) {
System.out.println
(
"Stringed.play()"
);
}
public
String what
(
) {
return
"Stringed"
; }
public
void
adjust
(
) {}
}
class
Brass extends
Wind {
public
void
play
(
) {
System.out.println
(
"Brass.play()"
);
}
public
void
adjust
(
) {
System.out.println
(
"Brass.adjust()"
);
}
}
class
Woodwind extends
Wind {
public
void
play
(
) {
System.out.println
(
"Woodwind.play()"
);
}
public
String what
(
) {
return
"Woodwind"
; }
}
public
class
Music5 {
// Le type n'est pas important, donc les nouveaux
// types ajoutés au système marchent sans problème :
static
void
tune
(
Instrument i) {
// ...
i.play
(
);
}
static
void
tuneAll
(
Instrument[] e) {
for
(
int
i =
0
; i <
e.length; i++
)
tune
(
e[i]);
}
public
static
void
main
(
String[] args) {
Instrument[] orchestra =
new
Instrument[5
];
int
i =
0
;
// Transtypage ascendant durant le stockage dans le tableau :
orchestra[i++
] =
new
Wind
(
);
orchestra[i++
] =
new
Percussion
(
);
orchestra[i++
] =
new
Stringed
(
);
orchestra[i++
] =
new
Brass
(
);
orchestra[i++
] =
new
Woodwind
(
);
tuneAll
(
orchestra);
}
}
///:~
Le reste du code fonctionne de la même manière. Peu importe si vous « upcastez » à une classe « régulière » appelée Instrument, une classe abstract Instrument, ou à une interface Instrument. Le comportement est le même. En fait, vous pouvez voir dans la méthode tune() qu'il n'y a pas de précisions sur le fait que Instrument soit une classe « régulière », une classe abstract, ou une interface. Voici le principe : chaque approche permet au programmeur de contrôler la façon dont les objets sont créés et utilisés.
VIII-A-1. « Héritage multiple » en Java▲
Une interface n'est pas simplement une forme « plus pure » d'une classe abstract. Elle a un but plus important que cela. Puisqu'une interface ne dispose d'aucune implémentation - autrement dit, aucun stockage n'est associé à une interface -, rien n'empêche de combiner plusieurs interfaces. Ceci est intéressant, car certaines fois on a la relation « Un x est un a et un b et un c ». En C++, le fait de combiner les interfaces de plusieurs classes est appelé héritage multiple, et entraîne une lourde charge du fait que chaque classe peut avoir sa propre implémentation. En Java, on peut réaliser la même chose, mais une seule classe peut avoir une implémentation, donc les problèmes rencontrés en C++ n'apparaissent pas en Java lorsqu'on combine les interfaces multiples :
Dans une classe dérivée, on n'est pas forcé d'avoir une classe de base qui soit abstract ou « concrète » (i.e. sans méthode abstract). Si une classe hérite d'une classe qui n'est pas une interface, elle ne peut dériver que de cette seule classe. Tous les autres types de base doivent être des interfaces. On place les noms des interfaces après le mot-clef implements en les séparant par des virgules. On peut spécifier autant d'interfaces qu'on veut - chacune devient un type indépendant vers lequel on peut transtyper. L'exemple suivant montre une classe concrète combinée à plusieurs interfaces pour produire une nouvelle classe :
//: c08:Adventure.java
// Interfaces multiples.
import
java.util.*;
interface
CanFight {
void
fight
(
);
}
interface
CanSwim {
void
swim
(
);
}
interface
CanFly {
void
fly
(
);
}
class
ActionCharacter {
public
void
fight
(
) {}
}
class
Hero extends
ActionCharacter
implements
CanFight, CanSwim, CanFly {
public
void
swim
(
) {}
public
void
fly
(
) {}
}
public
class
Adventure {
static
void
t
(
CanFight x) {
x.fight
(
); }
static
void
u
(
CanSwim x) {
x.swim
(
); }
static
void
v
(
CanFly x) {
x.fly
(
); }
static
void
w
(
ActionCharacter x) {
x.fight
(
); }
public
static
void
main
(
String[] args) {
Hero h =
new
Hero
(
);
t
(
h); // Le traite comme un CanFight
u
(
h); // Le traite comme un CanSwim
v
(
h); // Le traite comme un CanFly
w
(
h); // Le traite comme un ActionCharacter
}
}
///:~
Ici, Hero combine la classe concrète ActionCharacter avec les interfaces CanFight, CanSwim et CanFly. Quand on combine une classe concrète avec des interfaces de cette manière, la classe concrète doit être spécifiée en premier, avant les interfaces (autrement le compilateur génère une erreur).
Notons que la signature de fight() est la même dans l'interface CanFight et dans la classe ActionCharacter, et que Hero ne fournit pas de définition pour fight(). On peut hériter d'une interface (comme on va le voir bientôt), mais dans ce cas on a une autre interface. Si on veut créer un objet de ce nouveau type, ce doit être une classe implémentant toutes les définitions. Bien que la classe Hero ne fournisse pas explicitement une définition pour fight(), la définition est fournie par ActionCharacter, donc héritée par Hero et il est ainsi possible de créer des objets Hero.
Dans la classe Adventure, on peut voir quatre méthodes prenant les diverses interfaces et la classe concrète en argument. Quand un objet Hero est créé, il peut être utilisé dans chacune de ces méthodes, ce qui veut dire qu'il est transtypé tour à tour dans chaque interface. De la façon dont cela est conçu en Java, cela fonctionne sans problème et sans effort supplémentaire de la part du programmeur.
L'intérêt principal des interfaces est démontré dans l'exemple précédent : être capable de transtyper vers plus d'un type de base. Cependant, une seconde raison, la même que pour les classes de base abstract, plaide pour l'utilisation des interfaces : empêcher le programmeur client de créer un objet de cette classe et spécifier qu'il ne s'agit que d'une interface. Cela soulève une question : faut-il utiliser une interface ou une classe abstract ? Une interface apporte les bénéfices d'une classe abstract et les bénéfices d'une interface, donc s'il est possible de créer la classe de base sans définir de méthodes ou de données membres, il faut toujours préférer les interfaces aux classes abstract. En fait, si on sait qu'un type sera amené à être dérivé, il faut le créer d'emblée comme une interface, et ne le changer en classe abstract, voire en classe concrète, que si on est forcé d'y placer des définitions de méthodes ou des données membres.
VIII-A-1-a. Combinaison d'interfaces et collisions de noms▲
On peut rencontrer un problème lorsqu'on implémente plusieurs interfaces. Dans l'exemple précédent, CanFight et ActionCharacter ont tous les deux une méthode void fight() identique. Cela ne pose pas de problèmes parce que la méthode est identique dans les deux cas, mais que se passe-t-il lorsque ce n'est pas le cas ? Voici un exemple :
//: c08:InterfaceCollision.java
interface
I1 {
void
f
(
); }
interface
I2 {
int
f
(
int
i); }
interface
I3 {
int
f
(
); }
class
C {
public
int
f
(
) {
return
1
; }
}
class
C2 implements
I1, I2 {
public
void
f
(
) {}
public
int
f
(
int
i) {
return
1
; }
// surchargée
}
class
C3 extends
C implements
I2 {
public
int
f
(
int
i) {
return
1
; }
// surchargée
}
class
C4 extends
C implements
I3 {
// Identique, pas de problème :
public
int
f
(
) {
return
1
; }
}
// Les méthodes diffèrent seulement par le type de retour :
//! class C5 extends C implements I1 {}
//! interface I4 extends I1, I3 {} ///:~
Les difficultés surviennent parce que la redéfinition, l'implémentation et la surcharge sont toutes les trois utilisées ensemble, et que les fonctions surchargées ne peuvent différer seulement par leur type de retour. Quand les deux dernières lignes sont décommentées, le message d'erreur est explicite :
InterfaceCollision.java:23: f( ) in C cannot implement f( ) in I1; attempting to use incompatible return type
found : int
required: void
InterfaceCollision.java:24: interfaces I3 and I1 are incompatible; both define f( ), but with different return type
De toute façon, utiliser les mêmes noms de méthode dans différentes interfaces destinées à être combinées affecte la compréhension du code. Tachez donc de l'éviter.
VIII-A-2. Étendre une interface avec l'héritage▲
On peut facilement ajouter de nouvelles déclarations de méthodes à une interface en la dérivant, de même qu'on peut combiner plusieurs interfaces dans une nouvelle interface grâce à l'héritage. Dans les deux cas on a une nouvelle interface, comme dans l'exemple suivant :
//: c08:HorrorShow.java
// Extension d'une interface grâce à l'héritage.
interface
Monster {
void
menace
(
);
}
interface
DangerousMonster extends
Monster {
void
destroy
(
);
}
interface
Lethal {
void
kill
(
);
}
class
DragonZilla implements
DangerousMonster {
public
void
menace
(
) {}
public
void
destroy
(
) {}
}
interface
Vampire extends
DangerousMonster, Lethal {
void
drinkBlood
(
);
}
class
VeryBadVampire implements
Vampire {
public
void
menace
(
) {}
public
void
destroy
(
) {}
public
void
kill
(
) {}
public
void
drinkBlood
(
) {}
}
public
class
HorrorShow {
static
void
u
(
Monster b) {
b.menace
(
); }
static
void
v
(
DangerousMonster d) {
d.menace
(
);
d.destroy
(
);
}
static
void
w
(
Lethal l) {
l.kill
(
); }
public
static
void
main
(
String[] args) {
DangerousMonster barney =
new
DragonZilla
(
);
u
(
barney);
v
(
barney);
Vampire vlad =
new
VeryBadVampire
(
);
u
(
vlad);
v
(
vlad);
w
(
vlad);
}
}
///:~
DangerousMonster est une simple extension de Monster qui fournit une nouvelle interface. Elle est implémentée dans DragonZilla.
La syntaxe utilisée dans Vampire n'est valide que lorsqu'on dérive des interfaces. Normalement, on ne peut utiliser extends qu'avec une seule classe, mais comme une interface peut être constituée de plusieurs autres interfaces, extends peut se référer à plusieurs interfaces de base lorsqu'on construit une nouvelle interface. Comme vous pouvez le voir, les noms d'interface sont simplement séparés par des virgules.
VIII-A-3. Groupes de constantes▲
Puisque toutes les données membres d'une interface sont automatiquement static et final, une interface est un outil pratique pour créer des groupes de constantes, un peu comme avec le enum du C ou du C++. Par exemple :
//: c08:Months.java
// Utiliser les interfaces pour créer des groupes de constantes.
package
c08;
public
interface
Months {
int
JANUARY =
1
, FEBRUARY =
2
, MARCH =
3
,
APRIL =
4
, MAY =
5
, JUNE =
6
, JULY =
7
,
AUGUST =
8
, SEPTEMBER =
9
, OCTOBER =
10
,
NOVEMBER =
11
, DECEMBER =
12
;
}
///:~
Notons au passage l'utilisation des conventions de style Java pour les champs static finals initialisés par des constantes : rien que des majuscules (avec des underscores pour séparer les mots à l'intérieur d'un identifiant).
Les données membres d'une interface sont automatiquement public, il n'est donc pas nécessaire de le spécifier.
Maintenant on peut utiliser les constantes à l'extérieur du package en important c08.* ou c08.Months de la même manière qu'on le ferait avec n'importe quel autre package, et référencer les valeurs avec des expressions comme Months.JANUARY. Bien sûr, on ne récupère qu'un int, il n'y a donc pas de vérification additionnelle de type comme celle dont dispose l'enum du C++, mais cette technique (couramment utilisée) reste tout de même une grosse amélioration comparée aux nombres codés en dur dans les programmes (appelés « nombres magiques » et produisant un code pour le moins difficile à maintenir).
Si on veut une vérification additionnelle de type, on peut construire une classe de la manière suivante : (33)
//: c08:Month.java
// Un système d'énumération plus robuste.
package
c08;
import
com.bruceeckel.simpletest.*;
public
final
class
Month {
private
static
Test monitor =
new
Test
(
);
private
String name;
private
Month
(
String nm) {
name =
nm; }
public
String toString
(
) {
return
name; }
public
static
final
Month
JAN =
new
Month
(
"January"
),
FEB =
new
Month
(
"February"
),
MAR =
new
Month
(
"March"
),
APR =
new
Month
(
"April"
),
MAY =
new
Month
(
"May"
),
JUN =
new
Month
(
"June"
),
JUL =
new
Month
(
"July"
),
AUG =
new
Month
(
"August"
),
SEP =
new
Month
(
"September"
),
OCT =
new
Month
(
"October"
),
NOV =
new
Month
(
"November"
),
DEC =
new
Month
(
"December"
);
public
static
final
Month[] month =
{
JAN, FEB, MAR, APR, MAY, JUN,
JUL, AUG, SEP, OCT, NOV, DEC
}
;
public
static
final
Month number
(
int
ord) {
return
month[ord -
1
];
}
public
static
void
main
(
String[] args) {
Month m =
Month.JAN;
System.out.println
(
m);
m =
Month.number
(
12
);
System.out.println
(
m);
System.out.println
(
m ==
Month.DEC);
System.out.println
(
m.equals
(
Month.DEC));
System.out.println
(
Month.month[3
]);
monitor.expect
(
new
String[] {
"January"
,
"December"
,
"true"
,
"true"
,
"April"
}
);
}
}
///:~
Month est une classe final avec un constructeur private, afin que personne ne puisse la dériver ou en faire une instance. Les seules instances sont celles static final créées dans la classe elle-même : JAN, FEB, MAR, etc. Ces objets sont aussi utilisés dans le tableau Month, ce qui vous donne la possibilité d'itérer sur un tableau d'objet Month. La méthode number( ) vous permet de sélectionner un Month en utilisant sont numéro de mois correspondant. Dans main() on dispose de la vérification additionnelle de type : m est un objet Month et ne peut donc se voir assigné qu'un Month. L'exemple précédent Months.java ne fournissait que des valeurs int, et donc une variable int destinée à représenter un mois pouvait en fait recevoir n'importe quelle valeur entière, ce qui n'était pas très sûr.
Cette approche nous permet aussi d'utiliser indifféremment == ou equals(), ainsi que le montre la fin de main(). Ceci fonctionne, car il ne peut y avoir qu'une seule instance de chaque valeur de Month. Dans le Chapitre 11, vous apprendrez une autre manière d'utiliser les classes afin de comparer les objets entre eux.
Il y a aussi un champ Month dans java.util.Calendar.
Le projet Apache Jakarta Commons possède un outil pour créer des énumérations de façon similaire à ce qui à été vu dans l'exemple précédent, mais avec mois d'effort. Voir http://jakarta.apache.org/commons, dans « lang, » dans le package org.apache.commons.lang.enum. Ce projet possède aussi de nombreuses autres librairies très utiles.
VIII-A-4. Initialisation des champs dans les interfaces▲
Les champs définis dans les interfaces sont automatiquement static et final. Ils ne peuvent être des « finals vides », mais peuvent être initialisés avec des expressions non constantes. Par exemple :
//: c08:RandVals.java
// Initialisation de champs d'interface
// avec des valeurs non constantes.
import
java.util.*;
public
interface
RandVals {
Random rand =
new
Random
(
);
int
randomInt =
rand.nextInt
(
10
);
long
randomLong =
rand.nextLong
(
) *
10
;
float
randomFloat =
rand.nextLong
(
) *
10
;
double
randomDouble =
rand.nextDouble
(
) *
10
;
}
///:~
Puisque les champs sont static, ils sont initialisés quand la classe est chargée pour la première fois, ce qui arrive quand n'importe lequel des champs est accédé pour la première fois. Voici un simple test :
//: c08:TestRandVals.java
import
com.bruceeckel.simpletest.*;
public
class
TestRandVals {
private
static
Test monitor =
new
Test
(
);
public
static
void
main
(
String[] args) {
System.out.println
(
RandVals.randomInt);
System.out.println
(
RandVals.randomLong);
System.out.println
(
RandVals.randomFloat);
System.out.println
(
RandVals.randomDouble);
monitor.expect
(
new
String[] {
"%% -?
\\
d+"
,
"%% -?
\\
d+"
,
"%% -?
\\
d
\\
.
\\
d+E?-?
\\
d+"
,
"%% -?
\\
d
\\
.
\\
d+E?-?
\\
d+"
}
);
}
}
///:~
Les données membres, bien sûr, ne font pas partie de l'interface, mais sont stockées dans la zone de stockage static de cette interface.
VIII-A-5. Interfaces imbriquées▲
Les interfaces peuvent être imbriquées dans des classes ou à l'intérieur d'autres interfaces. (34) Ceci révèle nombre de fonctionnalités intéressantes :
//: c08:NestingInterfaces.java
class
A {
interface
B {
void
f
(
);
}
public
class
BImp implements
B {
public
void
f
(
) {}
}
private
class
BImp2 implements
B {
public
void
f
(
) {}
}
public
interface
C {
void
f
(
);
}
class
CImp implements
C {
public
void
f
(
) {}
}
private
class
CImp2 implements
C {
public
void
f
(
) {}
}
private
interface
D {
void
f
(
);
}
private
class
DImp implements
D {
public
void
f
(
) {}
}
public
class
DImp2 implements
D {
public
void
f
(
) {}
}
public
D getD
(
) {
return
new
DImp2
(
); }
private
D dRef;
public
void
receiveD
(
D d) {
dRef =
d;
dRef.f
(
);
}
}
interface
E {
interface
G {
void
f
(
);
}
// « public » est redondant :
public
interface
H {
void
f
(
);
}
void
g
(
);
// Ne peut pas être private dans une interface :
//! private interface I {}
}
public
class
NestingInterfaces {
public
class
BImp implements
A.B {
public
void
f
(
) {}
}
class
CImp implements
A.C {
public
void
f
(
) {}
}
// Ne peut pas implémenter une interface private sauf
// à l'intérieur de la classe définissant cette interface :
//! class DImp implements A.D {
//! public void f() {}
//! }
class
EImp implements
E {
public
void
g
(
) {}
}
class
EGImp implements
E.G {
public
void
f
(
) {}
}
class
EImp2 implements
E {
public
void
g
(
) {}
class
EG implements
E.G {
public
void
f
(
) {}
}
}
public
static
void
main
(
String[] args) {
À a =
new
A
(
);
// Ne peut accéder à A.D :
//! A.D ad = a.getD();
// Ne renvoie qu'un A.D :
//! A.DImp2 di2 = a.getD();
// Ne peut accéder à un membre de l'interface :
//! a.getD().f();
// Seul un autre A peut faire quelque chose avec getD() :
À a2 =
new
A
(
);
a2.receiveD
(
a.getD
(
));
}
}
///:~
La syntaxe permettant d'imbriquer une interface à l'intérieur d'une classe est relativement évidente ; et comme les interfaces non imbriquées, elles peuvent avoir une visibilité public ou « amicale ». On peut aussi constater que les interfaces public et « amicales » peuvent être implémentées dans des classes imbriquées public, « amicales » ou private.
Une nouvelle astuce consiste à rendre les interfaces private comme A.D (la même syntaxe est utilisée pour la qualification des interfaces imbriquées et pour les classes imbriquées). À quoi sert une interface imbriquée private ? On pourrait penser qu'elle ne peut être implémentée que comme une classe private imbriquée comme DImp, mais A.DImp2 montre qu'elle peut aussi être implémentée dans une classe public. Cependant, A.DImp2 ne peut être utilisée que comme elle-même : on ne peut mentionner le fait qu'elle implémente l'interface private, et donc implémenter une interface private est une manière de forcer la définition des méthodes de cette interface sans ajouter aucune information de type (c'est-à-dire, sans autoriser de transtypage ascendant).
La méthode getD( ) se trouve quant à elle dans une impasse du fait de l'interface private : c'est une méthode public qui renvoie une référence à une interface private. Que peut-on faire avec la valeur de retour de cette méthode ? Dans main( ), on peut voir plusieurs tentatives pour utiliser cette valeur de retour, qui échouent toutes. La seule solution possible est lorsque la valeur de retour est gérée par un objet qui a la permission de l'utiliser - dans ce cas, un objet A, via la méthode receiveD( ).
L'interface E montre que les interfaces peuvent être imbriquées les unes dans les autres. Cependant, les règles portant sur les interfaces - en particulier celle stipulant que tous les éléments doivent être public - sont strictement appliquées, donc une interface imbriquée à l'intérieur d'une autre interface est automatiquement public et ne peut être déclarée private.
NestingInterfaces montre les différentes manières dont les interfaces imbriquées peuvent être implémentées. En particulier, il est bon de noter que lorsqu'on implémente une interface, on n'est pas obligé d'en implémenter les interfaces imbriquées. De plus, les interfaces private ne peuvent être implémentées en dehors de leur classe de définition.
On peut penser que ces fonctionnalités n'ont été introduites que pour assurer une cohérence syntaxique, mais j'ai remarqué qu'une fois qu'une fonctionnalité est connue, on découvre souvent des endroits où elle se révèle utile.
VIII-B. Classes internes▲
Il est possible de placer la définition d'une classe à l'intérieur de la définition d'une autre classe. C'est ce qu'on appelle une classe interne. Les classes internes sont une fonctionnalité importante du langage, car elles permettent de grouper les classes qui sont logiquement rattachées entre elles, et de contrôler la visibilité de l'une à partir de l'autre. Cependant, il est important de comprendre que le mécanisme des classes internes est complètement différent de celui de la composition.
Souvent, lorsqu'on en entend parler pour la première fois, l'intérêt des classes internes n'est pas immédiatement évident. À la fin de cette section, après avoir discuté de la syntaxe et de la sémantique des classes internes, vous trouverez des exemples qui devraient clairement montrer les bénéfices des classes internes.
Une classe interne est créée comme on pouvait s'y attendre - en plaçant la définition de la classe à l'intérieur d'une autre classe :
//: c08:Parcel1.java
// Création de classes internes.
public
class
Parcel1 {
class
Contents {
private
int
i =
11
;
public
int
value
(
) {
return
i; }
}
class
Destination {
private
String label;
Destination
(
String whereTo) {
label =
whereTo;
}
String readLabel
(
) {
return
label; }
}
// L'utilisation d'une classe interne ressemble à
// l'utilisation de n'importe quelle autre classe depuis Parcell :
public
void
ship
(
String dest) {
Contents c =
new
Contents
(
);
Destination d =
new
Destination
(
dest);
System.out.println
(
d.readLabel
(
));
}
public
static
void
main
(
String[] args) {
Parcel1 p =
new
Parcel1
(
);
p.ship
(
"Tanzania"
);
}
}
///:~
Les classes internes, quand elles sont utilisées dans ship(), ressemblent à n'importe quelle autre classe. La seule différence en est que les noms sont imbriqués dans Parcel1. Mais nous allons voir dans un moment que ce n'est pas la seule différence.
Plus généralement, une classe externe peut définir une méthode qui renvoie une référence à une classe interne, comme ceci :
//: c08:Parcel2.java
// Renvoyer une référence à une classe interne.
public
class
Parcel2 {
class
Contents {
private
int
i =
11
;
public
int
value
(
) {
return
i; }
}
class
Destination {
private
String label;
Destination
(
String whereTo) {
label =
whereTo;
}
String readLabel
(
) {
return
label; }
}
public
Destination to
(
String s) {
return
new
Destination
(
s);
}
public
Contents cont
(
) {
return
new
Contents
(
);
}
public
void
ship
(
String dest) {
Contents c =
cont
(
);
Destination d =
to
(
dest);
System.out.println
(
d.readLabel
(
));
}
public
static
void
main
(
String[] args) {
Parcel2 p =
new
Parcel2
(
);
p.ship
(
"Tanzania"
);
Parcel2 q =
new
Parcel2
(
);
// Définition de références sur des classes internes :
Parcel2.Contents c =
q.cont
(
);
Parcel2.Destination d =
q.to
(
"Borneo"
);
}
}
///:~
Si on veut créer un objet de la classe interne ailleurs que dans une méthode non-static de la classe externe, il faut spécifier le type de cet objet comme NomDeClasseExterne.NomDeClasseInterne, comme on peut le voir dans main().
VIII-B-1. Classes internes et transtypage ascendant▲
Jusqu'à présent, les classes internes ne semblent pas si dramatiques. Après tout, si le but recherché est le camouflage, Java propose déjà un très bon mécanisme pour cela - il suffit de donner l'accès package à la classe (visible seulement depuis un package) plutôt que de la déclarer comme une classe interne.
Cependant, les classes internes prennent de l'intérêt lorsque l'on commence à transtyper vers une classe de base, et en particulier vers une interface. (produire une référence vers une interface depuis un objet l'implémentant revient à transtyper vers une classe de base). C'est parce que la classe interne - l'implémentation de l'interface - peut être complètement masquée et indisponible pour tout le monde, ce qui est pratique pour cacher l'implémentation. La seule chose qu'on récupère est une référence sur la classe de base ou l'interface.
Tout d'abord, les interfaces sont définies dans leurs propres fichiers afin de pouvoir être utilisées dans tous les exemples :
//: c08:Destination.java
public
interface
Destination {
String readLabel
(
);
}
///:~
//: c08:Contents.java
public
interface
Contents {
int
value
(
);
}
///:~
Maintenant Contents et Destination sont des interfaces disponibles pour le programmeur client. (L'interface, souvenez-vous, déclare automatiquement tous ses membres public).
Quand on récupère une référence sur la classe de base ou l'interface, il est possible qu'on ne puisse même pas en découvrir le type exact, comme on peut le voir dans le code suivant :
//: c08:TestParcel.java
// Renvoyer une référence sur une classe interne.
class
Parcel3 {
private
class
PContents implements
Contents {
private
int
i =
11
;
public
int
value
(
) {
return
i; }
}
protected
class
PDestination implements
Destination {
private
String label;
private
PDestination
(
String whereTo) {
label =
whereTo;
}
public
String readLabel
(
) {
return
label; }
}
public
Destination dest
(
String s) {
return
new
PDestination
(
s);
}
public
Contents cont
(
) {
return
new
PContents
(
);
}
}
public
class
TestParcel {
public
static
void
main
(
String[] args) {
Parcel3 p =
new
Parcel3
(
);
Contents c =
p.cont
(
);
Destination d =
p.dest
(
"Tanzania"
);
// Illégal -- ne peut accéder à une classe private :
//! Parcel3.PContents pc = p.new PContents();
}
}
///:~
Dans cet exemple, main( ) doit être dans une classe séparée afin de démontrer le caractère private de la classe interne PContents.
Dans Parcel3, de nouvelles particularités ont été ajoutées : la classe interne PContents est private, afin que seule Parcel3 puisse y accéder. PDestination est protected, afin que seules Parcel3, les classes du même package (puisque protected fournit aussi un accès package), et les héritiers de Parcel3 puissent accéder à PDestination. Cela signifie que le programmeur client n'a qu'une connaissance et des accès restreints à ces membres. En fait, on ne peut même pas transtyper vers une classe interne private (ou une classe interne protected à moins d'en hériter), parce qu'on ne peut accéder à son nom, comme on peut le voir dans la classe TestParcel. La classe interne private fournit donc un moyen pour le concepteur de la classe d'interdire tout code testant le type et de cacher complètement les détails de l'implémentation. De plus, l'extension d'une interface est inutile du point de vue du programmeur client puisqu'il ne peut accéder à aucune méthode additionnelle ne faisant pas partie de l'interface public. Cela permet aussi au compilateur Java de générer du code plus efficace.
Les classes normales (non internes) ne peuvent pas être déclarées private ou protected; mais uniquement public ou en accès package.
VIII-B-2. Classes internes dans les méthodes et les domaines d'application▲
Ce qu'on a pu voir jusqu'à présent constitue l'utilisation typique des classes internes. En général, le code impliquant des classes internes que vous serez amené à lire et à écrire ne mettra en œuvre que des classes internes « régulières », et sera simple à comprendre. Cependant, le support des classes internes est relativement complet, et il existe de nombreuses autres manières, plus obscures, de les utiliser si on le souhaite ; les classes internes peuvent être créées à l'intérieur d'une méthode ou même d'une portée quelconque. Deux raisons possibles à cela :
- Comme montré précédemment, on implémente une interface d'un certain type afin de pouvoir créer et renvoyer une référence.
- On résout un problème compliqué pour lequel la création d'une classe aiderait grandement, mais on ne veut pas la rendre publiquement accessible.
Dans les exemples suivants, le code précédent est modifié afin d'utiliser :
- Une classe définie dans une méthode
- Une classe définie dans une portée à l'intérieur d'une méthode
- Une classe anonyme implémentant une interface
- Une classe anonyme étendant une classe qui dispose d'un constructeur autre que le constructeur par défaut
- Une classe anonyme réalisant des initialisations de champs
- Une classe anonyme qui se construit en initialisant des instances (les classes internes anonymes ne peuvent pas avoir de constructeurs)
Bien que ce soit une classe ordinaire avec une implémentation, Wrapping est aussi utilisée comme une « interface » commune pour ses classes dérivées :
//: c08:Wrapping.java
public
class
Wrapping {
private
int
i;
public
Wrapping
(
int
x) {
i =
x; }
public
int
value
(
) {
return
i; }
}
///:~
Notez que Wrapping dispose d'un constructeur requérant un argument, afin de rendre les choses un peu plus intéressantes.
Le premier exemple montre la création d'une classe entière dans l'étendue d'une méthode (au lieu de l'étendue d'une autre classe). Cela est appelé une classe interne locale :
//: c08:Parcel4.java
//Imbrication une classe au sein d'une méthode.
public
class
Parcel4 {
public
Destination dest
(
String s) {
class
PDestination implements
Destination {
private
String label;
private
PDestination
(
String whereTo) {
label =
whereTo;
}
public
String readLabel
(
) {
return
label; }
}
return
new
PDestination
(
s);
}
public
static
void
main
(
String[] args) {
Parcel4 p =
new
Parcel4
(
);
Destination d =
p.dest
(
"Tanzania"
);
}
}
///:~
La classe PDestination est une partie de dest( ) plutôt que de Parcel4. (Notez aussi qu'on peut utiliser l'identifiant de classe PDestination pour une classe interne à l'intérieur de chaque classe du même sous-répertoire sans collision de nom). Par conséquent, PDestination ne peut pas être accédée en dehors de dest( ). Notez le transtypage ascendant réalisé par l'instruction de retour - dest( ) ne peut renvoyer qu'une référence à Destination, la classe de base. Bien sûr, le fait que le nom de la classe PDestination soit placé à l'intérieur de dest( ) ne veut pas dire que PDestination n'est pas un objet valide une fois revenu de dest( ).
L'exemple suivant montre comment on peut imbriquer une classe interne à l'intérieur de n'importe quelle étendue arbitraire :
//: c08:Parcel5.java
// Imbrication une classe à l'intérieur d'une étendue
public
class
Parcel5 {
private
void
internalTracking
(
boolean
b) {
if
(
b) {
class
TrackingSlip {
private
String id;
TrackingSlip
(
String s) {
id =
s;
}
String getSlip
(
) {
return
id; }
}
TrackingSlip ts =
new
TrackingSlip
(
"slip"
);
String s =
ts.getSlip
(
);
}
// Utilisation impossible ici ! En dehors de l'étendue :
//! TrackingSlip ts = new TrackingSlip("x");
}
public
void
track
(
) {
internalTracking
(
true
); }
public
static
void
main
(
String[] args) {
Parcel5 p =
new
Parcel5
(
);
p.track
(
);
}
}
///:~
La classe TrackingSlip est définie dans l'étendue de l'instruction if. Cela ne veut pas dire que la classe classe est créée conditionnellement - elle est compilée avec tout le reste. Cependant, elle n'est pas accessible en dehors de l'étendue dans laquelle elle est définie. Mis à part cette restriction, elle ressemble à n'importe quelle autre classe ordinaire.
VIII-B-3. Anonymous inner classes▲
The next example looks a little strange:
//: c08:Parcel6.java
// A method that returns an anonymous inner class.
public
class
Parcel6 {
public
Contents cont
(
) {
return
new
Contents
(
) {
private
int
i =
11
;
public
int
value
(
) {
return
i; }
}
; // Semicolon required in this case
}
public
static
void
main
(
String[] args) {
Parcel6 p =
new
Parcel6
(
);
Contents c =
p.cont
(
);
}
}
///:~
The cont( ) method combines the creation of the return value with the definition of the class that represents that return value! In addition, the class is anonymous; it has no name. To make matters a bit worse, it looks like you're starting out to create a Contents object:
return
new
Contents
(
)
But then, before you get to the semicolon, you say, « But wait, I think I'll slip in a class definition »:
return
new
Contents
(
) {
private
int
i =
11
;
public
int
value
(
) {
return
i; }
}
;
What this strange syntax means is: « Create an object of an anonymous class that's inherited from Contents. » The reference returned by the new expression is automatically upcast to a Contents reference. The anonymous inner-class syntax is a shorthand for:
class
MyContents implements
Contents {
private
int
i =
11
;
public
int
value
(
) {
return
i; }
}
return
new
MyContents
(
);
In the anonymous inner class, Contents is created by using a default constructor. The following code shows what to do if your base class needs a constructor with an argument:
//: c08:Parcel7.java
// An anonymous inner class that calls
// the base-class constructor.
public
class
Parcel7 {
public
Wrapping wrap
(
int
x) {
// Base constructor call:
return
new
Wrapping
(
x) {
// Pass constructor argument.
public
int
value
(
) {
return
super
.value
(
) *
47
;
}
}
; // Semicolon required
}
public
static
void
main
(
String[] args) {
Parcel7 p =
new
Parcel7
(
);
Wrapping w =
p.wrap
(
10
);
}
}
///:~
That is, you simply pass the appropriate argument to the base-class constructor, seen here as the x passed in new Wrapping(x).
The semicolon at the end of the anonymous inner class doesn't mark the end of the class body (as it does in C++). Instead, it marks the end of the expression that happens to contain the anonymous class. Thus, it's identical to the use of the semicolon everywhere else.
You can also perform initialization when you define fields in an anonymous class:
//: c08:Parcel8.java
// An anonymous inner class that performs
// initialization. À briefer version of Parcel4.java.
public
class
Parcel8 {
// Argument must be final to use inside
// anonymous inner class:
public
Destination dest
(
final
String dest) {
return
new
Destination
(
) {
private
String label =
dest;
public
String readLabel
(
) {
return
label; }
}
;
}
public
static
void
main
(
String[] args) {
Parcel8 p =
new
Parcel8
(
);
Destination d =
p.dest
(
"Tanzania"
);
}
}
///:~
If you're defining an anonymous inner class and want to use an object that's defined outside the anonymous inner class, the compiler requires that the argument reference be final, like the argument to dest( ).If you forget, you'll get a compile-time error message.
As long as you're simply assigning a field, the approach in this example is fine. But what if you need to perform some constructor-like activity? You can't have a named constructor in an anonymous class (since there's no name!), but with instance initialization, you can, in effect, create a constructor for an anonymous inner class, like this:
//: c08:AnonymousConstructor.java
// Creating a constructor for an anonymous inner class.
import
com.bruceeckel.simpletest.*;
abstract
class
Base {
public
Base
(
int
i) {
System.out.println
(
"Base constructor, i = "
+
i);
}
public
abstract
void
f
(
);
}
public
class
AnonymousConstructor {
private
static
Test monitor =
new
Test
(
);
public
static
Base getBase
(
int
i) {
return
new
Base
(
i) {
{
System.out.println
(
"Inside instance initializer"
);
}
public
void
f
(
) {
System.out.println
(
"In anonymous f()"
);
}
}
;
}
public
static
void
main
(
String[] args) {
Base base =
getBase
(
47
);
base.f
(
);
monitor.expect
(
new
String[] {
"Base constructor, i = 47"
,
"Inside instance initializer"
,
"In anonymous f()"
}
);
}
}
///:~
In this case, the variable i did not have to be final. While i is passed to the base constructor of the anonymous class, it is never directly used inside the anonymous class.
Here's the « parcel » theme with instance initialization. Note that the arguments to dest( ) must be final since they are used within the anonymous class:
//: c08:Parcel9.java
// Using "instance initialization" to perform
// construction on an anonymous inner class.
import
com.bruceeckel.simpletest.*;
public
class
Parcel9 {
private
static
Test monitor =
new
Test
(
);
public
Destination
dest
(
final
String dest, final
float
price) {
return
new
Destination
(
) {
private
int
cost;
// Instance initialization for each object:
{
cost =
Math.round
(
price);
if
(
cost >
100
)
System.out.println
(
"Over budget!"
);
}
private
String label =
dest;
public
String readLabel
(
) {
return
label; }
}
;
}
public
static
void
main
(
String[] args) {
Parcel9 p =
new
Parcel9
(
);
Destination d =
p.dest
(
"Tanzania"
, 101.395
F);
monitor.expect
(
new
String[] {
"Over budget!"
}
);
}
}
///:~
Inside the instance initializer you can see code that couldn't be executed as part of a field initializer (that is, the if statement). So in effect, an instance initializer is the constructor for an anonymous inner class. Of course, it's limited; you can't overload instance initializers, so you can have only one of these constructors.
VIII-B-4. Le lien vers la classe externe▲
Jusqu'à présent, les classes internes apparaissent juste comme un mécanisme de camouflage de nom et d'organisation du code, ce qui est intéressant, mais pas vraiment indispensable. Cependant, ici, les choses prennent une tournure inattendue. Quand on crée une classe interne, un objet de cette classe interne possède un lien vers l'objet extérieur qui l'a créé, il peut donc accéder aux membres de cet objet externe -sans aucune qualification spéciale. De plus, les classes internes ont accès à tous les éléments de la classe externe. (35) L'exemple suivant le démontre :
//: c08:Sequence.java
// Contient une séquence d'Objects.
import
com.bruceeckel.simpletest.*;
interface
Selector {
boolean
end
(
);
Object current
(
);
void
next
(
);
}
public
class
Sequence {
private
static
Test monitor =
new
Test
(
);
private
Object[] objects;
private
int
next =
0
;
public
Sequence
(
int
size) {
objects =
new
Object[size]; }
public
void
add
(
Object x) {
if
(
next <
objects.length)
objects[next++
] =
x;
}
private
class
SSelector implements
Selector {
private
int
i =
0
;
public
boolean
end
(
) {
return
i ==
objects.length; }
public
Object current
(
) {
return
objects[i]; }
public
void
next
(
) {
if
(
i <
objects.length) i++
; }
}
public
Selector getSelector
(
) {
return
new
SSelector
(
); }
public
static
void
main
(
String[] args) {
Sequence sequence =
new
Sequence
(
10
);
for
(
int
i =
0
; i <
10
; i++
)
sequence.add
(
Integer.toString
(
i));
Selector selector =
sequence.getSelector
(
);
while
(!
selector.end
(
)) {
System.out.println
(
selector.current
(
));
selector.next
(
);
}
monitor.expect
(
new
String[] {
"0"
,
"1"
,
"2"
,
"3"
,
"4"
,
"5"
,
"6"
,
"7"
,
"8"
,
"9"
}
);
}
}
///:~
La Sequence est simplement un tableau d'Object de taille fixe enveloppé dans une classe. On peut appeler add( ) pour ajouter un nouvel Object à la fin de la séquence (s'il reste de la place). Pour retrouver chacun des objets dans une Sequence, il existe une interface appelée Selector, qui permet de vérifier si l'on se trouve à la fin end( ), de récupérer l'Object current( ), et de se déplacer vers l'Object next( ) dans la Sequence. Comme Selector est une interface, beaucoup d'autres classes peuvent implémenter l'interface à leurs façons, et de nombreuses méthodes peuvent prendre interface comme un argument, afin de créer du code générique.
Ici, SSelector est une classe private qui fournit les fonctionnalités de Selector. Dans main( ), on peut voir la création d'une Sequence, suivie par l'addition d'un certain nombre d'objets String. Un Selector est alors produit grâce à un appel à getSelector( ), et celui-ci est alors utilisé pour se déplacer dans la Sequence et sélectionner chaque item.
Au premier abord, la création d'un SSelector ressemble à n'importe quelle autre classe interne. Mais regardez-la attentivement. Notez que chacune des méthodes -end( ), current( ), et next( )- utilisent objects, qui est une référence n'appartenant pas à SSelector, mais à la place un champ private de la classe externe. Cependant, la classe interne peut accéder aux méthodes et aux champs de la classe externe comme si elle les possédait. Ceci est très pratique, comme on peut le voir dans cet exemple.
Une classe interne a donc automatiquement accès aux membres de la classe externe. Comment cela est-il possible ? La classe interne doit garder une référence de l'objet de la classe externe responsable de sa création. Ainsi, quand on accède à un membre de la classe externe, cette référence (cachée) est utilisée pour sélectionner ce membre. Heureusement, le compilateur gère tous ces détails pour nous, mais vous pouvez maintenant comprendre qu'un objet d'une classe interne ne peut être créé qu'en association avec un objet de la classe externe. La construction d'un objet d'une classe interne requiert une référence sur l'objet de la classe externe, et le compilateur se plaindra s'il ne peut accéder à cette référence. La plupart du temps cela se fait sans aucune intervention de la part du programmeur.
VIII-B-5. Nested classes▲
If you don't need a connection between the inner class object and the outer class object, then you can make the inner class static. This is commonly called a nested class. (36) To understand the meaning of static when applied to inner classes, you must remember that the object of an ordinary inner class implicitly keeps a reference to the object of the enclosing class that created it. This is not true, however, when you say an inner class is static. À nested class means:
- You don't need an outer-class object in order to create an object of a nested class.
- You can't access a non-static outer-class object from an object of a nested class.
Nested classes are different from ordinary inner classes in another way, as well. Fields and methods in ordinary inner classes can only be at the outer level of a class, so ordinary inner classes cannot have static data, static fields, or nested classes. However, nested classes can have all of these:
//: c08:Parcel10.java
// Nested classes (static inner classes).
public
class
Parcel10 {
private
static
class
ParcelContents implements
Contents {
private
int
i =
11
;
public
int
value
(
) {
return
i; }
}
protected
static
class
ParcelDestination
implements
Destination {
private
String label;
private
ParcelDestination
(
String whereTo) {
label =
whereTo;
}
public
String readLabel
(
) {
return
label; }
// Nested classes can contain other static elements:
public
static
void
f
(
) {}
static
int
x =
10
;
static
class
AnotherLevel {
public
static
void
f
(
) {}
static
int
x =
10
;
}
}
public
static
Destination dest
(
String s) {
return
new
ParcelDestination
(
s);
}
public
static
Contents cont
(
) {
return
new
ParcelContents
(
);
}
public
static
void
main
(
String[] args) {
Contents c =
cont
(
);
Destination d =
dest
(
"Tanzania"
);
}
}
///:~
In main( ), no object of Parcel10 is necessary; instead, you use the normal syntax for selecting a static member to call the methods that return references to Contents and Destination.
As you will see shortly, in an ordinary (non-static) inner class, the link to the outer class object is achieved with a special this reference. A nested class does not have this special this reference, which makes it analogous to a static method.
Normally, you can't put any code inside an interface, but a nested class can be part of an interface. Since the class is static,it doesn't violate the rules for interfaces-the nested class is only placed inside the namespace of the interface:
//: c08:IInterface.java
// Nested classes inside interfaces.
public
interface
IInterface {
static
class
Inner {
int
i, j, k;
public
Inner
(
) {}
void
f
(
) {}
}
}
///:~
Earlier in this book I suggested putting a main( ) in every class to act as a test bed for that class. One drawback to this is the amount of extra compiled code you must carry around. If this is a problem, you can use a nested class to hold your test code:
//: c08:TestBed.java
// Putting test code in a nested class.
public
class
TestBed {
public
TestBed
(
) {}
public
void
f
(
) {
System.out.println
(
"f()"
); }
public
static
class
Tester {
public
static
void
main
(
String[] args) {
TestBed t =
new
TestBed
(
);
t.f
(
);
}
}
}
///:~
This generates a separate class called TestBed$Tester (to run the program, you say java TestBed$Tester). You can use this class for testing, but you don't need to include it in your shipping product; you can simply delete TestBed$Tester.class before packaging things up.
VIII-B-6. Se référer à l'objet de la classe externe▲
Si on a besoin de produire la référence à l'objet de la classe externe, il faut utiliser le nom de la classe externe suivi par un point et this. Par exemple, dans la classe Sequence.SSelector, chacune des méthodes peut accéder à la référence à la classe externe Sequence stockée en utilisant Sequence.this. Le type de la référence obtenue est automatiquement correct (il est connu et vérifié lors de la compilation, il n'y a donc aucune pénalité sur les performances lors de l'exécution).
Parfois on veut demander à un autre objet de créer un objet de l'une de ses classes internes. Pour faire cela, il faut fournir une référence à l'autre objet de la classe externe dans l'expression new, comme ceci :
//: c08:Parcel11.java
// Création d'instances de classes internes.
public
class
Parcel11 {
class
Contents {
private
int
i =
11
;
public
int
value
(
) {
return
i; }
}
class
Destination {
private
String label;
Destination
(
String whereTo) {
label =
whereTo; }
String readLabel
(
) {
return
label; }
}
public
static
void
main
(
String[] args) {
Parcel11 p =
new
Parcel11
(
);
// On doit utiliser une instance de la classe externe
// pour créer une instance de la classe interne :
Parcel11.Contents c =
p.new
Contents
(
);
Parcel11.Destination d =
p.new
Destination
(
"Tanzania"
);
}
}
///:~
Pour créer un objet de la classe interne directement, il ne faut pas utiliser la même syntaxe et se référer au nom de la classe externe Parcel11 comme on pourrait s'y attendre ; mais à la place il faut utiliser un objet de la classe externe pour créer un objet de la classe interne :
Parcel11.Contents c =
p.new
Contents
(
);
Il n'est donc pas possible de créer un objet de la classe interne sans disposer déjà d'un objet de la classe externe. Ceci parce qu'un objet de la classe interne est connecté silencieusement avec l'objet de la classe externe qui l'a créé. Cependant, si la classe est imbriquée (une classe interne static), alors elle n'a pas besoin d'une référence sur un objet de la classe externe.
VIII-B-7. Classe interne à plusieurs niveaux d'imbrication▲
(37) Une classe interne peut se situer à n'importe quel niveau d'imbrication - elle pourra toujours accéder de manière transparente à tous les membres de toutes les classes l'entourant, comme on peut le voir ici :
//: c08:MultiNestingAccess.java
// Les classes imbriquées peuvent accéder à tous les membres de tous
// les niveaux des classes dans lesquelles elles sont imbriquées.
class
MNA {
private
void
f
(
) {}
class
A {
private
void
g
(
) {}
public
class
B {
void
h
(
) {
g
(
);
f
(
);
}
}
}
}
public
class
MultiNestingAccess {
public
static
void
main
(
String[] args) {
MNA mna =
new
MNA
(
);
MNA.À mnaa =
mna.new
A
(
);
MNA.A.B mnaab =
mnaa.new
B
(
);
mnaab.h
(
);
}
}
///:~
On peut voir que dans MNA.A.B, les méthodes g( ) et f( ) sont appelées sans aucune qualification (malgré le fait qu'elles soient private). Cet exemple présente aussi la syntaxe nécessaire pour créer des objets de classes internes imbriquées quand on crée ces objets depuis une autre classe. La syntaxe « .new » fournit la portée correcte, donc on n'a pas besoin de qualifier le nom de la classe dans l'appel du constructeur.
VIII-B-8. Hériter d'une classe interne▲
Comme le constructeur d'une classe interne doit s'attacher à une référence à l'objet de la classe externe, les choses sont un peu plus compliquées lorsqu'on dérive une classe interne. Le problème est que la référence « secrète » sur l'objet de la classe externe doit être initialisée, et dans la classe dérivée il n'y a plus d'objet par défaut auquel se rattacher. La solution est donc d'utiliser une syntaxe qui rende cette association explicite :
//: c08:InheritInner.java
// Hériter d'une classe interne.
class
WithInner {
class
Inner {}
}
public
class
InheritInner extends
WithInner.Inner {
//! InheritInner() {} // Ne compilera pas.
InheritInner
(
WithInner wi) {
wi.super
(
);
}
public
static
void
main
(
String[] args) {
WithInner wi =
new
WithInner
(
);
InheritInner ii =
new
InheritInner
(
wi);
}
}
///:~
On peut voir que InheritInner étend juste la classe interne, et non la classe externe. Mais quand vient le temps de créer un constructeur, celui fourni par défaut ne convient pas, et on ne peut se contenter de passer une référence à un objet externe. De plus, on doit utiliser la syntaxe
enclosingClassReference.super
(
);
à l'intérieur du constructeur. Ceci fournit la référence nécessaire et le programme pourra alors être compilé.
VIII-B-9. Les classes internes peuvent-elles redéfinies ?▲
Que se passe-t-il quand on crée une classe interne, qu'on hérite la classe externe et qu'on redéfinit la classe interne ? Autrement dit, est-il possible de redéfinir entièrement la classe interne ? Ce concept semblerait particulièrement puissant, mais « redéfinir » une classe interne comme si c'était une autre méthode de la classe externe ne fait en réalité rien du tout :
//: c08:BigEgg.java
// Une classe interne ne peut être redéfinie comme une méthode.
import
com.bruceeckel.simpletest.*;
class
Egg {
private
Yolk y;
protected
class
Yolk {
public
Yolk
(
) {
System.out.println
(
"Egg.Yolk()"
); }
}
public
Egg
(
) {
System.out.println
(
"New Egg()"
);
y =
new
Yolk
(
);
}
}
public
class
BigEgg extends
Egg {
private
static
Test monitor =
new
Test
(
);
public
class
Yolk {
public
Yolk
(
) {
System.out.println
(
"BigEgg.Yolk()"
); }
}
public
static
void
main
(
String[] args) {
new
BigEgg
(
);
monitor.expect
(
new
String[] {
"New Egg()"
,
"Egg.Yolk()"
}
);
}
}
///:~
Le constructeur par défaut est généré automatiquement par le compilateur, et il appelle le constructeur par défaut de la classe de base. On pourrait penser que puisqu'on crée un BigEgg, a version « redéfinie » de Yolk serait utilisée, mais ce n'est pas le cas, comme on peut le voir d'après la sortie.
Cet exemple montre qu'il n'y a aucune magie spéciale associée aux classes internes quand on hérite d'une classe externe. Les deux classes internes sont des entités complètement séparées, chacune dans leur propre espace de noms. Cependant, il est toujours possible d'hériter explicitement la classe interne :
//: c08:BigEgg2.java
// Héritage propre d'une classe interne.
import
com.bruceeckel.simpletest.*;
class
Egg2 {
protected
class
Yolk {
public
Yolk
(
) {
System.out.println
(
"Egg2.Yolk()"
); }
public
void
f
(
) {
System.out.println
(
"Egg2.Yolk.f()"
);}
}
private
Yolk y =
new
Yolk
(
);
public
Egg2
(
) {
System.out.println
(
"New Egg2()"
); }
public
void
insertYolk
(
Yolk yy) {
y =
yy; }
public
void
g
(
) {
y.f
(
); }
}
public
class
BigEgg2 extends
Egg2 {
private
static
Test monitor =
new
Test
(
);
public
class
Yolk extends
Egg2.Yolk {
public
Yolk
(
) {
System.out.println
(
"BigEgg2.Yolk()"
); }
public
void
f
(
) {
System.out.println
(
"BigEgg2.Yolk.f()"
);
}
}
public
BigEgg2
(
) {
insertYolk
(
new
Yolk
(
)); }
public
static
void
main
(
String[] args) {
Egg2 e2 =
new
BigEgg2
(
);
e2.g
(
);
monitor.expect
(
new
String[] {
"Egg2.Yolk()"
,
"New Egg2()"
,
"Egg2.Yolk()"
,
"BigEgg2.Yolk()"
,
"BigEgg2.Yolk.f()"
}
);
}
}
///:~
Maintenant BigEgg2.Yolk étend explicitement Egg2.Yolk et redéfinit ses méthodes. La méthode insertYolk( ) permet à BigEgg2 de faire un transtypage ascendant sur un de ses propres objets Yolk dans la référence y de Egg2, donc quand g( ) appelle y.f( ), la version redéfinie de f( ) est utilisée. Le second appel à Egg2.Yolk( ) est l'appel du constructeur de la classe de base depuis le constructeur de BigEgg2.Yolk. On peut voir que la version redéfinie de f( ) est utilisée lorsque g( ) est appelée.
VIII-B-10. Les classes internes locales▲
Comme indiqué dans les paragraphes précédents, les classes internes peuvent être créées dans un bloc de code, typiquement elles sont créées dans le corps d'une méthode. Une classe interne locale ne peut pas avoir de spécificateur d'accès, car elle ne fait pas partie de la classe externe, mais elle a accès aux variables finales dans le bloc de code courant ainsi qu'à tous les membres de la classe englobante. Voici un exemple comparant la création d'une classe interne locale avec une classe interne anonyme :
//: c08:LocalInnerClass.java
// Garde une séquence d'Objects.
import
com.bruceeckel.simpletest.*;
interface
Counter {
int
next
(
);
}
public
class
LocalInnerClass {
private
static
Test monitor =
new
Test
(
);
private
int
count =
0
;
Counter getCounter
(
final
String name) {
// Une classe interne locale :
class
LocalCounter implements
Counter {
public
LocalCounter
(
) {
// Les classes interne locale peuvent avoir un constructeur
System.out.println
(
"LocalCounter()"
);
}
public
int
next
(
) {
System.out.print
(
name); // Accède à la variable finale
return
count++
;
}
}
return
new
LocalCounter
(
);
}
// La même chose, mais avec une classe interne anonyme :
Counter getCounter2
(
final
String name) {
return
new
Counter
(
) {
// Les classes interne anonymes ne peuvent pas
// avoir de constructeur, mais seulement
// une initialisation d'instance :
{
System.out.println
(
"Counter()"
);
}
public
int
next
(
) {
System.out.print
(
name); // Accède à la variable finale
return
count++
;
}
}
;
}
public
static
void
main
(
String[] args) {
LocalInnerClass lic =
new
LocalInnerClass
(
);
Counter
c1 =
lic.getCounter
(
"Local inner "
),
c2 =
lic.getCounter2
(
"Anonymous inner "
);
for
(
int
i =
0
; i <
5
; i++
)
System.out.println
(
c1.next
(
));
for
(
int
i =
0
; i <
5
; i++
)
System.out.println
(
c2.next
(
));
monitor.expect
(
new
String[] {
"LocalCounter()"
,
"Counter()"
,
"Local inner 0"
,
"Local inner 1"
,
"Local inner 2"
,
"Local inner 3"
,
"Local inner 4"
,
"Anonymous inner 5"
,
"Anonymous inner 6"
,
"Anonymous inner 7"
,
"Anonymous inner 8"
,
"Anonymous inner 9"
}
);
}
}
///:~
Counter retourne la valeur suivante dans la séquence. Cette classe est implémenté à la fois comme une classe interne anonyme et comme une classe interne locale, les deux implémentations ayant les mêmes comportements et les mêmes capacités. Comme le nom de la classe interne locale n'est pas accessible en dehors de la méthode, la seule justification de l'utilisation d'une classe interne locale au lieu d'une classe anonyme est le besoin d'un constructeur nommé et/ou d'un constructeur surchargé, car une classe interne anonyme ne peut utiliser que l'initialisation d'instance.
L'unique raison pour laquelle on peut préférer une classe interne à une classe interne anonyme est si l'on a besoin de créer plus d'une instance de cette classe.
VIII-B-11. Identifiants des classes internes▲
Puisque chaque classe produit un fichier .class qui contient toutes les informations concernant la création d'objets de ce type (ces informations produisent une « métaclasse » appelée l'objet Class), on peut deviner que les classes internes produisent aussi des fichiers .class qui contiennent des informations pour leurs objets Class. La nomenclature de ces fichiers / classes est stricte : le nom de la classe externe, suivie par un '$', suivi du nom de la classe interne. Par exemple, les fichiers .class créés par LocalInnerClass.java incluent :
Counter.class
LocalInnerClass$2.
class
LocalInnerClass$1
LocalCounter.class
LocalInnerClass.class
Si les classes internes sont anonymes, le compilateur génère simplement des nombres comme identifiants de classe interne. Si des classes internes sont imbriquées dans d'autres classes internes, leur nom est simplement ajouté après un '$' et le nom des identifiants des classes externes.
Bien que cette gestion interne des noms soit simple et directe, elle est aussi robuste et gère la plupart des situations. (38) Et comme cette notation est la notation standard pour Java, les fichiers générés sont automatiquement indépendants de la plateforme (Notez que le compilateur Java modifie les classes internes d'un tas d'autres manières afin de les faire fonctionner).
VIII-C. Raison d'être des classes internes▲
Jusqu'à présent, on a vu beaucoup de syntaxes et la sémantique décrivant la façon dont les classes internes fonctionnent, mais cela ne répond pas à la question du pourquoi de leur existence. Pourquoi Sun s'est-il donné tant de mal pour ajouter au langage cette fonctionnalité fondamentale ?
Typiquement, la classe interne hérite d'une classe ou implémente une interface, et le code de la classe interne manipule l'objet de la classe externe l'ayant créée. On peut donc dire qu'une classe interne est une sorte de fenêtre dans la classe externe.
Mais si j'ai juste besoin d'une référence sur une interface, pourquoi ne pas implémenter cette interface directement dans la classe externe ? La réponse à cette question allant au cœur des classes internes est : « Si c'est tout ce dont vous avez besoin, alors c'est ainsi qu'il faut procéder ». Alors qu'est-ce qui distingue une classe interne implémentant une interface d'une classe externe implémentant cette même interface ? C'est tout simplement qu'on ne dispose pas toujours des facilités fournies par les interfaces - quelquefois on est obligé de travailler avec des implémentations. Voici donc la raison principale d'utiliser des classes internes :
Chaque classe interne peut hériter indépendamment d'une implémentation. La classe interne n'est pas limitée par le fait que la classe externe hérite déjà d'une implémentation.
Sans cette capacité que fournissent les classes internes d'hériter - dans la pratique - de plus d'une classe concrète ou abstract, certains problèmes de conception ou de programmation seraient impossibles à résoudre. Les classes internes peuvent donc être considérées comme la suite de la solution au problème de l'héritage multiple. Les interfaces résolvent une partie du problème, mais les classes internes permettent réellement « l'héritage multiple d'implémentations ». Les classes internes vous permettent effectivement de dériver plus d'une non-interfaces.
Pour voir ceci plus en détail, imaginons une situation dans laquelle une classe doit implémenter d'une manière ou d'une autre deux interfaces. Du fait de la flexibilité des interfaces, on a deux choix : une unique classe ou une classe interne :
//: c08:MultiInterfaces.java
// Deux façons pour une classe d'implémenter des interfaces multiples.
interface
A {}
interface
B {}
class
X implements
A, B {}
class
Y implements
A {
B makeB
(
) {
// Classe interne anonyme :
return
new
B
(
) {}
;
}
}
public
class
MultiInterfaces {
static
void
takesA
(
A a) {}
static
void
takesB
(
B b) {}
public
static
void
main
(
String[] args) {
X x =
new
X
(
);
Y y =
new
Y
(
);
takesA
(
x);
takesA
(
y);
takesB
(
x);
takesB
(
y.makeB
(
));
}
}
///:~
Bien sûr, ceci suppose que la structure du code rend logique l'une ou l'autre de ces solutions. La nature du problème vous fournira généralement des indices pour choisir entre une classe unique ou une classe interne. Mais en l'absence de toute autre contrainte, l'approche choisie dans l'exemple précédent ne fait pas vraiment de différence du point de vue de l'implémentation. Les deux fonctionnent.
Cependant, si on a des classes abstract ou concrètes à la place des interfaces, vous êtes subitement obligés de recourir aux classes internes si la classe doit implémenter chacune :
//: c08:MultiImplementation.java
// Avec des classes concrètes ou abstract, les classes
// internes constituent le seul moyen de mettre en oeuvre
// « l'héritage multiple d'implémentations ».
package
c08;
class
D {}
abstract
class
E {}
class
Z extends
D {
E makeE
(
) {
return
new
E
(
) {}
; }
}
public
class
MultiImplementation {
static
void
takesD
(
D d) {}
static
void
takesE
(
E e) {}
public
static
void
main
(
String[] args) {
Z z =
new
Z
(
);
takesD
(
z);
takesE
(
z.makeE
(
));
}
}
///:~
Si vous n'aviez pas à résoudre le problème de « l'héritage multiple d'implémentations », vous pourriez tout à fait coder tout le reste sans avoir besoin des classes internes. Mais les classes internes fournissent ces fonctionnalités supplémentaires :
- Les classes internes peuvent avoir plusieurs instances, chacune avec ses propres informations d'état indépendantes des informations de l'objet de la classe externe.
- Dans une seule classe externe on peut avoir plusieurs classes internes, chacune implémentant la même interface ou dérivant la même classe d'une façon différente. Nous allons en voir un exemple bientôt.
- Le point de création d'un objet de la classe interne n'est pas lié à la création de l'objet de la classe externe.
- Il n'y a pas de relation « est-un », potentiellement ambiguë, avec la classe interne ; c'est une entité séparée.
Par exemple, si Sequence.java n'utilisait pas de classes internes, il aurait fallu dire « une Sequence est un Selector», et on n'aurait pu avoir qu'un seul Selector pour une Sequence particulière. On pourrait avoir facilement une seconde méthode, getRSelector( ), qui produirait un Selector parcourant la séquence dans l'ordre inverse. Cette flexibilité n'est possible qu'avec les classes internes.
VIII-C-1. Fermetures & callbacks▲
Une fermeture est un objet qui retient des informations de la portée dans laquelle il a été créé. À partir de cette définition, il est clair qu'une classe interne est une fermeture orientée objet, parce qu'elle ne contient pas seulement chaque élément d'information de l'objet de la classe externe (« la portée dans laquelle il a été créé »), mais elle contient aussi automatiquement une référence sur l'objet de la classe externe, avec la permission d'en manipuler tous les membres, y compris les private.
L'un des arguments les plus percutants mis en avant pour inclure certains mécanismes de pointeur dans Java était de permettre les callbacks. Avec un callback, on donne des informations à un objet lui permettant de revenir plus tard dans l'objet originel. Ceci est un concept particulièrement puissant, comme nous le verrons dans le livre. Cependant, si les callbacks étaient implémentés avec des pointeurs, le programmeur serait responsable de la gestion de ce pointeur et devrait faire attention afin de ne pas l'utiliser de manière incontrôlée. Mais comme on l'a déjà vu, Java n'aime pas ce genre de solutions reposant sur le programmeur, et les pointeurs ne furent pas inclus dans le langage.
Les classes internes fournissent une solution parfaite pour les fermetures, bien plus flexible et de loin plus sûre qu'un pointeur. Voici un exemple simple :
//: c08:Callbacks.java
// Utilisation des classes internes pour les callbacks
import
com.bruceeckel.simpletest.*;
interface
Incrementable {
void
increment
(
);
}
// Il est très facile d'implémenter juste l'interface :
class
Callee1 implements
Incrementable {
private
int
i =
0
;
public
void
increment
(
) {
i++
;
System.out.println
(
i);
}
}
class
MyIncrement {
void
increment
(
) {
System.out.println
(
"Other operation"
);
}
static
void
f
(
MyIncrement mi) {
mi.increment
(
); }
}
// Si la classe doit implémenter increment() d'une
// autre façon, il faut utiliser une classe interne :
class
Callee2 extends
MyIncrement {
private
int
i =
0
;
private
void
incr
(
) {
i++
;
System.out.println
(
i);
}
private
class
Closure implements
Incrementable {
public
void
increment
(
) {
incr
(
); }
}
Incrementable getCallbackReference
(
) {
return
new
Closure
(
);
}
}
class
Caller {
private
Incrementable callbackReference;
Caller
(
Incrementable cbh) {
callbackReference =
cbh; }
void
go
(
) {
callbackReference.increment
(
); }
}
public
class
Callbacks {
private
static
Test monitor =
new
Test
(
);
public
static
void
main
(
String[] args) {
Callee1 c1 =
new
Callee1
(
);
Callee2 c2 =
new
Callee2
(
);
MyIncrement.f
(
c2);
Caller caller1 =
new
Caller
(
c1);
Caller caller2 =
new
Caller
(
c2.getCallbackReference
(
));
caller1.go
(
);
caller1.go
(
);
caller2.go
(
);
caller2.go
(
);
monitor.expect
(
new
String[] {
"Other operation"
,
"1"
,
"2"
,
"1"
,
"2"
}
);
}
}
///:~
Cet exemple est un exemple supplémentaire montrant les différences entre l'implémentation d'une interface dans une classe externe ou une classe interne. Callee1 est sans conteste la solution la plus simple en terme de code. Callee2 hérite de MyIncrement qui dispose déjà d'une méthode increment() faisant quelque chose de complètement différent que ce qui est attendu par l'interface Incrementable. Quand MyIncrement est dérivée dans Callee2, increment() ne peut être redéfinie pour être utilisée par Incrementable, on est donc forcé d'utiliser une implémentation séparée avec une classe interne. Notez également que lorsqu'on crée une classe interne, on n'étend pas ni ne modifie l'interface de la classe externe.
Remarquez bien que tout dans Callee2 à l'exception de getCallbackReference() est private. L'interface Incrementable est essentielle pour permettre toute interaction avec le monde extérieur. Les interfaces permettent donc une séparation complète entre l'interface et l'implémentation.
La classe interne Closure implémente Incrementable uniquement pour fournir un point de retour dans Callee2 - mais un point de retour sûr. Quiconque récupère la référence sur Incrementable ne peut appeler qu'increment() et rien d'autre (contrairement à un pointeur, qui aurait permis de faire tout ce qu'on veut).
Caller prend une référence Incrementable dans son constructeur (bien qu'on puisse fournir cette référence - ce callback - n'importe quand), et s'en sert par la suite, parfois bien plus tard, pour « revenir » dans la classe Callee.
La valeur des callbacks réside dans leur flexibilité ; on peut décider dynamiquement quelles fonctions vont être appelées lors de l'exécution. Les avantages des callbacks apparaîtront dans le chapitre 14, où ils sont utilisés immodérément pour implémenter les interfaces graphiques utilisateurs (GUI).
VIII-C-2. Inner classes & control frameworks▲
Un exemple plus concret d'utilisation des classes internes est ce que j'appelle les structures de contrôle.
Une structure d'application est une classe ou un ensemble de classes conçues pour résoudre un type particulier de problème. Pour utiliser une structure d'application, il suffit typiquement de dériver d'une ou plusieurs de ces classes et de redéfinir certaines des méthodes. Le code écrit dans les méthodes redéfinies particularise la solution générale fournie par la structure d'application, afin de résoudre le problème considéré (c'est un exemple du design pattern Template Method ; voir Thinking in Patterns (avec Java) sur www.BruceEckel.com). Les structures de contrôle sont un type particulier des structures d'application dominées par la nécessité de répondre à des événements ; un système qui répond à des événements est appelé un système à programmation événementielle. L'un des problèmes les plus ardus en programmation est l'interface graphique utilisateur (GUI), qui est quasiment entièrement événementielle. Comme nous le verrons dans le Chapitre 14, la bibliothèque Java Swing est une structure de contrôle qui résoud élégamment le problème des interfaces utilisateurs en utilisant extensivement les classes internes.
Pour voir comment les classes internes permettent une mise en œuvre aisée des structures de contrôle, considérons le cas d'une structure de contrôle dont le rôle consiste à exécuter des événements dès lors que ces événements sont « prêts ». Bien que « prêt » puisse vouloir dire n'importe quoi, dans notre cas nous allons nous baser sur un temps d'horloge. Ce qui suit est une structure de contrôle qui ne contient aucune information spécifique sur ce qu'elle contrôle. Cette information est fournie au cours de l'héritage, lorsque le « modèle de la méthode » est implémenté.
Voici tout d'abord l'interface qui décrit tout événement. C'est une classe abstract plutôt qu'une interface parce que le comportement par défaut est de réaliser le contrôle sur le temps, donc une partie de l'implémentation peut y être incluse :
//: c08:controller:Event.java
// Les méthodes communes pour n'importe quel événement.
package
c08.controller;
public
abstract
class
Event {
private
long
eventTime;
protected
final
long
delayTime;
public
Event
(
long
delayTime) {
this
.delayTime =
delayTime;
start
(
);
}
public
void
start
(
) {
// Permet de redémarrer
eventTime =
System.currentTimeMillis
(
) +
delayTime;
}
public
boolean
ready
(
) {
return
System.currentTimeMillis
(
) >=
eventTime;
}
public
abstract
void
action
(
);
}
///:~
Le constructeur stocke l'heure (à partir du moment de la création de l'objet) à laquelle on veut que l'Event soit exécuté, afin de lancer par la suite des appels start( ), qui prennent le temps courant et ajoutent l'effet de latence inhérent au temps d'exécution de l'événement. Plutôt que d'être inclus dans le constructeur, start( ) est une méthode distincte. De cette façon, il vous permet de redémarrer le chronomètre après la fin de l'évènement, et ainsi, l'objet est réutilisable. Par exemple, si vous voulez un événement répétitif, vous pouvez simplement appeler start( ) dans votre méthode action( ).
ready( ) indique quand il est temps d'exécuter la méthode action( ). Évidemment, ready( ) peut-être redéfini dans une classe dérivée pour baser les Event sur autre chose que le temps.
Le fichier suivant contient la structure de contrôle proprement dite qui gère et déclenche les événements. Les objets Event sont contenus dans un conteneur d'objets de type ArrayList, dont vous en apprendrez plus dans le Chapter 11. Pour l'instant, tout ce que vous devez savoir est que add( ) permet de joindre un Object à la fin de l'ArrayList, size( ) retourne le nombre d'éléments dans l'ArrayList, get( ) permet de récupérer un élément de l'ArrayList à un index spécifique, et remove( ) supprime un élément de l'ArrayList, en lui donnant le numéro de l'élément à supprimer.
//: c08:controller:Controller.java
// Avec Event, la structure générique pour les systèmes de contrôle
package
c08.controller;
import
java.util.*;
public
class
Controller {
// Un objet de java.util pour stocker les objects Event :
private
List eventList =
new
ArrayList
(
);
public
void
addEvent
(
Event c) {
eventList.add
(
c); }
public
void
run
(
) {
while
(
eventList.size
(
) >
0
) {
for
(
int
i =
0
; i <
eventList.size
(
); i++
) {
Event e =
(
Event)eventList.get
(
i);
if
(
e.ready
(
)) {
System.out.println
(
e);
e.action
(
);
eventList.remove
(
i);
}
}
}
}
}
///:~
La méthode run( ) boucle à travers un eventList, à la recherche d'un objet Event qui exécute ready( ). Pour chacun, il trouve ready( ), il affiche les informations en utilisant la méthode toString( ) de l'objet, appelle la méthode action( ), et supprime l'Event de la liste.
Notez que jusqu'à présent dans la conception on ne sait rien sur ce que fait exactement un Event. Et c'est le point fondamental de la conception : comment elle « sépare les choses qui changent des choses qui ne bougent pas ». Ou, comme je l'appelle, le « vecteur de changement » est constitué des différentes actions des différents types d'objets Event, actions différentes réalisées en créant différentes sous-classes d'Event.
C'est là que les classes internes interviennent. Elles permettent deux choses :
- Réaliser l'implémentation complète d'une application de structure de contrôle dans une seule classe, encapsulant du même coup tout ce qui est unique dans cette implémentation. Les classes internes sont utilisées pour décrire les différents types d'action( ) nécessaires pour résoudre le problème.
- Empêcher que l'implémentation ne devienne trop lourde, puisqu'on est capable d'accéder facilement à chacun des membres de la classe externe. Sans cette facilité, le code deviendrait rapidement tellement confus qu'il faudrait chercher une autre solution.
Considérons une implémentation particulière de la structure de contrôle conçue pour contrôler les fonctions d'une serre. (39) Chaque action est complètement différente : contrôler les lumières, l'arrosage et la température, faire retentir des sonneries et relancer le système. Mais la structure de contrôle est conçue pour isoler facilement ce code différent. Les classes internes permettent d'avoir de multiples versions dérivées de la même classe de base Event, à l'intérieur d'une seule et même classe. Pour chaque type d'action on crée une nouvelle classe interne dérivée d'Event, et on écrit le code de contrôle dans la méthode action( ).
Typiquement, la classe GreenhouseControls hérite de Controller :
//: c08:GreenhouseControls.java
// Ceci est une application spécifique du
// système de contrôle, le tout dans une seule classe.
// Les classes internes permettent d'encapsuler des
// fonctionnalités différentes pour chaque type d'Event.
import
com.bruceeckel.simpletest.*;
import
c08.controller.*;
public
class
GreenhouseControls extends
Controller {
private
static
Test monitor =
new
Test
(
);
private
boolean
light =
false
;
public
class
LightOn extends
Event {
public
LightOn
(
long
delayTime) {
super
(
delayTime); }
public
void
action
(
) {
// Placer ici du code de contrôle hardware pour
// réellement allumer la lumière.
light =
true
;
}
public
String toString
(
) {
return
"Light is on"
; }
}
public
class
LightOff extends
Event {
public
LightOff
(
long
delayTime) {
super
(
delayTime); }
public
void
action
(
) {
// Placer ici du code de contrôle hardware pour
// réellement éteindre la lumière.
light =
false
;
}
public
String toString
(
) {
return
"Light is off"
; }
}
private
boolean
water =
false
;
public
class
WaterOn extends
Event {
public
WaterOn
(
long
delayTime) {
super
(
delayTime); }
public
void
action
(
) {
// Placer ici du code de contrôle hardware.
water =
true
;
}
public
String toString
(
) {
return
"Greenhouse water is on"
;
}
}
public
class
WaterOff extends
Event {
public
WaterOff
(
long
delayTime) {
super
(
delayTime); }
public
void
action
(
) {
// Placer ici du code de contrôle hardware.
water =
false
;
}
public
String toString
(
) {
return
"Greenhouse water is off"
;
}
}
private
String thermostat =
"Day"
;
public
class
ThermostatNight extends
Event {
public
ThermostatNight
(
long
delayTime) {
super
(
delayTime);
}
public
void
action
(
) {
// Placer ici du code de contrôle hardware.
thermostat =
"Night"
;
}
public
String toString
(
) {
return
"Thermostat on night setting"
;
}
}
public
class
ThermostatDay extends
Event {
public
ThermostatDay
(
long
delayTime) {
super
(
delayTime);
}
public
void
action
(
) {
// Placer ici du code de contrôle hardware.
thermostat =
"Day"
;
}
public
String toString
(
) {
return
"Thermostat on day setting"
;
}
}
// Un exemple d'une action() qui insère une nouvelle
// instance de son type dans la liste d'Event :
public
class
Bell extends
Event {
public
Bell
(
long
delayTime) {
super
(
delayTime); }
public
void
action
(
) {
addEvent
(
new
Bell
(
delayTime));
}
public
String toString
(
) {
return
"Bing!"
; }
}
public
class
Restart extends
Event {
private
Event[] eventList;
public
Restart
(
long
delayTime, Event[] eventList) {
super
(
delayTime);
this
.eventList =
eventList;
for
(
int
i =
0
; i <
eventList.length; i++
)
addEvent
(
eventList[i]);
}
public
void
action
(
) {
for
(
int
i =
0
; i <
eventList.length; i++
) {
eventList[i].start
(
); // Relance chaque Event
addEvent
(
eventList[i]);
}
start
(
); // Relance cet Event
addEvent
(
this
);
}
public
String toString
(
) {
return
"Restarting system"
;
}
}
public
class
Terminate extends
Event {
public
Terminate
(
long
delayTime) {
super
(
delayTime); }
public
void
action
(
) {
System.exit
(
0
); }
public
String toString
(
) {
return
"Terminating"
; }
}
}
///:~
Notez que light, water et thermostat appartiennent tous à la classe externe GreenhouseControls, et donc les classes internes peuvent accéder à ces champs sans qualification ou permission particulière. De plus, la plupart des méthodes action() effectuent un contrôle hardware.
La plupart des classes Event sont similaires, mais Bell et Restart sont spéciales. Bell sonne et elle ajoute un nouvel objet Bell à la liste des événements afin de sonner à nouveau plus tard. Notez comme les classes internes semblent bénéficier de l'héritage multiple : Bell et Restart possèdent toutes les méthodes d'Event, mais elles semblent disposer également de toutes les méthodes de la classe externe GreenhouseControls.
Restart est un tableau d'objet Event qui est ajouté au contrôleur. Puisque Restart( ) n'est qu'un objet Event comme un autre, on peut aussi ajouter un objet Restart depuis Restart.action() afin que le système se relance de lui-même régulièrement.
La classe suivante configure le système par la création d'un objet GreenhouseControls et ajoute différents types d'objets Event . C'est un exemple du design pattern Command :
//: c08:GreenhouseController.java
// Configure et exécute le système de serre.
// {Args: 5000}
import
c08.controller.*;
public
class
GreenhouseController {
public
static
void
main
(
String[] args) {
GreenhouseControls gc =
new
GreenhouseControls
(
);
// Au lieu de coder en dur, vous pouvez parser les
// informations de configuration depuis un fichier texte ici :
gc.addEvent
(
gc.new
Bell
(
900
));
Event[] eventList =
{
gc.new
ThermostatNight
(
0
),
gc.new
LightOn
(
200
),
gc.new
LightOff
(
400
),
gc.new
WaterOn
(
600
),
gc.new
WaterOff
(
800
),
gc.new
ThermostatDay
(
1400
)
}
;
gc.addEvent
(
gc.new
Restart
(
2000
, eventList));
if
(
args.length ==
1
)
gc.addEvent
(
gc.new
Terminate
(
Integer.parseInt
(
args[0
])));
gc.run
(
);
}
}
///:~
Cette classe initialise le système, ainsi elle ajoute tous les événements appropriés. Bien sûr, un moyen plus souple d'accomplir ceci est d'éviter le codage en dur des événements et à la place les lire depuis un fichier. (Un exercice dans le chapitre 12 vous demande de modifier cet exemple pour réaliser ceci.) Si vous fournissez un argument en ligne de commande, elle l'utilise pour terminer le programme après quelques millisecondes (ce qui est utilisé pour les essais).
Cet exemple vous amènera à apprécier à leur juste valeur l'utilisation des classes internes, en particulier lorsqu'elles sont utilisées dans un cadre de contrôle. Toutefois, au chapitre 14, vous verrez comment les classes internes sont utilisées avec élégance pour décrire les actions de l'interface graphique de l'utilisateur. Au moment où vous avez terminé ce chapitre, vous devriez être pleinement convaincu.
VIII-D. Summary▲
Les interfaces et les classes internes sont des concepts plus sophistiqués que ce que vous pourrez trouver dans beaucoup de langages de programmation orientés objet. Par exemple, rien de comparable n'existe en C++. Ensemble, elles résolvent le même problème que celui que le C++ tente de résoudre avec les fonctionnalités de l'héritage multiple. Cependant, l'héritage multiple en C++ se révèle relativement ardu à mettre en œuvre, tandis que les interfaces et les classes internes en Java sont, en comparaison, d'un abord nettement plus facile.
Bien que les fonctionnalités en elles-mêmes soient relativement simples, leur utilisation relève de la conception, de même que le polymorphisme. Avec le temps, vous reconnaîtrez plus facilement les situations dans lesquelles utiliser une interface, ou une classe interne, ou les deux. Mais à ce point du livre vous devriez au moins vous sentir à l'aise avec leur syntaxe et leur sémantique. Vous intègrerez ces techniques au fur et à mesure que vous les verrez utilisées.
VIII-E. Exercices▲
Les solutions des exercices sélectionnés sont disponibles dans le document électronique The Thinking in Java Annotated Solution Guide, disponible pour un faible coût sur www.BruceEckel.com.
- Prouver que les champs d'une interface sont implicitement static et final.
- Créer une interface contenant trois méthodes, dans son propre package. Implémenter cette interface dans un package différent.
- Prouver que toutes les méthodes d'une interface sont automatiquement public.
- Dans c07:Sandwich.java, créer une interface appelée FastFood (avec les méthodes appropriées) et changer Sandwich afin qu'il implémente FastFood.
- Créer trois interfaces, chacune avec deux méthodes. Créer une nouvelle interface héritant des trois, en ajoutant une nouvelle méthode. Créer une classe implémentant la nouvelle interface et héritant déjà d'une classe concrète. Écrire maintenant quatre méthodes, chacune d'entre elles prenant l'une des quatre interfaces en argument. Dans main( ), créer un objet de votre classe et le passer à chacune des méthodes.
- Modifier l'exercice 5 en créant une classe abstract et en la dérivant dans la dernière classe.
- Modifier Music5.java en ajoutant une interface Playable. Déplacer la déclaration de play( ) depuis Instrument vers Playable. Ajouter Playable à la classe dérivée en l'incluant dans la liste implements. Modifier tune( ) afin qu'il accepte un Playable au lieu d'un Instrument.
- Changer l'exercice 6 du Chapitre 7 afin que Rodent soit une interface.
- Dans Adventure.java, ajouter une interface appelée CanClimb, respectant la forme des autres interfaces.
- Écrire un programme qui importe et utilise Month.java.
- En suivant l'exemple donné dans Month.java, créer une énumération des jours de la semaine.
- Créer une interface dans son propre package contenant au moins une méthode. Créer une classe dans un package séparé. Ajouter une classe interne protected qui implémente l'interface. Dans un troisième package, dériver votre classe, et dans une méthode renvoyer un objet de la classe interne protected, en le transtypant en interface lors du retour.
- Créer une interface contenant au moins une méthode, et implémenter cette interface en définissant une classe interne à l'intérieur d'une méthode, qui renvoie une référence sur votre interface.
- Répéter l'exercice 13, mais définir la classe interne à l'intérieur d'un champ d'application d'une méthode.
- Répéter l'exercice 13 en utilisant une classe interne anonyme.
- Modifier HorrorShow.java pour implémenter DangerousMonster et Vampire en utilisant des classes anonymes.
- Créer une classe interne private qui implémente une interface public. Écrire une méthode qui renvoie une référence sur une instance de la classe interne private, transtypée (ascendant) en interface. Montrer que la classe interne est complètement cachée en essayant de faire un transtypage descendant.
- Créer une classe avec un constructeur autre que celui par défaut (un avec arguments) et sans constructeur par défaut (pas de constructeur sans argument). Créer une seconde classe ayant une méthode qui renvoie une référence à la première classe. Créer un objet à renvoyer en créant une classe interne anonyme dérivée de la première classe.
- Créer une classe avec un champ private et une méthode private. Créer une classe interne avec une méthode qui modifie le champ de la classe externe et appelle la méthode de la classe externe. Dans une seconde méthode de la classe externe, créer un objet de la classe interne et appeler sa méthode ; montrer alors l'effet sur l'objet de la classe externe.
- Répéter l'exercice 19 en utilisant une classe interne anonyme.
- Créer une classe contenant une classe imbriquée. Dans le main( ), créer une instance de la classe interne.
- Créer une interface contenant une classe imbriquée. Implémenter cette interface et créer une instance de la classe imbriquée.
- Créer une classe contenant une classe interne contenant elle-même une classe interne. Répéter ce schéma en utilisant des classes imbriquées. Noter les noms des fichiers .class produits par le compilateur.
- Créer une classe avec une classe interne. Dans une classe séparée, créer une instance de la classe interne.
- Créer une classe avec une classe interne disposant d'un constructeur autre que celui par défaut (un constructeur qui prend des arguments). Créer une seconde classe avec une classe interne qui hérite de la première classe interne.
- Corriger le problème dans WindError.java.
- Modifier Sequence.java en ajoutant une méthode getRSelector( ) qui produit une implémentation différente de l'interface Selector afin de parcourir la séquence en ordre inverse, de la fin vers le début.
- Créer une interface U contenant trois méthodes. Créer une classe A avec une méthode qui produit une référence sur un U en construisant une classe interne anonyme. Créer une seconde classe B qui contient un tableau de U. B doit avoir une méthode qui accepte et stocke une référence sur un U dans le tableau, une deuxième méthode qui positionne une référence (spécifiée par un argument de la méthode) dans le tableau à null, et une troisième méthode qui se déplace dans le tableau et appelle les méthodes de U. Dans main( ), créer un groupe d'objets A et un objet B. Remplir l'objet B avec les références U produites par les objets A. Utiliser B pour revenir dans tous les objets A. Supprimer certaines des références U de B.
- Dans GreenhouseControls.java, ajouter des classes internes Event qui allument et éteignent des ventilateurs. Configurer GreenhouseController.java pour utiliser ces nouveaux objets Event.
- Hériter de GreenhouseControls dans GreenhouseControls.java afin d'ajouter des classes internes Event qui allument et éteignent les générateurs de vapeur d'eau. Écrire une nouvelle version de GreenhouseController.java pour utiliser ces nouveaux objets Event.
- Montrer qu'une classe interne a accès aux éléments private de sa classe externe. Déterminer si l'inverse est vrai.