La notion d'interface est absolument centrale en Java, et massivement utilisée dans le design des API du JDK et de Java EE. Tout bon développeur Java doit absolument maîtriser ce point parfaitement.

La notion d'interface est utilisée pour représenter des propriétés transverses de classes. Là où une classe abstraite doit être étendue et spécialisée, une interface nous dit juste que telle classe possède telle propriété, indépendamment de ce qu'elle représente. Cette possibilité est massivement utilisée dans les API standard du JDK, et nous en verrons des exemples précis dans la suite.

Prenons l'exemple d'une hiérarchie de classes dont le but est de coder des moyens de locomotion. On peut imaginer une casse 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.


Codons à présent notre station service, notamment sa méthode 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.


Cette méthode fonctionne dans notre cas, mais elle est catastrophique, car elle ne fonctionne que dans notre cas ! Supposons qu'un autre développeur reprenne notre code, et sans connaître l'implémentation de 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.


On peut alors écrire notre classe StationService de la façon suivante


Ce qui nous donne un code qui ne dépend plus des classes de la hiérarchie de 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.

Techniquement, une interface s'écrit comme une classe, à la différence que l'on remplace le mot-clé class par interface.


Le mot-clé 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.


Une classe abstraite peut implémenter autant d'interfaces que l'on veut. Elle n'est pas tenue d'implémenter les méthodes des interfaces qu'elle utilise, pas même de façon abstraite.

Une interface peut être vue comme un niveau d'abstraction au-dessus des classes abstraites, à ceci près que l'on a plus de souplesse avec les interfaces que les classes, notamment en ce qui concerne l'héritage multiple.

La notion d'interface est profondément modifiée en Java 8. Jusqu'en Java 7 une interface ne peut posséder que des méthodes abstraites (donc des signatures de méthodes) et des constantes.

À partir de Java 8, on peut ajouter deux éléments supplémentaires dans une interfaces : des méthodes statiques et des méthodes par défaut.

Il n'existe qu'un seul type d'héritage multiple en Java : l'héritage multiple de type. Effectivement, un type peut étendre autant de type que l'on veut. On peut s'en rendre compte en examinant la classe String.


On se rend compte que le type 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.


Construisons une instance de 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.

  • Créer une implémentation concrète de la méthode a() dans la classe C. Un implémentation concrète a toujours la priorité sur une implémentation par défaut.

  • Décider qu'une des deux interfaces, par exemple 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é.

Deux règles très simples sont donc à retenir ici : en cas de conflit l'implémentation gagne contre l'implémentation par défaut, et l'implémentation par défaut plus spécifique gagne contre une moins spécifique.

Notons qu'une implémentation peut appeler explicitement une implémentation par défaut grâce à la syntaxe suivante.


Notons enfin qu'une méthode par défaut ne peut pas être une méthode de la classe 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.

La notion d'interface fonctionnelle est également une notion introduite en Java 8. Il n'y a pas d'interface fonctionnelle dans les projets jusqu'en Java 7.

La notions d'interface fonctionnelle est rétro-compatible. Cela signifie qu'une interface écrite en Java 7 (ou avant) peut devenir une interface fonctionnelle dans une application Java 8, sans modification, ni même être recompilée.

Cette notion est centrale en Java 8, puisque les interfaces fonctionnelles sont les modèles des expressions lambda.

Qu'est-ce qu'une interface fonctionnelle ? La définition est très simple : il s'agit d'une interface qui ne possède qu'une unique méthode abstraite .

Cette définition mérite quelques commentaires.

  • Une unique méthode abstraite signifie qu'une interface fonctionnelle peut compter autant de méthodes statiques ou de méthodes par défaut que l'on veut. Et c'est bien le cas dans la pratique !

  • Les méthodes statiques et par défaut n'existent qu'à partir de Java 8. En Java 7 et avant, une interface fonctionnelle ne comporte donc qu'une unique méthode.

  • Les méthodes de la classe 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.

Dans le cas de Java 8, on peut annoter une interface avec l'annotation @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.

Les interfaces sont massivement utilisées dans les API Java, que ce soit celles du JDK ou les API avancées de JEE (par exemple).

Notamment, spécifier une API signifie le plus souvent proposer un jeu d'interfaces qui exposent des fonctionnalités. À côté, on propose une implémentation de cette API, jeu de classes qui implémentent les interfaces de cette API.

C'est le cas dans le JDK de l'API JDBC, qui permet de se connecter aux bases de données. L'API définit tout d'abord l'interface 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 !

Java langage & API
Retour au blog Java le soir
Cours & Tutoriaux

Table des matières

