Effective Java Teil 3

Java Tutorial: Java 8 Methoden in Interfaces

Angelika Langer, Klaus Kreft
© S&S Media

Wie bereits in einem unserer vorhergehenden Beiträge [1] kurz erwähnt, ist es ab Java 8 möglich, Methoden in Interfaces zu implementieren. Warum das so ist, wie es genau geht und welche Auswirkungen dies in Zukunft auf das Programmieren in Java hat, wollen wir uns im Folgenden Java Tutorial genauer ansehen.

Bisher mussten Methoden in Interfaces in Java abstrakt sein. Das heißt, eine Methode in einem Interface legte allein ihre Signatur und ihre allgemeine Semantik fest. Eine Implementierung konnte sie nicht haben. Die Implementierung wurde erst von den (nicht abstrakten) Klassen, die von dem Interface abgeleitet waren, zur Verfügung gestellt. Wie dieses Java Tutorial zeigt, ändert sich das mit Java 8.

Java Tutorial: Methoden in Interfaces implementieren

Eine Default-Methode kann eine Methode mit Implementierung in einem Interface zur Verfügung stellen. Diese Implementierung wird dann an alle abgeleiteten Interfaces und Klassen vererbt, sofern diese sie nicht mit einer eigenen Implementierung überschreiben. Das ist eine tiefgreifende Änderung beim Programmieren in Java. Warum sie notwendig geworden ist und was sie im Detail bedeutet, sehen wir uns weiter unten genauer an.

Neu mit Java 8: statische Methoden in Interfaces zu implementieren

In beiden Fällen (Default-Methode und statische Methode) sind die Methoden automatisch public, wie es bei abstrakten Methoden auch bisher schon war. Es wurde diskutiert, andere Zugriff-Modifier wie z. B. private zuzulassen. Am Ende hat man dies aber aus Aufwandsgründen verworfen. Es ist aber nicht ausgeschlossen, dass Zugriff-Modifier wie z. B. private in einem zukünftigen Java-Release nach Java 8 erlaubt sein werden. Die Details der Diskussion zu diesem Thema finden sich unter [2].

Vertiefen können Sie Ihr Java-8-Know-How auf der kommenden JAX 2014 (12. – 16. Mai):

JDK 8 Day

Neue Sprachmittel in Java 8 Angelika Langer
Das Stream API – neue Abstraktionen im Collection-Framework von Java 8 Klaus Kreft
Neue Concurrency Utilities in Java 8 Angelika Langer
Preventing runtime errors at compile time using the Checker FrameworkWerner Dietl
Fragen und Antworten zu Java 8: Q&A mit Angelika Langer und Brian Goetz

 Ihre Fragen an Angelika Langer und Brian Goetz können Sie übrigens schon vorab hier einreichen!

www.jax.de

Copyright @ 2013-2014 by Angelika Langer & Klaus Kreft. All rights reserved

[ header = Seite 2: Default-Methoden ]

Default-Methoden

Überraschend ist, dass Default-Methoden im Rahmen des JSR 335 (Lambda Expressions for the Java Programming Language) entwickelt wurden. Auf den ersten Blick erschließt sich die Beziehung zwischen Lambda-Ausdrücken und Default-Methoden nicht. Deshalb wollen wir uns zunächst diesen Zusammenhang ansehen.

Wie wir auch in [1] bereits kurz erwähnt haben, werden die Collections im JDK 8 über zusätzliche Funktionalität für Bulk Operations bzw. interne Iterierung verfügen. Auch dies ist eine Entwicklung im Rahmen des JSR 335. Schauen wir uns dazu ein Beispiel an. Die Details finden sich in [3]. Mit dem folgenden Code kann man alle Element einer Collection (im Beispiel eine ArrayList<Integer>) ausgeben:

List<Integer> numbers = new ArrayList<>();
... populate list ...
numbers.forEach(number -> System.out.println(number));

Zentrales Element des Beispiels ist die mit Java 8 neu hinzugekommene forEach()-Methode. Ihr wird ein Lambda-Ausdruck übergeben, dessen Funktionalität dann im forEach() auf jedes Element der Collection angewandt wird. In unserem Beispiel besteht die Funktionalität des Lambda-Ausdrucks darin, das Element mit dem Aufruf von System.out.println() auszugeben.

