Les six rôles de l'interface

Les nouveaux arrivants au langage Java éprouvent souvent de la confusion. Leur perplexité est en grande partie due à la palette de fonctionnalités de langage exotiques de Java, telles que les génériques et les lambdas. Cependant, des fonctionnalités encore plus simples telles que les interfaces peuvent être déroutantes.

Récemment, j'ai rencontré une question sur la raison pour laquelle Java prend en charge les interfaces (via interfaceet implementsmots - clés). Quand j'ai commencé à apprendre Java dans les années 1990, on a souvent répondu à cette question en affirmant que les interfaces contournaient le manque de support de Java pour l' héritage d'implémentations multiples (classes enfants héritant de plusieurs classes parentes). Cependant, les interfaces servent bien plus qu'un kludge. Dans cet article, je présente les six rôles que jouent les interfaces dans le langage Java.

À propos de l'héritage multiple

Le terme héritage multiple est couramment utilisé pour désigner une classe enfant héritant de plusieurs classes parentes. En Java, le terme héritage d'implémentations multiples signifie la même chose. Java prend également en charge l' héritage d'interfaces multiples dans lequel une interface enfant peut hériter de plusieurs interfaces parent. Pour en savoir plus sur l'héritage multiple (y compris le célèbre problème du diamant), consultez l'entrée d'héritage multiple de Wikipedia.

Rôle 1: Déclaration des types d'annotations

Le interfacemot clé est surchargé pour être utilisé dans la déclaration de types d'annotations. Par exemple, le listing 1 présente un Stubtype d'annotation simple .

Liste 1. Stub.java

