La notion d'interface est absolument centrale en Java, et massivement utilisée dans le design des API du JDK et de JEE. 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. De plus, une interface ne peut pas posséder de méthode concrète, ni de paramètres. Voici un exemple d'interface.


Comme elle ne possède que des méthodes abstraites, 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.

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 et 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. Utilisation des interfaces
5.4. Définition de constantes dans les interfaces
5.5. 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