Die funktionalen Erweiterungen der Collections sind ein spannendes Thema, dem wir uns in zukünftigen Artikeln noch ausführlich widmen werden. Heute interessiert uns aber eher die Frage: Wo und wie ist die neue forEach()-Methode definiert?

Aus gutem Grund sind bisher in den Interfaces des JDK-Collection-Frameworks keine neuen Methoden hinzugefügt worden, denn die JDK-Entwickler haben immer empfohlen, eigene, benutzerspezifische Collections so zu implementieren, dass sie von den Interfaces der JDK-Collection-Frameworks ableiten [4]. Neue Methoden in den JDK-Collection-Interfaces hätten deshalb immer bedeutet, dass sich benutzerspezifische Collections, die davon abgeleitet sind, erst einmal nicht mehr übersetzen lassen.

Etwas verallgemeinert betrachtet ist es das Problem, dass Java Interface Evolution nicht besonders gut unterstützt hat: Wenn in einem Interface neue Methoden hinzukommen, müssen alle ableitenden Klassen diese implementieren, damit sie sich weiterhin übersetzen lassen. Das Ganze ist insbesondere dann ein Problem, wenn die Entwickler, die das Interface erweitern wollen, gar nicht die Möglichkeit haben, die abgeleiteten Klassen auch anzupassen. Genau diese Situation haben wir in unserem Beispiel oben. Die JDK-Entwickler wollen das Collection-Interface um die Methode forEach() erweitern. Sie können aber nicht weltweit alle benutzerspezifischen Collections, die davon ableiten, anpassen, damit diese sich weiter übersetzen lassen.

Die Lösung dieses Problems mit Java 8 ist nun, Interface Evolution mithilfe von Default-Methoden zu unterstützen. Die neue Default-Methode im Interface bringt nämlich gleich ihre Implementierung mit. Damit sind abgeleitete Klassen nicht mehr gezwungen, die neue Methode zu implementieren. Sie können sie aber überschreiben, wenn sie eine passendere Implementierung anbieten wollen.

Schauen wir uns an, wie die konkret Lösung im Fall von forEach()aussieht. Die Default-Methode ist im generischen Interface Iterable<T> (einem Superinterface von Collection<T>) definiert. Dies ist ihre Implementierung:

public interface Iterable<T> {
  Iterator<T> iterator();

  default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);

    for(T t : this)
    action.accept(t);
  }
}

Die Deklaration der Default-Methode beginnt mit dem Schlüsselwort default. Es gab Diskussionen, ob das Schlüsselwort default überhaupt benutzt werden und wo es stehen sollte. Syntaktisch ist es für den Compiler nicht notwendig. Er kann auch allein am Methoden-Body erkennen, dass es sich um eine Default-Methode handelt. Am Ende hat man sich aus zwei Gründen für die Syntax entschieden, die oben im Beispiel zu sehen ist. Zum einen ist das Schlüsselwort default eine Absicherung dafür, dass der Methoden-Body nicht nur versehentlich dort steht (zum Beispiel durch Copy and Paste der Methode aus einer Klasse). Der andere Grund ist, dass man beim Blick auf den Code einfacher erkennen kann, ob es sich um eine Default-Methode handelt oder nicht. Dies ist übrigens auch der Grund, warum man sich dafür entschieden hat, das Schlüsselwort default ganz nach vorne zu stellen.

Nicht überraschend ist, dass die Default-Methode ihren Namen erst nach dieser Syntaxentscheidung bekommen hat. Denn erst zu diesem Zeitpunkt war klar, dass das Schlüsselwort default benutzt wird. Vorher hatte man die Namen virtual extension method bzw. defender method für das Feature vorgesehen.