import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) public @interface Stub { int id(); // A semicolon terminates an element declaration. String dueDate(); String developer() default "unassigned"; }

Stubdécrit une catégorie d' annotations (instances de type annotation) qui désignent des types et des méthodes inachevés. Sa déclaration commence par un en-tête composé de @suivi du interfacemot - clé, suivi de son nom.

Ce type d'annotation déclare trois éléments , que vous pouvez considérer comme des en-têtes de méthode:

  • id() renvoie un identificateur basé sur un entier pour le stub
  • dueDate() identifie la date à laquelle le talon doit être rempli avec le code
  • developer() identifie le développeur chargé de remplir le stub

Un élément renvoie la valeur qui lui est attribuée par une annotation. Si l'élément n'est pas spécifié, sa valeur par défaut (après le defaultmot - clé dans la déclaration) est renvoyée.

Le listing 2 démontre Stubdans le contexte d'une ContactMgrclasse inachevée ; la classe et sa méthode solitaire ont été annotées avec des @Stubannotations.

Liste 2. ContactMgr.java

@Stub ( id = 1, dueDate = "12/31/2016" ) public class ContactMgr { @Stub ( id = 2, dueDate = "06/31/2016", developer = "Marty" ) public void addContact(String contactID) { } }

Une instance de type d'annotation commence par @, qui est suivi du nom du type d'annotation. Ici, la première @Stubannotation s'identifie comme numéro 1 avec une date d'échéance du 31 décembre 2016. Le développeur chargé de remplir le stub n'a pas encore été affecté. En revanche, la deuxième @Stubannotation s'identifie comme numéro 2 avec une date d'échéance du 31 juin 2016. Le développeur chargé de remplir le stub est identifié comme étant Marty.

Les annotations doivent être traitées pour être utiles. ( Stubest annoté @Retention(RetentionPolicy.RUNTIME)pour pouvoir être traité.) Le listing 3 présente une StubFinderapplication qui rapporte les @Stubannotations d' une classe .

Liste 3. StubFinder.java

import java.lang.reflect.Method; public class StubFinder { public static void main(String[] args) throws Exception { if (args.length != 1) { System.err.println("usage: java StubFinder classfile"); return; } Class clazz = Class.forName(args[0]); if (clazz.isAnnotationPresent(Stub.class)) { Stub stub = clazz.getAnnotation(Stub.class); System.out.println("Stub ID = " + stub.id()); System.out.println("Stub Date = " + stub.dueDate()); System.out.println("Stub Developer = " + stub.developer()); System.out.println(); } Method[] methods = clazz.getMethods(); for (int i = 0; i < methods.length; i++) if (methods[i].isAnnotationPresent(Stub.class)) { Stub stub = methods[i].getAnnotation(Stub.class); System.out.println("Stub ID = " + stub.id()); System.out.println("Stub Date = " + stub.dueDate()); System.out.println("Stub Developer = " + stub.developer()); System.out.println(); } } }

La main()méthode du listing 3 utilise l'API Reflection de Java pour récupérer toutes les @Stubannotations qui préfixent une déclaration de classe ainsi que ses déclarations de méthode.

Compilez les listes 1 à 3, comme suit:

javac *.java

Exécutez l'application résultante, comme suit:

java StubFinder ContactMgr

Vous devez observer la sortie suivante:

Stub ID = 1 Stub Date = 12/31/2016 Stub Developer = unassigned Stub ID = 2 Stub Date = 06/31/2016 Stub Developer = Marty

Vous pourriez dire que les types d'annotations et leurs annotations n'ont rien à voir avec les interfaces. Après tout, les déclarations de classe et le implementsmot clé ne sont pas présents. Cependant, je ne suis pas d'accord avec cette conclusion.

@interfaceest similaire à classen ce qu'il introduit un type. Ses éléments sont des méthodes implémentées (dans les coulisses) pour renvoyer des valeurs. Les éléments avec des defaultvaleurs renvoient des valeurs même lorsqu'ils ne sont pas présents dans les annotations, qui sont similaires aux objets. Les éléments non par défaut doivent toujours être présents dans une annotation et doivent être déclarés pour renvoyer une valeur. Par conséquent, c'est comme si une classe avait été déclarée et que la classe implémentait les méthodes d'une interface.

Rôle 2: Décrire les capacités indépendantes de la mise en œuvre

Différentes classes peuvent offrir une capacité commune. Par exemple, le java.nio.CharBuffer, javax.swing.text.Segment, java.lang.String, java.lang.StringBuffer, et les java.lang.StringBuilderclasses permettent d' accéder à des séquences lisibles de charvaleurs.

Lorsque les classes offrent une capacité commune, une interface vers cette capacité peut être extraite pour être réutilisée. Par exemple, une interface vers la charcapacité « séquence de valeurs lisibles » a été extraite dans l' java.lang.CharSequenceinterface. CharSequencefournit un accès uniforme et en lecture seule à de nombreux types de charséquences.

Supposons que vous a demandé d'écrire une petite application qui compte le nombre d'occurrences de chaque type de lettre minuscule dans CharBuffer, Stringet des StringBufferobjets. Après réflexion, vous pourriez trouver le Listing 4. (J'éviterais généralement les expressions culturellement biaisées telles que ch - 'a', mais je veux garder l'exemple simple.)

Listing 4. Freq.java(version 1)

import java.nio.CharBuffer; public class Freq { public static void main(String[] args) { if (args.length != 1) { System.err.println("usage: java Freq text"); return; } analyzeS(args[0]); analyzeSB(new StringBuffer(args[0])); analyzeCB(CharBuffer.wrap(args[0])); } static void analyzeCB(CharBuffer cb) { int counts[] = new int[26]; while (cb.hasRemaining()) { char ch = cb.get(); if (ch >= 'a' && ch <= 'z') counts[ch - 'a']++; } for (int i = 0; i < counts.length; i++) System.out.printf("Count of %c is %d%n", (i + 'a'), counts[i]); System.out.println(); } static void analyzeS(String s) { int counts[] = new int[26]; for (int i = 0; i = 'a' && ch <= 'z') counts[ch - 'a']++; } for (int i = 0; i < counts.length; i++) System.out.printf("Count of %c is %d%n", (i + 'a'), counts[i]); System.out.println(); } static void analyzeSB(StringBuffer sb) { int counts[] = new int[26]; for (int i = 0; i = 'a' && ch <= 'z') counts[ch - 'a']++; } for (int i = 0; i < counts.length; i++) System.out.printf("Count of %c is %d%n", (i + 'a'), counts[i]); System.out.println(); } }

