|
|||||||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||||||||||
|
Effective Java - Java 8 - Default Interface Methods
|
||||||||||||||||||||||||||||||
Wir haben bereits in einem unserer vorhergehenden Beiträge (/ JAV8-1 /) kurz erwähnt, dass es ab Java 8 möglich sein wird, 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 in diesem Artikel genauer ansehen. Was sich mit Java 8 bei Interfaces ändert
Bisher
war es in Java so, dass Methoden in Interfaces abstrakt sein mussten.
Das heißt, eine Methode in einem Interface legte allein ihre Signatur
und ihre allgemeine Semantik fest. Eine Implementierung konnten sie nicht
haben. Die Implementierung wurde erst von den (nicht-abstrakten) Klassen,
die von dem Interface abgeleitet waren, zur Verfügung gestellt.
Dies ändert sich mit
Java 8. 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. Dies 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.
Kommen wir hier erst noch
zu einer weiteren Änderung. Mit Java 8 wird es auch möglich sein,
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 hier: /PBK/.
Default-Methoden
Schauen wir uns die Default-Methoden
nun etwas genauer an. Überraschend ist, dass sie im Rahmen des JSR 335
(Lambda Expressions for the Java Programming Language) entwickelt wurde.
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 einem
vorhergehenden Artikel (/
JAVA8-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 /BOP/. 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, benutzer-spezifische Collections so zu implementieren, dass sie von den Interfaces der JDK Collection Frameworks ableiten (siehe /CCI/). Neue Methoden in den JDK Collection Interfaces hätten deshalb immer bedeutet, dass sich benutzer-spezifische Collections, die davon abgeleitet sind, erst einmal nicht mehr übersetzen lassen. Etwas verallgemeinert betrachtet ist das Problem, dass Java Interface Evolution nicht besonders gut unterstützt hat: Wenn in einem Interface neue Methoden dazu kommen, 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 benutzer-spezifischen Collections, die davon ableiten, anpassen, damit diese sich weiter übersetzen lassen.
Die Lösung dieses Problems
mit Java 8 ist nun, Interface Evolution mit Hilfe 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 Super-Interface 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 sollte 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 da steht (zum Beispiel durch Copy-Paste der Methode aus einer Klasse). Der andere Grund ist, dass das 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 Syntax-Entscheidung 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-final 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 Interfaces 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 vorhergehenden Artikel (/LAM/) 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! Genaugenommen 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 schauen wir uns weiter unten noch mal 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ückgreifen. Wie in dem hier diskutierten Beispiel war immer eine sinnvolle Implementierung der Default-Methoden auf Basis der bereits vorhandenen abstrakten Methoden möglich. 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 zusätzlich 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 Super-Klasse
und beliebig vielen Super-Interfaces (mit Default-Methoden) erbt.
Bemerkenswert ist die
Mehrfachvererbung von Funktionalität deshalb, weil dieses Feature beim
ursprünglichen Design von Java vor fast 20 Jahren bewusst ausgeschlossen
wurde. James Gosling hat das in einem damals veröffentlichten White
Paper zu Java 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.
(/JAO/)
Warum jetzt der Sinneswandel
mit Java 8? Der Grund ist wohl, dass Design immer etwas Zeitbehaftetes
hat, selbst wenn es sich um eine Programmiersprache handelt. Mitte der
neunziger Jahre war auf Grund der Erfahrungen mit C++ die allgemeine Meinung,
dass Mehrfachvererbung von Funktionalität ein Feature ist, das äußerst
komplex ist und deshalb schwer zu verstehen und einzusetzen ist. 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 mich 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:
Abbildung 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? Ewas 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. So dass sich
die Frage, wie häufig dieFelder 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) Methode des Interfaces 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. 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 mit der Version 8 von Java
neue Syntax und Regeln. Im Folgenden wollen wir uns einige dieser neuen
Situationen und Regeln ansehen.
Abbildung 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 Super-Interface I spielt keine Rolle.
Abbildung 3: Mehrfachvererbung, Situation
2
Und wie ist das, wenn eine Klasse C von zwei Interfaces I1 und I2 erbt, wobei die Interface ihrerseits wieder von einander abgeleitet sind (siehe Abbildung 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 Interfaces, das der Klasse am nächsten ist, vererbt sich. In unserm Fall ist das I2.foo() .
Abbildung 4: Mehrfachvererbung, Situation
3
Wie ist das, wenn die Interfaces I1 und I2
nicht von einander abgeleitet sind, sondern beide direkte Super-Interfaces
der Klasse C sind (siehe Abbildung 4). In diesem Fall ist die Vererbung
nicht automatisch geregelt. Hier kommt es zu einem Compilier-Fehler.
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 Super-Interface 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, sprengt den Rahmen des Artikels. Eine ausführliche Betrachtung weiterer Situationen findet sich hier / LAM /. 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 ist 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.
Schauen wir uns als Beispiel das funktionale Interface
Consumer<T>
an, das wir als Parameter-Typ 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 keiner Felder.
Bemerkenswert daran ist,
dass
Consumer
ein Interface ist, dass
neu in der Version 8 des JDK sein wird. Das heißt, es geht nicht um
ein schon existierendes, altes Interface. Mit anderen Worte, 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 Hierarchie-Graphen
hinauf: Statt wie bisher in Klassen (also relativ weit unten) wird Funktionalität,
soweit es möglich ist, bereits in den Interface 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. Interface 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.b ar() 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 Super-Interface 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 Methode 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. Statische Methoden in Interfaces nutzen
Wie und wofür das neue
Feature der statischen Interface-Methoden 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 Interfaces
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 Interface-Namens
gewählt. Ein Beispiel ist das Interface
Collection
und die begleitende Klasse
Collections
.
Collection / Collections
Mit dem neuen Sprachmittel
der statischen Interface-Methoden 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 Adapter-Methoden in der Klasse
Collections
wie
synchronizedList()
,
synchronizedSet()
,
etc. Sie verwenden nicht den Interface-Typ
Collection
,
sondern sie verwenden von
Collection
abgeleitete
Interface-Typen wie
Set
,
List
,
etc. Wenn man diese statischen Adapter-Methoden 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 Interface-Methoden
bereits verwendet. Als Beispiel sehen wir uns die Interfaces
java.util.stream.Stream
und
java.util.stream.Collector
an.
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
Stream
s
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
Stream
s
(
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
Strea
ms
.
Sie war ursprünglich einmal vorgesehen, ist aber seit der Beta Version
97 (b97) des JDK 8 entfallen, weil alle anfangs in der Klasse
Stream
s
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 statischen Factory-Methoden und die Klasse
Collector
s
enthält über 30 statische Factory-Methoden, um alle möglichen Kollektoren
zu erzeugen. Warum sind die mehr als 30 Factory-Methoden nicht auch im
Interface definiert?
Der Grund ist, dass das Interface Collector nur fünf abstrakte Instanz-Methoden enthält. Wenn die mehr als 30 statischen Factory-Methoden auch noch in dem Interface definiert worden wären, wäre es für Benutzer des Interfaces schwierig geworden, die Instanz-Methoden zu finden, auf die es ja bei einem Interface im Wesentlichen ankommt. (Im vorangegangenen Beispiel des Interfaces Stream sind die Zahlenverhältnisse im Übrigen genau umgekehrt: auf knapp 30 abstrakten Instanz-Methoden kommen etwa sechs statische Methoden.) Nun stellt sich die Frage: warum sind nicht alle statischen Factory-Methoden in der Klasse Collector s 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 30 Factory-Methoden. Sie sind deshalb etwas Besonderes, weil sie sehr eng und direkt mit dem Interface Collector zusammen hä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 Collector Impl könnte ganz einfach aussehen: sie hat fünf Felder für einen Supplier, einen Accumulator, usw. und implementiert die fünf abstrakten Interface-Methoden, 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 entgegen nehmen und daraus einen Collector wie soeben skizziert konstruieren. 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 30 anderen statischen Factory-Methoden (in der Klasse Collector s ) eher lösungsorientiert. Zum Beispiel die Methode toList in der Klasse Collector s
public static <T> Collector<T, ?, List<T>>
toList()
konstruiert einen
Collector
,
der die Elemente des Streams in einer
List
speichert. Diese Methoden hängen jetzt 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. (/HCL/) 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 Mehrfach-Vererbung
von Funktionalität möglich wird.
Im nächsten Beitrag, dessen Erscheinungstermin in etwa mit dem Release Termin von Java 8 zusammenfällt, werden wir uns in alle Neuerungen von Java 8 im Überblick ansehen. Literaturverweise
Die gesamte Serie über Java 8:
|
|||||||||||||||||||||||||||||||
© Copyright 1995-2018 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/72.Java8.DefaultMethods/72.Java8.DefaultMethods.html> last update: 26 Oct 2018 |