Nun ist es ab Java 8 möglich, dass Methoden in Interfaces eine Implementierung haben. Es ist aber weiterhin nicht erlaubt, dass man in Interfaces nicht statische, nicht finale Felder definiert. Die Konsequenz daraus ist, dass man für die Implementierung von Default-Methoden keine Felder definieren kann. Das bedeutet, dass man für die Implementierung einer Default-Methode im Wesentlichen nur die Funktionalität der anderen (abstrakten) Methoden des Interface nutzen kann.

Schauen wir uns unter diesem Aspekt die Implementierung von forEach() in Iterable an. Sie besteht aus einer for-each-Schleife, in der auf jedes Element der Collection die Funktionalität des Parameters action angewandt wird. Dies geschieht durch den Aufruf von accept() aus dem funktionalen Interface Consumer. Wie das mit Lambda-Ausdrücken und ihren korrespondierenden Functional Interfaces geht, haben wir ausführlich im letzten Artikel [5] besprochen.

Eine Frage steht noch im Raum: Wo wird bei der Implementierung von forEach() die andere Methode (d. h. iterator()) aus dem Interface Iterable genutzt? Bei der for-each-Schleife! Genau genommen können wir diese Schleife nämlich nur hinschreiben, weil das Iterable eben diese Methode iterator() hat, mit deren Hilfe der Compiler die for-each-Schleife folgendermaßen umwandeln kann:

Iterator iter = iterator();
while(iter.hasNext())
  action.accept(iter.next());

Es ist übrigens Absicht, dass Default-Methoden keine neuen Felder in Interfaces definieren können und deshalb für die Implementierung von nicht trivialer Funktionalität auf die anderen Methoden im Interface zurückgreifen müssen. Den Grund dafür sehen wir uns weiter unten ausführlicher an. Triviale Funktionalität, wie das Werfen einer Runtime Exception oder return null, lässt sich natürlich immer in einer Default-Methode implementieren. Bei der Erweiterung der Collections für Java 8 brauchte man auf solche Tricks aber nicht zurückzugreifen. Wie in dem hier diskutierten Beispiel war immer eine sinnvolle Implementierung der Default-Methoden auf Basis der bereits vorhandenen abstrakten Methoden möglich.

Copyright @ 2013-2014 by Angelika Langer & Klaus Kreft. All rights reserved

[ header = Seite 3: Mehrfachvererbung von Funktionalität ]

Mehrfachvererbung von Funktionalität

Bemerkenswert ist, dass Default-Methoden dazu führen, dass Java mit der Version 8 Mehrfachvererbung von Funktionalität ermöglicht. Zu Mehrfachvererbung kommt es zum Beispiel, wenn eine Klasse mehrere Interfaces implementiert, die alle Default-Methoden enthalten. Die Funktionalität aller dieser Default-Methoden ist dann natürlich in der abgeleiteten Klasse verfügbar. Zusätzlich kann unsere Klasse natürlich auch von einer anderen Klasse abgeleitet sein und deren Funktionalität auch noch erben. Damit ist es dann in Java 8 möglich, dass eine Klasse die Funktionalität von einer Superklasse und beliebig vielen Superinterfaces (mit Default-Methoden) erbt.

Bemerkenswert ist die Mehrfachvererbung von Funktionalität deshalb, weil dieses Feature beim ursprünglichen Design von Java vor fast zwanzig Jahren bewusst ausgeschlossen wurde. James Gosling hat das in einem damals veröffentlichten White Paper zu Java  (PDF) ganz klar zum Ausdruck gebracht: „Java omits many rarely used, poorly understood, confusing features of C++ that in our experience bring more grief than benefit. This primarily consists of operator overloading (although it does have method overloading), multiple inheritance, and extensive automatic coercions“ [6].

Warum jetzt der Sinneswandel mit Java 8? Der Grund ist wohl, dass Design immer auch dem Zeitgeist folgt, selbst wenn es sich um eine Programmiersprache handelt. Mitte der neunziger Jahre herrschte aufgrund der Erfahrungen mit C++ die Meinung vor, dass Mehrfachvererbung von Funktionalität ein Feature ist, das äußerst komplex sei und deshalb schwer zu verstehen und einzusetzen. Also ist Java so entstanden, wie wir es heute kennen: mit Mehrfachvererbung von Interfaces, aber ohne Mehrfachvererbung von Funktionalität. Über all die Jahre hat sich dann gezeigt, dass dieser Ansatz, gerade auch mit Blick auf die Probleme bei der Interface Evolution, vielleicht ein wenig zu zurückhaltend war. Zumal es mittlerweile auch Programmiersprachen gibt, die Mehrfachvererbung von Funktionalität mit neuen Ansätzen ganz gut in den Griff bekommen haben. Beispiele dafür sind Mixins in Ruby und Traits in Scala.