Le listing 4 présente trois analyzeméthodes différentes pour enregistrer le nombre d'occurrences de lettres minuscules et produire cette statistique. Bien que les variantes Stringet StringBuffersoient pratiquement identiques (et que vous pourriez être tenté de créer une seule méthode pour les deux), la CharBuffervariante diffère de manière plus significative.

Le listing 4 révèle beaucoup de code dupliqué, ce qui conduit à un fichier de classe plus volumineux que nécessaire. Vous pouvez atteindre le même objectif statistique en travaillant avec l' CharSequenceinterface. Le listing 5 présente une version alternative de l'application de fréquence basée sur CharSequence.

Listing 5. Freq.java(version 2)

import java.nio.CharBuffer; public class Freq { public static void main(String[] args) { if (args.length != 1) { System.err.println("usage: java Freq text"); return; } analyze(args[0]); analyze(new StringBuffer(args[0])); analyze(CharBuffer.wrap(args[0])); } static void analyze(CharSequence cs) { int counts[] = new int[26]; for (int i = 0; i = 'a' && ch <= 'z') counts[ch - 'a']++; } for (int i = 0; i < counts.length; i++) System.out.printf("Count of %c is %d%n", (i + 'a'), counts[i]); System.out.println(); } }

Listing 5 reveals a much simpler application, which is due to codifying analyze() to receive a CharSequence argument. Because each of String, StringBuffer, and CharBuffer implements CharSequence, it's legal to pass instances of these types to analyze().

Another example

Expression CharBuffer.wrap(args[0]) is another example of passing a String object to a parameter of type CharSequence.

To sum up, the second role of an interface is to describe an implementation-independent capability. By coding to an interface (such as CharSequence) instead of to a class (such as String, StringBuffer, or CharBuffer), you avoid duplicate code and generate smaller classfiles. In this case, I achieved a reduction of more than 50%.

Role 3: Facilitating library evolution

Java 8 introduced us to the extremely useful lambda language feature and Streams API (with a focus on what computation should be performed rather than on how it should be performed). Lambdas and Streams make it much easier for developers to introduce parallelism into their applications. Unfortunately, the Java Collections Framework could not leverage these capabilities without needing an extensive rewrite.

To quickly enhance collections for use as stream sources and destinations, support for default methods (also known as extension methods), which are non-static methods whose headers are prefixed with the default keyword and which supply code bodies, was added to Java's interface feature. Default methods belong to interfaces; they're not implemented (but can be overridden) by classes that implement interfaces. Also, they can be invoked via object references.

Once default methods became part of the language, the following methods were added to the java.util.Collection interface, to provide a bridge between collections and streams:

  • default Stream parallelStream(): Return a (possibly) parallel java.util.stream.Stream object with this collection as its source.
  • default Stream stream(): Return a sequential Stream object with this collection as its source.

Suppose you've declared the following java.util.List variable and assignment expression:

List innerPlanets = Arrays.asList("Mercury", "Venus", "Earth", "Mars");

You would traditionally iterate over this collection, as follows:

for (String innerPlanet: innerPlanets) System.out.println(innerPlanet);

You can replace this external iteration, which focuses on how to perform a computation, with Streams-based internal iteration, which focuses on what computation to perform, as follows:

innerPlanets.stream().forEach(System.out::println); innerPlanets.parallelStream().forEach(System.out::println);

Here, innerPlanets.stream() and innerPlanets.parallelStream() return sequential and parallel streams to the previously created List source. Chained to the returned Stream references is forEach(System.out::println), which iterates over the stream's objects and invokes System.out.println() (identified by the System.out::println method reference) for each object to output its string representation to the standard output stream.

Les méthodes par défaut peuvent rendre le code plus lisible. Par exemple, la java.util.Collectionsclasse déclare une void sort(List list, Comparator c)méthode statique pour trier le contenu d'une liste soumis au comparateur spécifié. Java 8 a ajouté une default void sort(Comparator c)méthode à l' Listinterface afin que vous puissiez écrire le plus lisible myList.sort(comparator);au lieu de Collections.sort(myList, comparator);.

Le rôle de méthode par défaut offert par les interfaces a donné une nouvelle vie à Java Collections Framework. Vous pouvez envisager ce rôle pour vos propres bibliothèques basées sur une interface héritée.