Die sechs Rollen der Schnittstelle

Neulinge in der Java-Sprache sind häufig verwirrt. Ihre Verwirrung ist größtenteils auf Javas Palette exotischer Sprachmerkmale wie Generika und Lambdas zurückzuführen. Noch einfachere Funktionen wie Schnittstellen können jedoch rätselhaft sein.

Kürzlich stand ich vor der Frage, warum Java Schnittstellen (via interfaceund implementsSchlüsselwörter) unterstützt. Als ich in den 1990er Jahren anfing, Java zu lernen, wurde diese Frage oft mit der Feststellung beantwortet, dass Schnittstellen Javas mangelnde Unterstützung für die Vererbung mehrerer Implementierungen (untergeordnete Klassen, die von mehreren übergeordneten Klassen erben) umgehen. Schnittstellen sind jedoch weit mehr als nur ein Kludge. In diesem Beitrag stelle ich die sechs Rollen vor, die Schnittstellen in der Java-Sprache spielen.

Über Mehrfachvererbung

Der Begriff Mehrfachvererbung wird üblicherweise verwendet, um sich auf eine untergeordnete Klasse zu beziehen, die von mehreren übergeordneten Klassen erbt. In Java bedeutet der Begriff Vererbung mehrerer Implementierungen dasselbe. Java unterstützt auch die Vererbung mehrerer Schnittstellen, bei der eine untergeordnete Schnittstelle von mehreren übergeordneten Schnittstellen erben kann. Weitere Informationen zur Mehrfachvererbung (einschließlich des berühmten Diamantproblems) finden Sie im Wikipedia-Eintrag zur Mehrfachvererbung.

Rolle 1: Deklarieren von Annotationstypen

Das interfaceSchlüsselwort ist zur Verwendung beim Deklarieren von Anmerkungstypen überladen. In Listing 1 wird beispielsweise ein einfacher StubAnmerkungstyp dargestellt.

Listing 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"; }

Stubbeschreibt eine Kategorie von Annotationen (Instanzen von Annotationstypen ), die unfertige Typen und Methoden bezeichnen. Die Deklaration beginnt mit einem Header, der @aus dem interfaceSchlüsselwort gefolgt von seinem Namen besteht.

Dieser Annotationstyp deklariert drei Elemente , die Sie sich als Methodenheader vorstellen können:

  • id() Gibt einen ganzzahligen Bezeichner für den Stub zurück
  • dueDate() Gibt das Datum an, bis zu dem der Stub mit Code ausgefüllt werden muss
  • developer() Identifiziert den Entwickler, der für das Ausfüllen des Stubs verantwortlich ist

Ein Element gibt den Wert zurück, der ihm durch eine Anmerkung zugewiesen wurde. Wenn das Element nicht angegeben wird, wird sein Standardwert (nach dem defaultSchlüsselwort in der Deklaration) zurückgegeben.

Listing 2 zeigt Stubim Kontext einer unvollendeten ContactMgrKlasse; Die Klasse und ihre Einzelmethode wurden mit @StubAnmerkungen versehen.

Listing 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) { } }

Eine Annotationstypinstanz beginnt mit @, gefolgt vom Namen des Annotationstyps. Hier @Stubidentifiziert sich die erste Anmerkung als Nummer 1 mit einem Fälligkeitsdatum am 31. Dezember 2016. Der Entwickler, der für das Ausfüllen des Stubs verantwortlich ist, wurde noch nicht zugewiesen. Im Gegensatz dazu @Stubidentifiziert sich die zweite Anmerkung als Nummer 2 mit einem Fälligkeitsdatum am 31. Juni 2016. Der Entwickler, der für das Ausfüllen des Stubs verantwortlich ist, wird als Marty identifiziert.

Anmerkungen müssen verarbeitet werden, um von Nutzen zu sein. ( Stubist mit Anmerkungen versehen, @Retention(RetentionPolicy.RUNTIME)damit es verarbeitet werden kann.) Listing 3 zeigt eine StubFinderAnwendung, die die @StubAnmerkungen einer Klasse meldet .

Listing 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(); } } }

Die main()Methode von Listing 3 verwendet die Reflection-API von Java, um alle @StubAnmerkungen abzurufen , denen eine Klassendeklaration vorangestellt ist, sowie deren Methodendeklarationen.

Stellen Sie die Listen 1 bis 3 wie folgt zusammen:

javac *.java

Führen Sie die resultierende Anwendung wie folgt aus:

java StubFinder ContactMgr

Sie sollten die folgende Ausgabe beachten:

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

Sie könnten argumentieren, dass Annotationstypen und ihre Annotationen nichts mit Schnittstellen zu tun haben. Klassendeklarationen und das implementsSchlüsselwort sind schließlich nicht vorhanden. Ich würde dieser Schlussfolgerung jedoch nicht zustimmen.