Als dann beim Design der Erweiterung der Collection Interfaces für Bulk Operations die Einschränkungen bezüglich Interface Evolution wieder deutlich wurden, hat man sich entschlossen, auf Basis von Default-Methoden Mehrfachvererbung von Funktionalität in Java einzuführen. Damit hat Java eine relativ sichere Lösung bekommen, die es dem Programmierer einfach macht, das Feature zu nutzen.

Warum die Mehrfachvererbung von Funktionalität auf Basis von Default-Methoden relativ einfach und sicher zu benutzen ist, wollen wir uns jetzt genauer ansehen. Was sind eigentlich die Probleme bei der Mehrfachvererbung von Funktionalität, die dazu geführt haben, dass dieses Feature als so schwierig gilt? Bei Programmiersprachen, die Mehrfachvererbung von Klassen unterstützen (typisches Beispiel: C++), ist es möglich, die Vererbung unter vier Klassen so zu implementieren, dass sie einen rautenförmigen Vererbungsgraphen bilden (Abb. 1).

Abb. 1: Mehrfachvererbung – „Deadly Diamond of Death“

Von der Klasse A erben die beiden Klassen B und C. Die Klasse D ist via Mehrfachvererbung von diesen beiden Klassen abgeleitet. Das Problem ist nun: Wie häufig sind die Attribute von A in D enthalten? Einmal oder zweimal? Etwas weniger formal ausgedrückt lautet die Frage: Ist der A-Teil, den D über B und C erbt, der gleiche? (Dann sind die Attribute aus A nur einmal vererbt worden.) Oder erbt D über B und C je einen anderen A-Teil? (Dann sind die Attribute aus A zweimal vererbt worden.) Auf diese Frage gibt es keine allgemein richtige Antwort. Vielmehr ist die richtige Antwort jeweils von der konkreten Situation abhängig. Dies macht Mehrfachvererbung so schwierig. Nicht umsonst nennt sich dieser Vererbungsgraph auch „Deadly Diamond of Death“. C++ bietet die Möglichkeit, beide Varianten (mit virtueller bzw. nicht virtueller Ableitung) zu implementieren, aber die Entscheidung, welches die richtige Antwort ist, muss der Programmierer selbst fällen.

Bei Javas Mehrfachvererbung von Funktionalität auf Basis von Default-Methoden stellt sich die Frage glücklicherweise erst gar nicht:

  • Wenn D eine Klasse in Java ist, die von B und C Funktionalität erbt, dann muss mindestens einer der Typen B und C ein Interface sein, da es in Java auch weiterhin keine Mehrfachvererbung von Klassen gibt.
  • Wenn aber mindestens B oder C ein Interface ist, dann muss auch A ein Interface sein, denn ein Interface kann in Java nicht von einer Klasse abgeleitet sein.
  • Wenn nun A ein Interface ist, dann kann es zwar Funktionalität in Form von Default-Methoden enthalten, trotzdem hat A keine Felder, sodass sich die Frage, wie häufig die Felder von A in D enthalten sein sollen, gar nicht stellt.

Aus diesem Grund ist die Mehrfachvererbung von Funktionalität in Java relativ einfach zu handhaben (im Vergleich zu C++). Man sieht also: Es ist entscheidend, (nicht statische, nicht finale) Felder in Interfaces weiterhin zu verbieten, damit die Mehrfachvererbung in Java problemlos ist. Um einem möglichen Missverständnis vorzubeugen, sei darauf hingewiesen, dass dies nicht bedeutet, dass Default-Methoden stateless sein müssen. Da Default-Methoden in ihrer Implementierung andere (zumeist abstrakte) Methoden des Interface benutzen, können sie über diese Methoden sehr wohl in einer konkreten Klasse, in die sie vererbt werden, Attribute und damit den Zustand einer Instanz dieser Klasse verändern. Das geht dann indirekt über die aufgerufenen Methoden.

