Transport
, classe de base de laquelle toutes les autres classes vont hériter. Puis des classes
Avion
,
Voiture
,
Moto
,
Camion
, etc...
Nos moyens de locomotion ont besoin de faire le plein (on peut le regretter, mais la vie est ainsi faite). Pour cela ils se rendent dans une station service. Cette station service possède une méthode
faireLePlein(...)
, censée prendre un moyen de transport en paramètre. Ecrivons tout d'abord notre jeu de classes.
Exemple 89. Les classes "moyen de locomotion"
public class Transport { public void roule() ; } public class Voiture extends Transport { public void conduit() ; } public class Avion extends Transport { public void vole() ; } public class Moto extends Transport { public void seFaufile() ; } public class Velo extends Transport { public void pedale() ; }
faireLePlein(...)
. On pourrait penser que cette méthode
faireLePlein(...)
peut prendre un objet instance de
Transport
en paramètres, après tout cette classe est la super classe de toute notre hiérarchie. Malheureusement, dans la hiérarchie de
Transport
, il y a la classe
Velo
, et un vélo ne fréquente pas les stations service. Ou en tout cas pas pour y faire le plein. Une mauvaise approche serait de coder la méthode
faireLePlein(...)
de cette façon.
Exemple 90. Méthode
faireLePlein(...)
- 1
public class StationService { public void faireLePlein(Transport transport) { if (transport instanceof Velo) { // ne pas faire le plein } else { // faire le plein } } }
StationService
, écrive une autre extension de
Transport
,
Tricycle
. Si un
tricycle
se présente à la station service, notre système aura un problème...
Le principal problème de cette approche est qu'il faut modifier le code de cette méthode à chaque fois que l'on ajoute des classes dans la hiérarchie de
Transport
. De plus, si la classe
Transport
est exposée dans une API destinée à être réutilisée, si l'on n'y prend pas garde, elle peut être étendue par des classes dont on n'aura jamais connaissance. Une telle approche est donc à proscrire absolument !
C'est là que les interfaces entrent en jeu et nous aident à résoudre notre problème.
Écrivons une interface
Motorise
, et utilisons-la dans notre hiérarchie d'objets.
Exemple 91. Interface
Motorise
public interface Motorise { // notre interface public void faisLePlein() ; } public class Transport { // une instance de Transport ne sait pas toujours // faire le plein public void roule() {} } public class Voiture extends Transport implements Motorise { public void conduit() {} public void faisLePlein() {} } public class Avion extends Transport implements Motorise { public void vole() ; public void faisLePlein() {} } public class Moto extends Transport implements Motorise { public void seFaufile() ; public void faisLePlein() {} } public class Velo extends Transport { // ne sait pas faire le plein public void pedale() ; }
StationService
de la façon suivante
Exemple 92. Ecriture de
StationService
avec
Motorise
public class StationService { public void faireLePlein(Motorise motorise) { motorise.faisLePlein() ; } }
Transport
, et en particulier du fait que l'on peut en ajouter dedans. Ici, notre station service accepte toute instance d'une classe qui possède une méthode
faisLePlein()
, dont l'existence est spécifiée par l'interface
Motorise
. Que cette classe soit une extension de
Transport
ou non n'a pas d'importance, la station service ne le voit même pas. C'est en ce sens que l'on parle de propriétés transverses pour les interfaces.
C'est bien sûr ce type d'approche qu'il faut choisir lorsque l'on veut traiter des cas analogues à celui-la.
class
par
interface
.
Exemple 93. Une interface
public interface Motorise { // déclarée dans Motorise.java
public void faisLePlein() ;
}
abstract
peut être omis des signatures de méthodes (on le rencontre parfois, et il est parfaitement légal). Le mot-clé
public
aussi, car toutes les méthodes d'une interface le sont obligatoirement. Une méthode privée dans une interface ne serait pas vraiment utile...
Une interface peut en étendre une autre, et même plusieurs. Elle ne peut pas étendre de classe, abstraite ou concrète.
Une classe n'
étend
pas une interface, elle l'implémente. On utilise pour cela le mot-clé
implements
. Une classe peut implémenter autant d'interfaces que l'on veut. Une classe concrète doit obligatoirement fournir une implémentation pour toutes les méthodes déclarées par toutes les interfaces qu'elle implémente, soit elle-même, soit une de ses super classes.
Exemple 94. Implémentation d'une interface
public class Voiture implements Motorise {
public void faisLePlein() {
// corps de la classe
}
}
Exemple 95. Méthode statique dans une interface
public interface Function<T, R> { R apply(T t) ; static <T> Function<T, T> identity() { return t -> t ; } }
Exemple 96. Méthode par défaut dans une interface
public interface List<T> { default void sort(Comparator<t> comparator) { Collections.sort(this, comparator) ; } }
String
.
Exemple 97. Héritage multiple de type sur la classe
String
public class String implements Serializable, Comparable, CharSequence {
// corps de la classe
}
String
étend tout à la fois les types
Serializable
,
Comparable
et
CharSequence
.
Ce que ne possède pas Java 7 et que Java 8 introduit est l'héritage multiple d'
implémentation
. Effectivement, une implémentation dans une classe concrète peut étendre plusieurs méthodes par défaut à la fois. Considérons l'exemple suivant.
Exemple 98. Héritage multiple d'implémentation
public interface A { default void a() { // implémentation } } public interface B { default void a() { // implémentation } } public class C implements A, B { // !!! erreur de compilation !!! // corps de la classe }
C
et invoquons la méthode
a()
. La question se pose : quelle implémentation de la méthode
a()
va-t-elle être invoquée ? Celle de
A
? Celle de
B
?
La vérité est qu'il y a la une ambiguïté qui n'existait pas en Java 7. L'héritage multiple d'implémentation amène des cas ambiguës, chose que l'on ne connaissait pas en Java. La bonne nouvelle tout de même est que cette ambiguïté peut être détectée à la compilation. Et en fait, dans ce cas, une erreur sera levée par le compilateur, qu'il va nous falloir corriger.
Comment corriger cette erreur ? Tout simplement en levant l'ambiguïté. Nous avons deux solutions pour ce faire.
a()
dans la classe
C
. Un implémentation concrète a toujours la priorité sur une implémentation par défaut.
A
étend la seconde. Dans ce cas,
A
devient plus spécifique que
B
, et l'on dit que c'est l'implémentation la plus spécifique qui a la priorité.
Exemple 99. Héritage multiple d'implémentation
public interface A { default void a() { // implémentation } } public class C implements A { public void a() { return A.super.a() ; // appelle la méthode par défaut de A } }
Object
. Dans la mesure où une implémentation surcharge systématiquement une méthode par défaut, on a la garantie qu'une telle méthode ne serait jamais appelée ! Ce point est également détecté à la compilation.
Object
ne comptent pas dans ce total. Une interface fonctionnelle peut donc définir une première méthode abstraite
a()
et une seconde
toString()
. Pourquoi ? Tout simplement parce que tous les objets en Java étendent
Object
, donc on est sûr d'avoir une implémentation de ces méthodes dans les instances effectivement créées. En fait, si l'on déclare une méthode de la classe
Object
dans une interface, ce n'est pas pour garantir sa présence dans l'instance de cette interface, qui possédera de toute façon cette méthode, mais pour redéfinir la sémantique de cette méthode dans la Javadoc. C'est le cas par exemple dans la méthode
equals(Object o)
, redéfinie dans l'interface
Collection
.
@FunctionalInterface
. Si cette annotation est présente, alors le compilateur vérifiera que cette interface est bien fonctionnelle, et lèvera une erreur si tel n'est pas le cas.
Serializable
est utilisée comme un tag. Cette interface ne comporte aucune méthode, sa seule fonction est de marquer les classes qui pourront être "sérialisées", c'est-à-dire dont les instances pourront être écrites dans des fichiers ou transmises via un réseau.
L'interface
Comparable
est ajoutée à certaines classes, comme les classes enveloppe des types de base, ou la classe
String
. Elle indique que les instances de ces classes peuvent être comparées au sens d'un algorithme fourni par l'implémentation. Donc, on peut ranger des instances de
Comparable
dans des ensembles auto-ordonnés. Nous verrons un exemple précis lorsque nous présenterons l'API
Collection
.
#define
en Java. Une façon de faire est de placer ces constantes dans une interface. Toutes les classes qui ont besoin d’accéder à ces constantes n’ont plus qu’à déclarer cette interface dans leur clause
implements
. Voyons ceci sur l’exemple suivant.
Exemple 100. Constante définie dans une interface
public interface Constantes { // dans le fichier Constantes.java public static final double G = 9.81 ; } public class ChampGravitationnel // dans le fichier ChampGravitationnel.java implements Constantes { private double vitesse ; public double calculeVitesse(double temps) { return G*temps ; } }
Connection
, sans en fournir d'implémentation. Cette implémentation est fournie par les fabricants de bases de données, via les pilotes propres à chaque base.
L'interface
Connection
propose ensuite un jeu de méthodes qui retournent toutes des interfaces également. Par exemple, la méthode
createStatement()
retourne un objet
Statement
. Cet objet possède lui-même une méthode
execute(String)
qui permet de lancer des requêtes SQL sur la base de données.
Enfin, lorsque l'on gère des projets volumineux, et que la séparation en modules devient nécessaire, les interfaces se révèlent très utiles pour séparer "ce que fait" chaque module de "la façon dont il le fait". Chaque module expose des interfaces, qui, une fois fixées, ne bougeront plus, alors que les implémentations, non exposées, ont toute latitude pour être redéfinies en permanence.
Et pour terminer, il n'est pas possible de ne pas citer ce que l'utilisation des interfaces peut apporter aux tests que tout bon développeur ne manque pas d'écrire en parallèle du développement de son code !
String
StringBuffer
et
StringBuilder