@interfaceist insofern ähnlich class, als es einen Typ einführt. Seine Elemente sind Methoden, die (hinter den Kulissen) implementiert werden, um Werte zurückzugeben. Elemente mit defaultWerten geben Werte zurück, auch wenn sie nicht in Anmerkungen vorhanden sind, die Objekten ähnlich sind. Nicht standardmäßige Elemente müssen immer in einer Anmerkung vorhanden sein und müssen deklariert werden, um einen Wert zurückzugeben. Daher ist es so, als ob eine Klasse deklariert wurde und die Klasse die Methoden einer Schnittstelle implementiert.

Rolle 2: Beschreibung implementierungsunabhängiger Funktionen

Verschiedene Klassen können eine gemeinsame Fähigkeit bieten. Zum Beispiel die java.nio.CharBuffer, javax.swing.text.Segment, java.lang.String, java.lang.StringBuffer, und java.lang.StringBuilderKlassen ermöglichen den Zugriff auf lesbare Sequenzen von charWerten.

Wenn Klassen eine gemeinsame Funktion bieten, kann eine Schnittstelle zu dieser Funktion zur Wiederverwendung extrahiert werden. Beispielsweise wurde eine Schnittstelle zur Funktion "Lesbare Folge von charWerten" in die java.lang.CharSequenceSchnittstelle extrahiert . CharSequenceBietet einheitlichen, schreibgeschützten Zugriff auf viele verschiedene Arten von charSequenzen.

Angenommen , Sie wurden gebeten , eine kleine Anwendung , die zählt die Anzahl der Vorkommen von jeder Art von Kleinbuchstaben in zu schreiben CharBuffer, Stringund StringBufferObjekte. Nach einigem Überlegen könnten Sie Listing 4 erstellen. (Ich würde normalerweise kulturell voreingenommene Ausdrücke wie vermeiden ch - 'a', aber ich möchte das Beispiel einfach halten.)

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(); } }

Listing 4 enthält drei verschiedene analyzeMethoden zum Aufzeichnen der Anzahl von Kleinbuchstaben und zum Ausgeben dieser Statistik. Obwohl die Varianten Stringund StringBufferpraktisch identisch sind (und Sie möglicherweise versucht sind, eine einzige Methode für beide zu erstellen), unterscheidet sich die CharBufferVariante erheblich.

Listing 4 zeigt viel doppelten Code, was zu einer größeren Klassendatei führt als nötig. Sie können das gleiche statistische Ziel erreichen, indem Sie mit der CharSequenceSchnittstelle arbeiten. Listing 5 enthält eine alternative Version der Frequenzanwendung, auf der basiert 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 zeigt eine viel einfachere Anwendung, die auf die Kodierung zurückzuführen ist analyze(), um ein CharSequenceArgument zu erhalten . Da jeder String, StringBufferund CharBufferGeräte CharSequence, ist es rechtliche Instanzen dieser Typen zu übergeben analyze().

Ein anderes Beispiel

Der Ausdruck CharBuffer.wrap(args[0])ist ein weiteres Beispiel für die Übergabe eines StringObjekts an einen Parameter vom Typ CharSequence.

Zusammenfassend besteht die zweite Rolle einer Schnittstelle darin, eine implementierungsunabhängige Fähigkeit zu beschreiben. Durch die Codierung an eine Schnittstelle (wie CharSequence) statt zu einer Klasse (wie String, StringBuffer, oder CharBuffer), vermeiden Sie doppelten Code und kleinere Klassendateien erzeugen. In diesem Fall habe ich eine Reduzierung von mehr als 50% erreicht.

Rolle 3: Erleichterung der Bibliotheksentwicklung

Java 8 führte uns in die äußerst nützliche Lambda-Sprachfunktion und die Streams-API ein (mit dem Schwerpunkt darauf, welche Berechnungen durchgeführt werden sollten und nicht darauf, wie sie durchgeführt werden sollten). Lambdas und Streams erleichtern Entwicklern die Einführung von Parallelität in ihre Anwendungen erheblich. Leider konnte das Java Collections Framework diese Funktionen nicht nutzen, ohne dass ein umfangreiches Umschreiben erforderlich war.

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.

Standardmethoden können den Code lesbarer machen. Beispielsweise java.util.Collectionsdeklariert die Klasse eine void sort(List list, Comparator c)statische Methode zum Sortieren des Inhalts einer Liste, die dem angegebenen Komparator unterliegt. Java 8 hinzugefügt , um eine default void sort(Comparator c)Methode , um die ListSchnittstelle , so dass Sie das besser lesbar schreiben myList.sort(comparator);statt Collections.sort(myList, comparator);.

Die von Schnittstellen angebotene Standardmethodenrolle hat dem Java Collections Framework neues Leben eingehaucht. Sie können diese Rolle für Ihre eigenen älteren schnittstellenbasierten Bibliotheken in Betracht ziehen.