Copyright @ 2013-2014 by Angelika Langer & Klaus Kreft. All rights reserved

[ header = Seite 4: Neue Syntax und Regeln ]

Neue Syntax und Regeln

Wie wir bisher gesehen haben, ist die Mehrfachvererbung von Funktionalität, die wir mit der Version 8 von Java bekommen, relativ einfach zu benutzen. Trotzdem ergeben sich aus der Mehrfachvererbung neue Programmier- und Fehlersituationen beim Programmieren mit Java. Um mit diesen Situationen umgehen zu können, gibt es mit der Version 8 von Java neue Syntax und Regeln. Im Folgenden wollen wir uns einige dieser neuen Situationen und Regeln ansehen.

Abb. 2: Mehrfachvererbung, Situation 1

Was ist, wenn, wie in Abbildung 2 gezeigt, die Klasse C2 sowohl vom Interface I als auch von der Klasse C1 die gleiche Methode foo() erbt? Welche Implementierung erbt C2? Die von I oder die von C1? Oder ist das sowieso ein Fehler? Für diese Situation gibt es eine neue Regel: Die Methodenimplementierung aus der Super-Klasse C1 wird an C2 vererbt, und die aus dem Superinterface I spielt keine Rolle.

Abb. 3: Mehrfachvererbung, Situation 2

Und wie ist das, wenn eine Klasse C von zwei Interfaces I1 und I2 erbt, wobei die Interfaces ihrerseits wieder voneinander abgeleitet sind (Abb. 3)? Beide Interfaces habe eine Default-Implementierung für die gleiche Methode foo(). Welche vererbt sich an die Klasse C? Auch für diese Situation gibt es eine neue Regel: Die Default-Implementierung des Interface, das der Klasse am nächsten ist, vererbt sich. In unserm Fall ist das I2.foo().

Abb. 4: Mehrfachvererbung, Situation 3

Wie ist das, wenn die Interfaces I1 und I2 nicht voneinander abgeleitet sind, sondern beide direkte Superinterfaces der Klasse C (Abb. 4)? In diesem Fall ist die Vererbung nicht automatisch geregelt. Hier kommt es zu einem Kompilierfehler. Als Programmierer kann man nun explizit auswählen, welche Implementierung an die Klasse C vererbt werden soll. Dafür gibt es eine neue Syntax, um ein Superinterface zu benennen. Damit sieht die Auswahl von I2.foo() dann so aus:

class C implements I1, I2 {
  public void foo() { I2.super().foo(); }
}

Das waren die wichtigsten neuen Situationen, die sich aus der Mehrfachvererbung von Funktionalität basierend auf Default-Methoden ergeben. Natürlich lassen sich weitere Situationen finden. Sie alle hier zu diskutieren, würde den Rahmen des Artikels sprengen. Eine ausführliche Betrachtung weiterer Situationen findet sich unter [7].

Copyright @ 2013-2014 by Angelika Langer & Klaus Kreft. All rights reserved

[ header = Seite 5: Default-Methoden nutzen ]

Default-Methoden nutzen

Wir haben oben bereits diskutiert, wie Default-Methoden für die Interface Evolution genutzt werden können. Neue Default-Methoden können in bestehenden Interfaces definiert werden, ohne dass die direkt oder indirekt abgeleiteten Klassen angepasst werden müssen.

Auch wenn dies der wesentliche Grund für das neue Sprachmittel der Default-Methoden in Java 8 ist, gibt es weitere Gründe für die Benutzung von Default-Methoden. Dies hat sich bei den Erweiterungen des JDK für die Version 8 schon gezeigt. Sehen wir uns als Beispiel das funktionale Interface Consumer<T> an, das wir als Parametertyp der Methode forEach(Consumer<? super T> action) bereits oben kennengelernt haben. Consumer definiert die abstrakte Methode void accept(T t), die die Signatur für die Lambdas festlegt, die an forEach() übergeben werden. Daneben enthält Consumer noch die folgende Default-Methode:

default Consumer<T> andThen(Consumer<? super T> other) {
  Objects.requireNonNull(other);
  return (T t) -> { accept(t); other.accept(t); };
}

Wie der Name der Methode (andThen) schon sagt, verkettet die Methode zwei Consumer zu einem neuen Consumer. Zur Implementierung dieser Funktionalität bedarf es keine Felder.

Bemerkenswert daran ist, dass Consumer ein Interface ist, das neu in der Version 8 des JDK sein wird. Das heißt, es geht nicht um ein schon existierendes, altes Interface. Mit anderen Worten, Interface Evolution findet hier nicht statt. Vielmehr sehen wir, dass nicht abstrakte Methoden bereits in neu definierten Interfaces Verwendung finden. Die Voraussetzung ist, dass für ihre Implementierung keine eigenen Felder benötigt werden.

Damit verändert sich die Art, wie ab Java 8 Klassenhierarchien designt und implementiert werden. Die zu implementierende Funktionalität wandert den Hierarchiegraphen hinauf: Statt wie bisher in Klassen (also relativ weit unten) wird Funktionalität, soweit es möglich ist, bereits in den Interfaces implementiert (also relativ weit oben).

Statische Methoden in Interfaces

Wir haben es in der Einleitung bereits erwähnt: Neben den Default-Methoden gibt es noch eine zweite Änderung bei den Interfaces. Diese können ab Java 8 statische Methoden enthalten. Statische Methoden in Interfaces verhalten sich im Wesentlichen so wie statische Methoden in Klassen, mit einem Unterschied: Sie vererben sich nicht an abgeleitete Typen. Das heißt, wenn von einem Interface I mit einer statischen Methode foo() die Klasse C abgeleitet wird, so ist der Aufruf

C.foo();

ungültig. Auch der Aufruf von foo() auf einem Objekt von C ist nicht möglich. foo() kann nur als

I.foo();

aufgerufen werden. Der Grund für das andere Verhalten von statischen Methoden in Interfaces gegenüber statischen Methoden in Klassen liegt daran, dass man nicht durch das nachträgliche Einfügen von statischen Methoden in Interfaces den bisherigen Programmablauf ändern möchte. Wie hätte das sein können? Nehmen wir wieder an, wir haben ein Interface I, von dem eine Klasse C abgeleitet ist. In C gibt es eine Methode:

public static void bar(long l) { ... }

Irgendwo im Programm wird diese Methode genutzt:

C.bar(5);

Das funktioniert, obwohl hier bar() mit einem int-Parameter aufgerufen wird und C.bar() mit einem long-Parameter definiert ist. Der Compiler macht die notwendige Konvertierung, ohne dass wir im Programm explizit, etwa durch einen Cast, eingreifen müssen. Nehmen wir weiter an, dass zu einem späteren Zeitpunkt in I die Methode

public static void bar(int i) { ... }

definiert wird. Wenn sich die neue statische Methode im Interface I vererben würde, müsste der Compiler nun diese Methode statt der aus C aufrufen. Denn es existiert nun eine Methode mit exakt passendem Parametertyp im Superinterface I. Auf diese Art würden sich existierende Programmabläufe verändern, ohne dass dies bei der Änderung des Sourcecodes offensichtlich wird. Um solche Situationen zu vermeiden, hat man beschlossen, dass sich statische Methoden aus Interfaces nicht vererben. Man war sich sogar einig darüber, dass das Vererben von statischen Methoden bei Klassen eigentlich auch nicht korrekt sei. Aus Kompatibilitätsgründen lässt sich das heute aber natürlich nicht mehr ändern.

Copyright @ 2013-2014 by Angelika Langer & Klaus Kreft. All rights reserved

[ header = Seite 6: Statische Methoden in Interfaces nutzen ]

Statische Methoden in Interfaces nutzen

