L'usage conjoint des classes et des interfaces permet de découpler la réalisation de la déclaration : aux interfaces la déclaration des méthodes publiques (de leur nom, des types de leurs paramètres, de leur type de retour), aux classes la définition des méthodes (leur corps, c'est-à-dire ce qu'elles font) et la construction des objets. Pour assurer ce découplage, on observera autant que possible les deux règles suivantes, qui constituent une discipline d'abstraction pour l'utilisateur, c'est-à-dire du côté du client :
L'idée est de choisir séparément les fonctionnalités souhaitées, ce qui détermine l'interface (par exemple, a-t-on besoin d'une méthode de comparaison ?), et la représentation des données et la définition des méthodes, ce qui détermine les classes de réalisation.
Ce découplage est particulièrement utile quand plusieurs réalisations d'une même interface coexistent ; nous en verrons des exemples à propos de la famille des collections. Voici l'exemple des piles, dont le type abstrait est formé des déclarations suivantes :
package structures; public interface Pile { boolean estVide(); void empiler(Object o); Object sommet(); Object dépiler(); }
Notons qu'il s'agit d'un type générique au sens où n'importe quel
objet (c'est-à-dire instance de la classe Object) peut être
empilé. Un programme utilisant des piles doit connaître ce type
abstrait, et peut ignorer la nature de la réalisation des piles (par
un tableau, une liste chaînée, etc.). Il doit même ignorer cette
réalisation, afin d'être indépendant de la réalisation choisie,
laquelle doit être modifiable sans remettre en cause les modules qui
l'utilisent. Il
suffit au programme de connaître le nom d'une classe
de réalisation. Cela ne signifie pas que le choix d'une classe
de réalisation est arbitraire ; des considérations d'efficacité, mais
aussi de disponibilité des classes de réalisation guident généralement
ce choix. Cependant, ce découplage met en uvre la liaison tardive,
qui est moins efficace qu'une liaison déterminée à la compilation.
Voici par exemple une méthode
toString, à ajouter à la
classe ArbreBinaire, afin d'obtenir une représentation
textuelle d'un arbre binaire parcouru en profondeur d'abord ; on utilise
la première réalisation (§ ), pour laquelle l'arbre vide est
représenté par la valeur null :
class ArbreBinaire { int étiquette; ArbreBinaire gauche; ArbreBinaire droit; // ... public String toString() { StringBuffer tampon = new StringBuffer(); Pile p = new PileParListe(); p.empiler(this); while (!p.estVide()) { ArbreBinaire t = (ArbreBinaire) p.dépiler(); if (t != null) { tampon.append(t.étiquette).append(" "); p.empiler(t.droit); p.empiler(t.gauche); } } return tampon.toString(); } }
Ici, PileParListe est une réalisation de l'interface Pile. Il est inutile d'en connaître le contenu pour définir la méthode toString. Si l'on préférait utiliser une autre réalisation de Pile, soit PileParTableau, il suffirait de remplacer l'appel au constructeur PileParListe par un appel au constructeur PileParTableau.
Du fait de la généricité de Pile, la valeur retournée par dépiler est de type Object ; avant de l'affecter à t, on lui applique un transtypage vers le type ArbreBinaire, de façon à pouvoir accéder aux membres de la classe ArbreBinaire à travers t.