Introduction : un peu d'histoire
1. Java : genèse d'un nouveau langage
Programmer en Java
1. Un premier exemple
1.1. Aperçu général, cycle de vie
1.2. Un premier programme
1.3. Programmes, applets, servlets, etc...
2. Une première classe
2.1. Écriture d'une classe
2.2. Instanciation de cette classe
3. Types de base, classes et objets
3.1. Types de base
3.2. Classes et objets
Classes Object et String
1. Introduction
2. La classe Object
2.1. La méthode toString()
2.2. La méthode clone()
2.3. La méthode equals()
2.4. La méthode hashCode()
2.5. La méthode finalize()
2.6. La méthode getClass()
3. La classe String
3.1. Introduction
3.2. Construction d'un objet de type String
3.3. Concaténation, StringBuffer et StringBuilder
3.4. Extraction d'une sous-chaîne de caractères
3.5. Comparaison de deux chaînes de caractères
3.6. Méthodes de comparaisons lexicographiques
3.7. Méthode de recherche de caractères
3.8. Méthode de modification de chaîne
3.9. Méthode de duplication
3.10. Support de l'unicode, internationalisation
Structure d'une classe
1. Introduction
2. Classes
2.1. Classes publiques
2.2. Classes internes
2.3. Classe membre
2.4. Classes locales
2.5. Classes anonymes
2.6. Le mot-clé this
3. Éléments statiques
3.1. Champ statique
3.2. Cas des constantes
3.3. Bloc statique
3.4. Classe membre statique
4. Membres d'une classe, visibilité
4.1. Bloc non statique
4.2. Accès à un membre, visibilité
4.3. Les champs
4.4. Signature d'une méthode
4.5. Les méthodes
4.6. Getters et Setters
5. Constructeur, instanciation
5.1. Chargement d'une classe
5.2. Constructeurs d'une classe
5.3. Instanciation d'un objet
5.4. Destruction d'objets
5.5. Le mot-clé final
6. Énumérations
6.1. Déclaration d'une énumération
6.2. Classe énumération
6.3. Méthode toString()
6.4. Méthode valueOf()
6.5. Méthode values()
6.6. Méthode ordinal()
6.7. Méthode compareTo()
6.8. Constructeurs privés
6.9. Classe utilitaire : EnumSet
Noms, opérateurs, tableaux
1. Introduction
2. Identificateurs, noms et expressions
2.1. Identificateurs et noms
2.2. Expressions
3. Opérateurs, ordre d'exécution
3.1. Ordre d'exécution
3.2. Les opérateurs
3.3. Les opérateurs ++ et --
3.4. Les opérateurs % et /
3.5. Les opérateurs <<, >> et >>>
3.6. L'opérateur instanceof
3.7. Les opérateurs &, | et ^
3.8. Les opérateurs && et ||
3.9. L'opérateur ? ... :
3.10. Les opérateurs d'affectation
4. Tableaux
4.1. Création d'un tableau
4.2. Initialisation d'un tableau
4.3. Utilisation d'un tableau comme un Object
4.4. Tableaux de tableaux
4.5. Copie de tableaux
5. Blocs, boucles et contrôles
5.1. Blocs
5.2. Mots-clés réservés
5.3. Tests : if et switch
5.4. Boucles : for, while, do ... while
5.5. Commandes continue et break
5.6. Commandes return et goto
Nombres, précision, calculs
1. Introduction
2. Calculs
2.1. Précision
2.2. Codage des nombres flottants
2.3. Le mot-clé strictfp
2.4. Conversion de types
3. Dépassements de capacité
3.1. Cas des entiers
3.2. Cas des flottants
3.3. Bibliothèques BigInteger et BigDecimal
4. Fonctions mathématiques
4.1. Fonctions usuelles
4.2. Générateurs aléatoires
5. Classes enveloppe
5.1. Associer les types de base à des objets
5.2. Auto-boxing
Héritage, abstraction, interfaces
1. Introduction
2. Abstraction et encapsulation
2.1. Abstraction
2.2. Encapsulation
3. Héritage
3.1. Définition de l'héritage
3.2. Conséquences pour les membres
3.3. Polymorphisme
3.4. Empêcher l'héritage
4. Classes abstraites
5. Interfaces
5.1. Introduction
5.2. Définition
5.3. Java 8 et les interfaces
5.4. Utilisation des interfaces
5.5. Définition de constantes dans les interfaces
5.6. Utilité des interfaces
Packages
1. Introduction
2. Notion de paquet
2.1. Déclaration d’appartenance à un paquet
2.2. Chargement d’une classe
2.3. Choix de nom
3. Archives, chemin de recherche, classpath
3.1. Archives
3.2. Variable CLASSPATH
3.3. Notion de classloader
3.4. Bilan sur les classes chargées
3.5. Visibilité
3.6. Conseils d'écriture, bibliothèque standard
Exceptions
1. Introduction
2. Erreurs et Exceptions
2.1. Classe Throwable, notion de stack trace
2.2. Classe Error
2.3. Classe Exception
3. Déclenchement d'une exception
3.1. Exceptions déclenchées par la JVM
3.2. Exceptions déclenchées par l'application
4. Capter une exception
4.1. Traiter une exception localement
4.2. Code de captage
5. Créer ses propres exceptions
Entrées / sorties
1. Introduction
2. Notion de fichier
2.1. Introduction
2.2. La classe File
2.3. Construction d'une instance de File
2.4. Méthodes exposées
3. Flux de sortie
3.1. Introduction, notion de flux
3.2. Écriture de caractères, classe Writer
3.3. Bufferisation, construction d'un flux sur un autre
3.4. Utilisation de PrintWriter
3.5. Écriture d'octets, OuputStream
3.6. Écriture de types primitifs : DataOutputStream
3.7. Écriture d'objets : ObjectOutputStream
4. Flux d'entrée
4.1. Introduction
4.2. Lecture de caractères, classe Reader
4.3. Bufferisation, lecture ligne par ligne
4.4. Lecture d'octets : InputStream
4.5. Lecture de types primitifs : DataInputStream
4.6. Lecture d'objets : ObjectInputStream
5. Lecture et écriture de flux croisés
5.1. Introduction
5.2. Lire des caractères sur un flux binaire : classe InputStreamReader
5.3. Écrire des caractères sur un flux binaire : classe OutputStreamWriter
5.4. Remarque sur les jeux de caractères
6. Serialization d'objets
6.1. Enjeu de la sérialization d'objets
6.2. Serialization d'un objet
6.3. Sérialization d'une grappe d'objets
6.4. Première surcharge : méthode writeObject() readObject()
6.5. Deuxième surcharge : utilisation d'un externalizer
6.6. Troisième surcharge : utilisation d'un objet proxy
7. Flux compressés
7.1. Introduction
7.2. Flux de type gzip
7.3. Flux de type zip