Wie und wofür das neue Feature der statischen Interfacemethoden verwendet werden kann oder soll, lässt sich derzeit noch nicht eindeutig beantworten. Klar ist zumindest, dass das neue Feature „statische Methoden in Interfaces“ insbesondere dann nützlich ist, wenn man zusätzliche Funktionalität im Kontext eines Interface implementieren will. Bisher hat man im JDK eine solche Situation dadurch gelöst, dass man neben dem Interface eine weitere Klasse mit einem private Konstruktor und ausschließlich statischen Methoden implementiert hat. Als Namen der Klasse hat man üblicherweise die Pluralform des Interfacenamens gewählt. Ein Beispiel ist das Interface Collection und die begleitende Klasse Collections.

Collection/Collections

Mit dem neuen Sprachmittel der statischen Interfacemethoden könnte man nun in Java 8 versuchen, alle Methoden aus der Klasse Collections als statische Methoden im Interface Collection zu implementieren und die Klasse Collections komplett wegfallen zu lassen. Würde man das tun? Nein, aus Kompatibilitätsgründen natürlich nicht. Aber selbst wenn Kompatibilität irrelevant wäre, würde eine solche Reorganisation keinen Sinn ergeben. Betrachten wir zum Beispiel die Adaptermethoden in der Klasse Collections wie synchronizedList(), synchronizedSet() etc. Sie verwenden nicht den Interfacetyp Collection, sondern sie verwenden von Collection abgeleitete Interfacetypen wie Set, List etc. Wenn man diese statischen Adaptermethoden in ein Interface verschieben wollte, dann müsste man sie logischerweise in die zugehörigen Interfaces Set, List etc. verschieben, statt in das Interface Collection. Im Falle der Collections würde man also auch weiterhin die statischen Methoden in der Klasse belassen, statt sie auf zahlreiche Interfaces zu verteilen.

Um einen Eindruck davon zu bekommen, wie das neue Sprachmittel in der Praxis benutzt wird, können wir uns seine Verwendung im JDK 8 ansehen. Dort werden statische Interfacemethoden bereits verwendet. Als Beispiel sehen wir uns die Interfaces java.util.stream.Stream und java.util.stream.Collector an.

Copyright @ 2013-2014 by Angelika Langer & Klaus Kreft. All rights reserved

[ header = Seite 7: Stream/Streams ]

Stream/Streams

Im Interface java.util.stream.Stream gibt es eine Reihe von statischen Methoden. Mit einer Ausnahme handelt es sich dabei um Factory-Methoden, die spezifische Streams erzeugen, zum Beispiel einen leeren Stream:

public static<T> Stream<T> empty()

Die Ausnahme unter den statischen Methoden im Interface java.util.stream.Stream ist:

public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)

Sie ist keine Factory-Methode im engeren Sinne, sondern sie hängt zwei Streams (a und b) hintereinander und erzeugt damit eine neue Stream-Instanz, die als Ergebnis zurückgegeben wird.

In Falle der Streams sind alle statischen Methoden im Interface Stream angesiedelt und es gibt keine begleitende Klasse Streams. Sie war ursprünglich einmal vorgesehen, ist aber seit der Betaversion 97 (b97) des JDK 8 entfallen, weil alle anfangs in der Klasse Streams definierten Methoden nach und nach ins Interface Stream verschoben worden sind.

Collector/Collectors

Anders sieht es im Fall von java.util.stream.Collector (Interface) und java.util.stream.Collectors (Klasse) aus. Das Interface Collector enthält zwei statische Factory-Methoden und die Klasse Collectors enthält über dreißig statische Factory-Methoden, um alle möglichen Kollektoren zu erzeugen. Warum sind die mehr als dreißig Factory-Methoden nicht auch im Interface definiert?

Der Grund ist, dass das Interface Collector nur fünf abstrakte Instanzmethoden enthält. Wenn die mehr als dreißig statischen Factory-Methoden auch noch in dem Interface definiert worden wären, wäre es für Benutzer des Interface schwierig geworden, die Instanzmethoden zu finden, auf die es ja bei einem Interface im Wesentlichen ankommt. (Im vorangegangenen Beispiel des Interface Stream sind die Zahlenverhältnisse im Übrigen genau umgekehrt: auf knapp dreißig abstrakten Instanzmethoden kommen etwa sechs statische Methoden.)

Nun stellt sich die Frage: Warum sind nicht alle statischen Factory-Methoden in der Klasse Collectors definiert? Was ist mit den beiden statischen Factory-Methoden, die im Interface Collector definiert sind? Der Grund ist, dass sie anders sind als die übrigen dreißig Factory-Methoden. Sie sind deshalb etwas Besonderes, weil sie sehr eng und direkt mit dem Interface Collector zusammenhängen.

Das Interface Collector hat fünf abstrakte Methoden, die jeweils Funktionalität zurückgeben für einen Supplier, einen Accumulator usw. Eine implementierende Klasse CollectorImpl könnte ganz einfach aussehen: Sie hat fünf Felder für einen Supplier, einen Accumulator usw. und implementiert die fünf abstrakten Interfacemethoden, indem sie die Werte der Felder zurückgibt. Die beiden speziellen statischen Factory-Methoden im Interface sind nun so angelegt, dass sie genau diese fünf Funktionsteile entgegennehmen und daraus einen Collector, wie soeben skizziert, konstruieren (Listing 1).

public interface Collector<T, A, R> {
  Supplier<A>          supplier();
  BiConsumer<A, T>     accumulator();
  BinaryOperator<A>    combiner();
  Function<A, R>       finisher();
  Set<Characteristics> characteristics();
  ...
  public static<T, A, R> Collector<T, A, R> of(Supplier<A>        supplier,
                                               BiConsumer<A, T>   accumulator,
                                               BinaryOperator<A>  combiner,
                                               Function<A, R>     finisher,
                                               Characteristics... characteristics) {
    return new CollectorImpl<>(supplier, accumulator, combiner, finisher, characteristics);
  }
}

Diese (und die hier nicht gezeigte zweite) statische Methode of hängen so eng mit dem Interface Collector zusammen, dass man sie im Interface angesiedelt hat.

Im Unterschied dazu sind die mehr als dreißig anderen statischen Factory-Methoden (in der Klasse Collectors) eher lösungsorientiert. Zum Beispiel die Methode toList in der Klasse Collectors

public static <T> Collector<T, ?, List<T>> toList()

konstruiert einen Collector, der die Elemente des Streams in einer List speichert. Diese Methoden hängen nicht so eng mit dem Interface zusammen; man hat sie deshalb (und wegen ihrer großen Anzahl) in die Klasse ausgelagert.

Wie man an den Beispielen sieht, wird nicht alles, was sich als statische Methode im Interface implementieren lassen könnte, auch tatsächlich so implementiert. Zumindest gilt das für die Java-8-Erweiterungen im JDK. Wie das neue Feature nun von der Java-Community aufgenommen und benutzt wird, bleibt abzuwarten. Oder wie Brian Goetz in einem Diskussionsbeitrag zu diesem Thema geschrieben hat: „So, while this gives API designers one more tool, there don’t seem to be obvious hard and fast rules about how to use this tool yet, and the simple-minded ‚all or nothing’ candidates are likely to give the wrong result.“ [8].

Zusammenfassung und Ausblick

Wir haben uns in diesem Beitrag die Java-8-Erweiterungen angesehen, die Interfaces betreffen: Default-Methoden und statische Methoden. Für Design und Programmierung in Java sind vor allem Default-Methoden eine wichtige Änderung, weil mit ihnen Mehrfachvererbung von Funktionalität möglich wird.

Copyright @ 2013-2014 by Angelika Langer & Klaus Kreft. All rights reserved

Geschrieben von
Angelika Langer
Angelika Langer
  Angelika Langer arbeitet selbstständig als Trainer mit einem eigenen Curriculum von Java- und C++-Kursen. Kontakt: http://www.AngelikaLanger.com.
Klaus Kreft
Klaus Kreft
  Klaus Kreft arbeitet selbstständig als Consultant und Trainer. Kontakt: http://www.AngelikaLanger.com.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: