Kapitel 1. Die Grundlagen

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Die größte Veränderung in Java 8 ist die Aufnahme von Konzepten aus der funktionalen Programmierung in die Sprache. Insbesondere wurden Lambda-Ausdrücke, Methodenreferenzen und Streams in die Sprache aufgenommen.

Wenn du die neuen funktionalen Funktionen noch nicht genutzt hast, wirst du wahrscheinlich überrascht sein, wie anders dein Code im Vergleich zu früheren Java-Versionen aussehen wird. Die Änderungen in Java 8 sind die größten Änderungen an der Sprache überhaupt. In vielerlei Hinsicht fühlt es sich so an, als würdest du eine völlig neue Sprache lernen.

Dann stellt sich die Frage: Warum tun wir das? Warum so drastische Änderungen an einer Sprache vornehmen, die bereits zwanzig Jahre alt ist und die Abwärtskompatibilität beibehalten will? Warum so drastische Änderungen an einer Sprache, die nach allem, was man hört, äußerst erfolgreich war? Warum zu einem funktionalen Paradigma wechseln, nachdem sie all die Jahre eine der erfolgreichsten objektorientierten Sprachen aller Zeiten war?

Die Antwort ist, dass sich die Welt der Softwareentwicklung verändert hat und Sprachen, die in Zukunft erfolgreich sein wollen, sich ebenfalls anpassen müssen. Mitte der 90er Jahre, als Java glänzte und neu war, war das Mooresche Gesetz1 noch voll in Kraft. Du musstest nur ein paar Jahre warten und dein Computer wurde doppelt so schnell.

Die heutige Hardware verlässt sich nicht mehr auf die zunehmende Chipdichte, um schneller zu werden. Stattdessen haben sogar die meisten Handys mehrere Kerne, was bedeutet, dass Software so geschrieben werden muss, dass sie in einer Multiprozessorumgebung ausgeführt werden kann. Die funktionale Programmierung mit ihrem Schwerpunkt auf "reinen" Funktionen (die bei gleichen Eingaben das gleiche Ergebnis liefern, ohne Seiteneffekte) und Unveränderlichkeit vereinfacht die Programmierung in parallelen Umgebungen. Wenn du keinen gemeinsamen, veränderbaren Zustand hast und dein Programm in Sammlungen einfacher Funktionen zerlegt werden kann, ist es einfacher, sein Verhalten zu verstehen und vorherzusagen.

Dies ist jedoch kein Buch über Haskell, Erlang, Frege oder eine der anderen funktionalen Programmiersprachen. In diesem Buch geht es um Java und die Änderungen, die an der Sprache vorgenommen wurden, um funktionale Konzepte in eine im Grunde immer noch objektorientierte Sprache einzufügen.

Java unterstützt jetzt Lambda-Ausdrücke, die im Wesentlichen Methoden sind, die wie Objekte erster Klasse behandelt werden. Die Sprache verfügt auch über Methodenreferenzen, mit denen du eine vorhandene Methode überall dort verwenden kannst, wo ein Lambda-Ausdruck erwartet wird. Um die Vorteile von Lambda-Ausdrücken und Methodenreferenzen nutzen zu können, hat die Sprache auch ein Stream-Modell eingeführt, das Elemente erzeugt und sie durch eine Pipeline von Transformationen und Filtern leitet, ohne die ursprüngliche Quelle zu verändern.

Die Rezepte in diesem Kapitel beschreiben die grundlegende Syntax für Lambda-Ausdrücke, Methodenreferenzen und funktionale Schnittstellen sowie die neue Unterstützung für statische und Standardmethoden in Schnittstellen. Streams werden in Kapitel 3 ausführlich behandelt.

1.1 Lambda-Ausdrücke

Problem

Du willst Lambda-Ausdrücke in deinem Code verwenden.

Lösung

Verwende eine der Varianten der Lambda-Ausdruckssyntax und weise das Ergebnis einer Referenz vom Typ funktionale Schnittstelle zu.

Diskussion

Eine funktionale Schnittstelle ist eine Schnittstelle mit einer einzigen abstrakten Methode (SAM). Eine Klasse implementiert eine beliebige Schnittstelle, indem sie Implementierungen für alle Methoden der Schnittstelle bereitstellt. Dies kann mit einer Top-Level-Klasse, einer inneren Klasse oder sogar einer anonymen inneren Klasse geschehen.

Nehmen wir zum Beispiel die Schnittstelle Runnable, die es in Java seit Version 1.0 gibt. Sie enthält eine einzige abstrakte Methode namens run, die keine Argumente annimmt und void zurückgibt. Der Konstruktor der Klasse Thread nimmt ein Runnable als Argument an. Die anonyme Implementierung der inneren Klasse wird in Beispiel 1-1 gezeigt.

Beispiel 1-1. Anonyme Implementierung einer inneren Klasse von Runnable
public class RunnableDemo {
    public static void main(String[] args) {
        new Thread(new Runnable() {  1
            @Override
            public void run() {
                System.out.println(
                    "inside runnable using an anonymous inner class");
            }
        }).start();
    }
}
1

Anonyme innere Klasse

Die Syntax der anonymen inneren Klasse besteht aus dem Wort new, gefolgt von dem Schnittstellennamen Runnable und Klammern, was bedeutet, dass du eine Klasse ohne expliziten Namen definierst, die diese Schnittstelle implementiert. Der Code in den geschweiften Klammern ({}) setzt dann die Methode run außer Kraft, die einfach einen String auf der Konsole ausgibt.

Der Code in Beispiel 1-2 zeigt das gleiche Beispiel unter Verwendung eines Lambda-Ausdrucks.

Beispiel 1-2. Verwendung eines Lambda-Ausdrucks in einem Thread-Konstruktor
new Thread(() -> System.out.println(
    "inside Thread constructor using lambda")).start();

Die Syntax verwendet einen Pfeil, um die Argumente (da es hier keine Argumente gibt, wird nur ein Paar leere Klammern verwendet) vom Textkörper zu trennen. In diesem Fall besteht der Körper aus einer einzigen Zeile, sodass keine Klammern erforderlich sind. Dies ist ein sogenanntes Ausdruckslambda. Der Wert, den der Ausdruck auswertet, wird automatisch zurückgegeben. Da println in diesem Fall void zurückgibt, ist die Rückgabe des Ausdrucks auch void, was dem Rückgabetyp der Methode run entspricht.

Ein Lambda-Ausdruck muss mit den Argumenttypen und dem Rückgabetyp in der Signatur der einzelnen abstrakten Methode in der Schnittstelle übereinstimmen. Dies wird als Kompatibilität mit der Signatur der Methode bezeichnet. Der Lambda-Ausdruck ist also die Implementierung der Schnittstellenmethode und kann auch einer Referenz dieses Schnittstellentyps zugewiesen werden.

Zur Veranschaulichung zeigt Beispiel 1-3 das Lambda, das einer Variablen zugewiesen ist.

Beispiel 1-3. Zuweisung eines Lambda-Ausdrucks an eine Variable
Runnable r = () -> System.out.println(
    "lambda expression implementing the run method");
new Thread(r).start();
Hinweis

In der Java-Bibliothek gibt es keine Klasse namens Lambda. Lambda-Ausdrücke können nur funktionalen Schnittstellenreferenzen zugewiesen werden.

Ein Lambda der funktionalen Schnittstelle zuzuordnen ist dasselbe wie zu sagen, dass das Lambda die Implementierung der einzelnen abstrakten Methode innerhalb der Schnittstelle ist. Du kannst dir das Lambda als den Körper einer anonymen inneren Klasse vorstellen, die die Schnittstelle implementiert. Deshalb muss das Lambda mit der abstrakten Methode kompatibel sein; seine Argumenttypen und sein Rückgabetyp müssen mit der Signatur dieser Methode übereinstimmen. Der Name der Methode, die implementiert wird, ist jedoch nicht wichtig. Er wird in der Syntax des Lambda-Ausdrucks nirgends erwähnt.

Dieses Beispiel war besonders einfach, weil die Methode run keine Argumente benötigt und void zurückgibt. Betrachte stattdessen die funktionale Schnittstelle java.io.Filename​Filter, die ebenfalls seit Version 1.0 Teil der Java-Standardbibliothek ist. Instanzen von Filename​Filter werden als Argumente für die Methode File.list verwendet, um die zurückgegebenen Dateien auf diejenigen zu beschränken, die der Methode genügen.

Aus den Javadocs geht hervor, dass die Klasse FilenameFilter eine einzige abstrakte Methode accept mit der folgenden Signatur enthält:

boolean accept(File dir, String name)

Das Argument File ist das Verzeichnis, in dem die Datei gefunden wird, und String ist der Name der Datei.

Der Code in Beispiel 1-4 implementiert FilenameFilter mit einer anonymen inneren Klasse, um nur Java-Quelldateien zurückzugeben.

Beispiel 1-4. Eine anonyme innere Klassenimplementierung von FilenameFilter
File directory = new File("./src/main/java");

String[] names = directory.list(new FilenameFilter() {  1
    @Override
    public boolean accept(File dir, String name) {
        return name.endsWith(".java");
    }
});
System.out.println(Arrays.asList(names));
1

Anonyme innere Klasse

In diesem Fall gibt die Methode accept true zurück, wenn der Dateiname mit .java endet, und false, wenn nicht.

Die Version des Lambda-Ausdrucks wird in Beispiel 1-5 gezeigt.

Beispiel 1-5. Lambda-Ausdruck, der FilenameFilter implementiert
File directory = new File("./src/main/java");

String[] names = directory.list((dir, name) -> name.endsWith(".java")); 1
    System.out.println(Arrays.asList(names));
}
1

Lambda-Ausdruck

Der resultierende Code ist viel einfacher. Diesmal stehen die Argumente in Klammern, aber es sind keine Typen deklariert. Zur Kompilierzeit weiß der Compiler, dass die Methode list ein Argument des Typs FilenameFilter annimmt, und kennt daher die Signatur ihrer einzigen abstrakten Methode (accept). Er weiß daher, dass die Argumente von accept vom Typ File und String sind, so dass die kompatiblen Argumente des Lambda-Ausdrucks diesen Typen entsprechen müssen. Der Rückgabetyp von accept ist ein boolescher Wert, also muss der Ausdruck rechts vom Pfeil ebenfalls einen booleschen Wert zurückgeben.

Wenn du die Datentypen im Code angeben möchtest, kannst du das tun, wie in Beispiel 1-6.

Beispiel 1-6. Lambda-Ausdruck mit expliziten Datentypen
File directory = new File("./src/main/java");

String[] names = directory.list((File dir, String name) -> 1
    name.endsWith(".java"));
1

Explizite Datentypen

Wenn die Implementierung des Lambdas mehr als eine Zeile benötigt, musst du geschweifte Klammern und eine explizite Rückgabeanweisung verwenden, wie in Beispiel 1-7 gezeigt.

Beispiel 1-7. Ein Block-Lambda
File directory = new File("./src/main/java");

String[] names = directory.list((File dir, String name) -> {  1
    return name.endsWith(".java");
});
System.out.println(Arrays.asList(names));
1

Blocksyntax

Dies ist ein sogenanntes Block-Lambda. In diesem Fall besteht der Textkörper immer noch aus einer einzigen Zeile, aber die geschweiften Klammern ermöglichen jetzt mehrere Anweisungen. Das Schlüsselwort return ist jetzt erforderlich.

Lambda-Ausdrücke existieren nie allein. Es gibt immer einen Kontext für den Ausdruck, der die funktionale Schnittstelle angibt, der der Ausdruck zugeordnet ist. Ein Lambda kann ein Argument für eine Methode sein, ein Rückgabetyp einer Methode oder einer Referenz zugewiesen werden. In jedem Fall muss der Typ der Zuweisung eine funktionale Schnittstelle sein.

1.2 Referenzen zur Methode

Problem

Du willst eine Methodenreferenz verwenden, um auf eine bestehende Methode zuzugreifen und sie wie einen Lambda-Ausdruck zu behandeln.

Lösung

Verwende die Doppelpunktschreibweise, um eine Instanzreferenz oder einen Klassennamen von der Methode zu trennen.((("

(Doppelpunkt) Notation in Methodenreferenzen")))

Diskussion

Wenn ein Lambda-Ausdruck eine Methode so behandelt, als wäre sie ein Objekt, dann behandelt eine Methodenreferenz eine bestehende Methode so, als wäre sie ein Lambda.

Zum Beispiel nimmt die Methode forEach in Iterable ein Consumer als Argument. Beispiel 1-8 zeigt, dass die Methode Consumer entweder als Lambda-Ausdruck oder als Methodenreferenz implementiert werden kann.

Beispiel 1-8. Verwendung einer Methodenreferenz für den Zugriff auf println
Stream.of(3, 1, 4, 1, 5, 9)
        .forEach(x -> System.out.println(x));     1

Stream.of(3, 1, 4, 1, 5, 9)
        .forEach(System.out::println);            2

Consumer<Integer> printer = System.out::println;  3
Stream.of(3, 1, 4, 1, 5, 9)
        .forEach(printer);
1

Einen Lambda-Ausdruck verwenden

2

Verwendung einer Methodenreferenz

3

Zuweisung der Methodenreferenz zu einer funktionalen Schnittstelle

Die Doppelpunktschreibweise gibt den Verweis auf die Methode println der Instanz Sys⁠tem.out an, die eine Referenz vom Typ PrintStream ist. Am Ende der Methodenreferenz werden keine Klammern gesetzt. In dem gezeigten Beispiel wird jedes Element des Streams auf der Standardausgabe ausgegeben.2

Tipp

Wenn du einen Lambda-Ausdruck schreibst, der aus einer Zeile besteht, die eine Methode aufruft, solltest du stattdessen die entsprechende Methodenreferenz verwenden.

Die Methodenreferenz bietet einige (kleine) Vorteile gegenüber der Lambda-Syntax. Erstens ist sie in der Regel kürzer, und zweitens enthält sie oft den Namen der Klasse, die die Methode enthält. Beides macht den Code einfacher zu lesen.

Methodenreferenzen können auch mit statischen Methoden verwendet werden, wie in Beispiel 1-9 gezeigt.

Beispiel 1-9. Verwendung einer Methodenreferenz für eine statische Methode
Stream.generate(Math::random)          1
        .limit(10)
        .forEach(System.out::println); 2
1

Statische Methode

2

Instanzmethode

Die Methode generate auf Stream nimmt Supplier als Argument an, eine funktionale Schnittstelle, deren einzige abstrakte Methode keine Argumente annimmt und ein einziges Ergebnis liefert. Die Methode random in der Klasse Math ist mit dieser Signatur kompatibel, da sie ebenfalls keine Argumente entgegennimmt und einen einzelnen, gleichmäßig verteilten Pseudozufallswert zwischen 0 und 1 erzeugt. Die Methodenreferenz Math::random verweist auf diese Methode als Implementierung der Schnittstelle Supplier.

Da Stream.generate einen unendlichen Strom erzeugt, wird die Methode limit verwendet, um sicherzustellen, dass nur 10 Werte erzeugt werden, die dann mit der Methodenreferenz System.out::println als Implementierung von Consumer auf die Standardausgabe ausgegeben werden.

Syntax

Es gibt drei Formen der Methodenreferenz Syntax, von denen eine etwas irreführend ist:

object::instanceMethod

Verweise auf eine Instanzmethode mit einer Referenz auf das übergebene Objekt, wie in Syste⁠m.out::println

Class::staticMethod

Verweis auf statische Methode, wie in Math::max

Class::instanceMethod

Rufe die Instanzmethode mit einem Verweis auf ein vom Kontext geliefertes Objekt auf, wie in String::length

Das letzte Beispiel ist das verwirrende, denn als Java-Entwickler sind wir es gewohnt, dass nur statische Methoden über einen Klassennamen aufgerufen werden. Erinnere dich daran, dass Lambda-Ausdrücke und Methodenreferenzen nie in einem Vakuum existieren - es gibt immer einen Kontext. Im Falle einer Objektreferenz liefert der Kontext die Argumente für die Methode. Im Fall des Druckens lautet der entsprechende Lambda-Ausdruck (wie im Kontext in Beispiel 1-8 gezeigt):

// equivalent to System.out::println
x -> System.out.println(x)

Der Kontext liefert den Wert von x, der als Argument für die Methode verwendet wird.

Ähnlich verhält es sich mit der statischen Methode max:

// equivalent to Math::max
(x,y) -> Math.max(x,y)

Jetzt muss der Kontext zwei Argumente liefern, und der Lambda gibt das größere zurück.

Die Syntax "Instanzmethode durch den Klassennamen" wird anders interpretiert. Das entsprechende Lambda ist:

// equivalent to String::length
x -> x.length()

Wenn der Kontext x bereitstellt, wird es diesmal als Ziel der Methode verwendet und nicht als Argument.

Tipp

Wenn du dich über den Klassennamen auf eine Methode beziehst, die mehrere Argumente entgegennimmt, wird das erste vom Kontext gelieferte Element zum Ziel und die übrigen Elemente sind Argumente für die Methode.

Beispiel 1-10 zeigt den Beispielcode.

Beispiel 1-10. Aufrufen einer Instanzmethode mit mehreren Argumenten aus einer Klassenreferenz
List<String> strings =
    Arrays.asList("this", "is", "a", "list", "of", "strings");
List<String> sorted = strings.stream()
        .sorted((s1, s2) -> s1.compareTo(s2))  1
        .collect(Collectors.toList());

List<String> sorted = strings.stream()
        .sorted(String::compareTo)             1
        .collect(Collectors.toList());
1

Methodenreferenz und äquivalentes Lambda

Die Methode sorted auf Stream nimmt ein Comparator<T> als Argument, dessen einzige abstrakte Methode int compare(String other) ist. Die Methode sorted übergibt jedes String-Paar an den Komparator und sortiert es nach dem Vorzeichen der zurückgegebenen Ganzzahl. In diesem Fall ist der Kontext ein Paar von Strings. Die Methodenreferenzsyntax mit dem Klassennamen String ruft die Methode compareTo für das erste Element auf (s1 im Lambda-Ausdruck) und verwendet das zweite Element s2 als Argument für die Methode.

Bei der Stream-Verarbeitung greifst du häufig über den Klassennamen in einer Methodenreferenz auf eine Instanzmethode zu, wenn du eine Reihe von Eingaben verarbeitest. Der Code in Beispiel 1-11 zeigt den Aufruf der Methode length für jede einzelne String im Stream.

Beispiel 1-11. Aufrufen der length-Methode für String mit einer Methodenreferenz
Stream.of("this", "is", "a", "stream", "of", "strings")
        .map(String::length)            1
        .forEach(System.out::println);  2
1

Instanzmethode über Klassenname

2

Instanzmethode über Objektreferenz

Dieses Beispiel wandelt jede Zeichenkette in eine ganze Zahl um, indem es die Methode length aufruft, und gibt dann jedes Ergebnis aus.

Eine Methodenreferenz ist im Wesentlichen eine verkürzte Syntax für ein Lambda. Lambda-Ausdrücke sind allgemeiner, da jede Methodenreferenz einen entsprechenden Lambda-Ausdruck hat, aber nicht umgekehrt. Die entsprechenden Lambdas für die Methodenreferenzen aus Beispiel 1-11 sind in Beispiel 1-12 dargestellt.

Beispiel 1-12. Lambda-Ausdruck-Äquivalente für Methodenreferenzen
Stream.of("this", "is", "a", "stream", "of", "strings")
        .map(s -> s.length())
        .forEach(x -> System.out.println(x));

Wie bei jedem Lambda-Ausdruck ist der Kontext wichtig. Du kannst auch this oder super als linke Seite einer Methodenreferenz verwenden, wenn es Unklarheiten gibt.

Siehe auch

Du kannst Konstruktoren auch mit der Syntax der Methodenreferenz aufrufen. Konstruktorreferenzen werden in Rezept 1.3 gezeigt. Das Paket der funktionalen Schnittstellen, einschließlich der in diesem Rezept behandelten Schnittstelle Supplier, wird in Kapitel 2 behandelt.

1.3 Konstruktor-Referenzen

Problem

Du willst ein Objekt mithilfe einer Methodenreferenz als Teil einer Stream-Pipeline instanziieren.

Lösung

Verwende das Schlüsselwort new als Teil einer Methodenreferenz.

Diskussion

Wenn die Leute über die neue Syntax von Java 8 sprechen, erwähnen sie Lambda-Ausdrücke, Methodenreferenzen und Streams. Nehmen wir an, du hast eine Liste von Personen und möchtest sie in eine Liste von Namen umwandeln. Eine Möglichkeit wäre das in Beispiel 1-13 gezeigte Snippet.

Beispiel 1-13. Eine Liste von Personen in eine Liste von Namen umwandeln
List<String> names = people.stream()
    .map(person -> person.getName()) 1
    .collect(Collectors.toList());

// or, alternatively,

List<String> names = people.stream()
    .map(Person::getName)           2
    .collect(Collectors.toList());
1

Lambda-Ausdruck

2

Referenz der Methode

Was ist, wenn du den umgekehrten Weg gehen willst? Was ist, wenn du eine Liste von Strings hast und daraus eine Liste von Person Referenzen erstellen willst? In diesem Fall kannst du eine Methodenreferenz verwenden, aber dieses Mal mit dem Schlüsselwort new. Diese Syntax wird Konstruktorreferenz genannt.

Um zu zeigen, wie es verwendet wird, fangen wir mit der Klasse Person an, die so ziemlich das einfachste Plain Old Java Object (POJO) ist, das man sich vorstellen kann. Sie umhüllt lediglich ein einfaches String-Attribut, das in Beispiel 1-14 name heißt.

Beispiel 1-14. Eine Klasse Person
public class Person {
    private String name;

    public Person() {}

    public Person(String name) {
        this.name = name;
    }

    // getters and setters ...

    // equals, hashCode, and toString methods ...
}

Wenn du eine Sammlung von Strings hast, kannst du jeden einzelnen in eine Person abbilden, indem du entweder einen Lambda-Ausdruck oder den Konstruktorverweis in Beispiel 1-15 verwendest.

Beispiel 1-15. Strings in Person-Instanzen umwandeln
List<String> names =
    Arrays.asList("Grace Hopper", "Barbara Liskov", "Ada Lovelace",
        "Karen Spärck Jones");

List<Person> people = names.stream()
    .map(name -> new Person(name)) 1
    .collect(Collectors.toList());

// or, alternatively,

List<Person> people = names.stream()
    .map(Person::new)              2
    .collect(Collectors.toList());
1

Einen Lambda-Ausdruck verwenden, um den Konstruktor aufzurufen

2

Mit einer Konstruktorreferenz instanziieren Person

Die Syntax Person::new bezieht sich auf den Konstruktor in der Klasse Person. Wie bei allen Lambda-Ausdrücken bestimmt der Kontext, welcher Konstruktor ausgeführt wird. Da der Kontext einen String liefert, wird der Ein-Arg-Konstruktor String verwendet.

Konstruktor kopieren

Ein Kopierkonstruktor nimmt ein Person Argument und gibt ein neues Person mit den gleichen Attributen zurück, wie in Beispiel 1-16 gezeigt.

Beispiel 1-16. Ein Kopierkonstruktor für Person
public Person(Person p) {
    this.name = p.name;
}

Das ist nützlich, wenn du den Streaming-Code von den ursprünglichen Instanzen isolieren willst. Wenn du zum Beispiel bereits eine Liste von Personen hast, die Liste in einen Stream umwandelst und dann wieder in eine Liste, sind die Referenzen dieselben (siehe Beispiel 1-17).

Beispiel 1-17. Konvertierung einer Liste in einen Stream und zurück
Person before = new Person("Grace Hopper");

List<Person> people = Stream.of(before)
    .collect(Collectors.toList());
Person after = people.get(0);

assertTrue(before == after);                          1

before.setName("Grace Murray Hopper");                2
assertEquals("Grace Murray Hopper", after.getName()); 3
1

Dasselbe Objekt

2

Name ändern mit before Referenz

3

Der Name wurde in der Referenz after geändert.

Mit einem Kopierkonstruktor kannst du diese Verbindung unterbrechen, wie in Beispiel 1-18.

Beispiel 1-18. Den Kopierkonstruktor verwenden
people = Stream.of(before)
      .map(Person::new)            1
      .collect(Collectors.toList());
after = people.get(0);
assertFalse(before == after);      2
assertEquals(before, after);       3

before.setName("Rear Admiral Dr. Grace Murray Hopper");
assertFalse(before.equals(after));
1

Kopierkonstruktor verwenden

2

Verschiedene Objekte

3

Aber gleichwertig

Dieses Mal ist der Kontext beim Aufruf der Methode map ein Strom von Person Instanzen. Deshalb ruft die Person::new Syntax den Konstruktor auf, der eine Person Instanz nimmt und eine neue, aber gleichwertige Instanz zurückgibt, und hat die Verbindung zwischen der Vorher-Referenz und der Nachher-Referenz unterbrochen.3

Varargs-Konstruktor

Betrachte nun einen varargs-Konstruktor , der der Person POJO hinzugefügt wurde, wie in Beispiel 1-19 gezeigt.

Beispiel 1-19. Ein Person-Konstruktor, der eine variable Argumentliste von String annimmt
public Person(String... names) {
    this.name = Arrays.stream(names)
                      .collect(Collectors.joining(" "));
}

Dieser Konstruktor nimmt null oder mehr String-Argumente und verkettet sie mit einem einzelnen Leerzeichen als Trennzeichen.

Wie kann dieser Konstruktor aufgerufen werden? Jeder Client, der null oder mehr durch Kommas getrennte String-Argumente übergibt, ruft ihn auf. Eine Möglichkeit, das zu tun, ist, die Methode split auf String zu nutzen, die ein Trennzeichen annimmt und ein Array String zurückgibt:

String[] split(String delimiter)

Deshalb zerlegt der Code in Beispiel 1-20 jeden String in der Liste in einzelne Wörter und ruft den varargs-Konstruktor auf.

Beispiel 1-20. Verwendung des varargs-Konstruktors
names.stream()                     1
    .map(name -> name.split(" "))  2
    .map(Person::new)              3
    .collect(Collectors.toList()); 4
1

Einen Strom von Strings erstellen

2

Abbildung auf einen Stream von String-Arrays

3

Karte zu einem Strom von Person

4

Sammeln zu einer Liste von Person

Dieses Mal ist der Kontext für die Methode map, die die Person::new Konstruktorreferenz enthält, ein Stream von String-Arrays, also wird der varargs-Konstruktor aufgerufen. Wenn du eine einfache Druckanweisung zu diesem Konstruktor hinzufügst:

System.out.println("Varargs ctor, names=" + Arrays.asList(names));

dann ist das Ergebnis:

Varargs ctor, names=[Grace, Hopper]
Varargs ctor, names=[Barbara, Liskov]
Varargs ctor, names=[Ada, Lovelace]
Varargs ctor, names=[Karen, Spärck, Jones]

Arrays

Konstruktorreferenzen können auch mit Arrays verwendet werden. Wenn du ein Array von Person Instanzen, Person[], anstelle einer Liste haben möchtest, kannst du die Methode toArray auf Stream verwenden, deren Signatur lautet:

<A> A[] toArray(IntFunction<A[]> generator)

Diese Methode verwendet A, um den generischen Typ des zurückgegebenen Arrays mit den Elementen des Streams zu repräsentieren, der mit der bereitgestellten Generatorfunktion erstellt wird. Das Tolle daran ist, dass dafür auch eine Konstruktorreferenz verwendet werden kann, wie in Beispiel 1-21.

Beispiel 1-21. Ein Array von Personenreferenzen erstellen
Person[] people = names.stream()
    .map(Person::new)         1
    .toArray(Person[]::new);  2
1

Konstruktor-Referenz für Person

2

Konstruktorreferenz für ein Array von Person

Das Argument der Methode toArray erstellt ein Array von Person Referenzen in der richtigen Größe und füllt es mit den instanziierten Person Instanzen.

Konstruktorreferenzen sind nur Methodenreferenzen unter einem anderen Namen, die das Wort new verwenden, um einen Konstruktor aufzurufen. Welcher Konstruktor das ist, wird wie üblich durch den Kontext bestimmt. Diese Technik bietet eine große Flexibilität bei der Verarbeitung von Streams.

Siehe auch

Methodenreferenzen werden in Rezept 1.2 besprochen.

1.4 Funktionale Schnittstellen

Problem

Du willst eine bestehende funktionale Schnittstelle verwenden oder deine eigene schreiben.

Lösung

Erstelle eine Schnittstelle mit einer einzelnen, abstrakten Methode und füge die Anmerkung @FunctionalInterface hinzu.

Diskussion

Eine funktionale Schnittstelle in Java 8 ist eine Schnittstelle mit einer einzelnen, abstrakten Methode. Als solche kann sie das Ziel für einen Lambda-Ausdruck oder eine Methodenreferenz sein.

Die Verwendung des Begriffs abstract ist hier von Bedeutung. Vor Java 8 galten alle Methoden in Schnittstellen standardmäßig als abstrakt - du musstest das Schlüsselwort nicht einmal hinzufügen.

Hier ist zum Beispiel die Definition einer Schnittstelle namens PalindromeChecker, die in Beispiel 1-22 gezeigt wird.

Beispiel 1-22. Ein Palindrom Checker Interface
@FunctionalInterface
public interface PalindromeChecker {
    boolean isPalidrome(String s);
}

Alle Methoden in einer Schnittstelle sind public,4 Du kannst also den Zugriffsmodifikator weglassen, genauso wie du das Schlüsselwort abstract weglassen kannst.

Da diese Schnittstelle nur eine einzige, abstrakte Methode hat, ist sie eine funktionale Schnittstelle. Java 8 bietet eine Annotation namens @FunctionalInterface im Paket java.lang, die auf die Schnittstelle angewendet werden kann, wie im Beispiel gezeigt.

Diese Anmerkung ist nicht erforderlich, aber aus zwei Gründen eine gute Idee. Erstens wird zur Kompilierzeit überprüft, ob die Schnittstelle tatsächlich die Anforderung erfüllt. Wenn die Schnittstelle entweder keine oder mehr als eine abstrakte Methode hat, bekommst du einen Compilerfehler.

Ein weiterer Vorteil der @FunctionalInterface Annotation ist, dass sie eine Aussage in den Javadocs wie folgt erzeugt:

Functional Interface:
This is a functional interface and can therefore be used as the assignment
target for a lambda expression or method reference.

Funktionale Schnittstellen können auch default und static Methoden haben. Sowohl Standard- als auch statische Methoden haben Implementierungen, sodass sie nicht gegen die Anforderung einer einzigen abstrakten Methode verstoßen. Beispiel 1-23 zeigt den Beispielcode.

Beispiel 1-23. MyInterface ist eine funktionale Schnittstelle mit statischen und Standardmethoden
@FunctionalInterface
public interface MyInterface {
    int myMethod();          1
    // int myOtherMethod();  2

    default String sayHello() {
        return "Hello, World!";
    }

    static void myStaticMethod() {
        System.out.println("I'm a static method in an interface");
    }
}
1

Einzelne abstrakte Methode

2

Wenn sie hinzugefügt wird, wäre dies keine funktionale Schnittstelle mehr.

Wenn die kommentierte Methode myOtherMethod enthalten wäre, würde die Schnittstelle nicht mehr die Anforderungen an eine funktionale Schnittstelle erfüllen. Die Annotation würde einen Fehler der Form "multiple non-overriding abstract methods found" erzeugen.

Schnittstellen können andere Schnittstellen erweitern, sogar mehr als eine. Die Annotation prüft die aktuelle Schnittstelle. Wenn also eine Schnittstelle eine bestehende funktionale Schnittstelle erweitert und eine weitere abstrakte Methode hinzufügt, ist sie selbst keine funktionale Schnittstelle. Siehe Beispiel 1-24.

Beispiel 1-24. Erweiterung einer funktionalen Schnittstelle - nicht mehr funktional
public interface MyChildInterface extends MyInterface {
    int anotherMethod(); 1
}
1

Zusätzliche abstrakte Methode

Die MyChildInterface ist keine funktionale Schnittstelle, weil sie zwei abstrakte Methoden hat: myMethod, die sie von MyInterface erbt; und anotherMethod, die sie deklariert. Ohne die @FunctionalInterface Annotation lässt sie sich kompilieren, weil sie eine Standardschnittstelle ist. Sie kann jedoch nicht das Ziel eines Lambda-Ausdrucks sein.

Ein Kanten-Fall sollte ebenfalls beachtet werden. Die Schnittstelle Comparator wird für die Sortierung verwendet, die in anderen Rezepten behandelt wird. Wenn du dir die Javadocs für diese Schnittstelle ansiehst und die Registerkarte Abstrakte Methoden auswählst, siehst du die in Abbildung 1-1 dargestellten Methoden.

mjr 0101
Abbildung 1-1. Abstrakte Methoden in der Klasse Comparator

Moment, was? Wie kann dies eine funktionale Schnittstelle sein, wenn es zwei abstrakte Methoden gibt, insbesondere wenn eine davon tatsächlich in java.lang.Object implementiert ist?

Die Besonderheit hier ist, dass die gezeigte Methode equals von Object stammt und daher bereits eine Standardimplementierung hat. In der ausführlichen Dokumentation heißt es, dass du aus Leistungsgründen deine eigene equals Methode bereitstellen kannst, die denselben Vertrag erfüllt, dass es aber "immer sicher ist, diese Methode nicht zu überschreiben" (Hervorhebung im Original).

Die Regeln für funktionale Schnittstellen besagen, dass die Methoden von Object nicht auf die Grenze von einer abstrakten Methode angerechnet werden. Comparator ist also immer noch eine funktionale Schnittstelle.

Siehe auch

Standardmethoden in Schnittstellen werden in Rezept 1.5 und statische Methoden in Schnittstellen in Rezept 1.6 behandelt.

1.5 Standardmethoden in Schnittstellen

Problem

Du möchtest eine Implementierung einer Methode innerhalb einer Schnittstelle bereitstellen.

Lösung

Verwende das Schlüsselwort default für die Schnittstellenmethode und füge die Implementierung auf normale Weise hinzu.

Diskussion

Der traditionelle Grund, warum Java nie Mehrfachvererbung unterstützt hat, ist das so genannte Rautenproblem. Angenommen, du hast eine Vererbungshierarchie, wie sie in der (vage an die UML angelehnten) Abbildung 1-2 dargestellt ist.

mjr 0102
Abbildung 1-2. Tierische Vererbung

Die Klasse Animal hat zwei Unterklassen, Bird und Horse, die jeweils die Methode speak von Animal überschreiben, um in Horse "wiehern" und in Bird "zwitschern" zu sagen. Was sagt dann Pegasus (das mehrfach von Horse und Bird erbt)?5 sagen? Was ist, wenn du eine Referenz vom Typ Animal einer Instanz von Pegasus zugewiesen hast? Was sollte die Methode speak dann zurückgeben?

Animal animal = new Pegaus();
animal.speak(); // whinny, chirp, or other?

Verschiedene Sprachen gehen unterschiedlich mit diesem Problem um. In C++ zum Beispiel ist Mehrfachvererbung erlaubt, aber wenn eine Klasse widersprüchliche Implementierungen erbt, wird sie nicht kompiliert.6 In Eiffel,7 lässt der Compiler dir die Wahl, welche Implementierung du möchtest.

Javas Ansatz war es, Mehrfachvererbung zu verbieten, und Schnittstellen wurden als Abhilfe eingeführt, wenn eine Klasse eine "ist eine Art von"-Beziehung zu mehr als einem Typ hat. Da Schnittstellen nur abstrakte Methoden haben, gibt es keine Implementierungen, die in Konflikt geraten könnten. Bei Schnittstellen ist Mehrfachvererbung erlaubt, aber auch das funktioniert, weil nur die Methodensignaturen vererbt werden.

Das Problem ist, dass du nie eine Methode in einer Schnittstelle implementieren kannst, was zu umständlichen Designs führt. Zu den Methoden der Schnittstelle java.util.Collection gehören zum Beispiel:

boolean isEmpty()
int     size()

Die Methode isEmpty gibt true zurück, wenn keine Elemente in der Sammlung vorhanden sind, und false, wenn nicht. Die Methode size gibt die Anzahl der Elemente in den Sammlungen zurück. Unabhängig von der zugrundeliegenden Implementierung kannst du die Methode isEmpty sofort in Form von size implementieren, wie in Beispiel 1-25.

Beispiel 1-25. Implementierung von isEmpty in Bezug auf die Größe
public boolean isEmpty() {
    return size() == 0;
}

Da Collection eine Schnittstelle ist, kannst du dies nicht in der Schnittstelle selbst tun. Stattdessen enthält die Standardbibliothek eine abstrakte Klasse namens java.util.AbstractCollection, die neben anderem Code genau die hier gezeigte Implementierung von isEmpty enthält. Wenn du deine eigene Sammelimplementierung erstellst und noch keine Oberklasse hast, kannst du AbstractCollection erweitern und erhältst die Methode isEmpty kostenlos. Wenn du bereits eine Oberklasse hast, musst du stattdessen die Schnittstelle Col⁠lection implementieren und dich daran erinnern, deine eigene Implementierung von isEmpty sowie size bereitzustellen.

Erfahrene Java-Entwickler sind mit all dem vertraut, aber mit Java 8 ändert sich die Situation. Jetzt kannst du Implementierungen zu Schnittstellenmethoden hinzufügen. Alles, was du tun musst, ist, das Schlüsselwort default zu einer Methode hinzuzufügen und eine Implementierung anzugeben. Der Code in Beispiel 1-26 zeigt eine Schnittstelle mit abstrakten und Standardmethoden.

Beispiel 1-26. Eine Mitarbeiterschnittstelle mit einer Standardmethode
public interface Employee {
    String getFirst();

    String getLast();

    void convertCaffeineToCodeForMoney();

    default String getName() {  1
        return String.format("%s %s", getFirst(), getLast());
    }
}
1

Standardmethode mit einer Implementierung

Die Methode getName hat das Schlüsselwort default und ihre Implementierung erfolgt durch die anderen, abstrakten Methoden der Schnittstelle, getFirst und getLast.

Viele der bestehenden Schnittstellen in Java wurden um Standardmethoden erweitert, um die Abwärtskompatibilität zu gewährleisten. Wenn du eine neue Methode zu einer Schnittstelle hinzufügst, machst du normalerweise alle bestehenden Implementierungen kaputt. Wenn man eine neue Methode als Standardmethode hinzufügt, erben alle bestehenden Implementierungen die neue Methode und funktionieren weiterhin. Auf diese Weise konnten die Betreuer der Bibliothek neue Standardmethoden im gesamten JDK hinzufügen, ohne bestehende Implementierungen zu zerstören.

Zum Beispiel enthält java.util.Collection jetzt die folgenden Standardmethoden:

default boolean        removeIf(Predicate<? super E> filter)
default Stream<E>      stream()
default Stream<E>      parallelStream()
default Spliterator<E> spliterator()

Die Methode removeIf entfernt alle Elemente aus der Sammlung, die dem Predicate8 Argument erfüllen, und gibt true zurück, wenn irgendwelche Elemente entfernt wurden. Die Methoden stream und parallelStream sind Factory-Methoden zur Erstellung von Streams. Die Methode spliterator gibt ein Objekt einer Klasse zurück, die die Schnittstelle Spliterator implementiert, also ein Objekt zum Durchlaufen und Aufteilen von Elementen aus einer Quelle.

Standardmethoden werden genauso wie alle anderen Methoden verwendet, wie Beispiel 1-27 zeigt.

Beispiel 1-27. Standardmethoden verwenden
List<Integer> nums = new ArrayList<>();
nums.add(-3); nums.add(1); nums.add(4);
nums.add(-1); nums.add(5); nums.add(9);
boolean removed = nums.removeIf(n -> n <= 0);  1
System.out.println("Elements were " + (removed ? "" : "NOT") + " removed");
nums.forEach(System.out::println);             2
1

Verwende die Methode default removeIf von Collection

2

Verwende die Methode default forEach von Iterator

Was passiert, wenn eine Klasse zwei Schnittstellen mit der gleichen Standardmethode implementiert? Das ist das Thema von Rezept 5.5, aber die kurze Antwort lautet: Wenn die Klasse die Methode selbst implementiert, ist alles in Ordnung. Siehe Rezept 5.5 für weitere Details.

Siehe auch

Rezept 5.5 zeigt die Regeln, die gelten, wenn eine Klasse mehrere Schnittstellen mit Standardmethoden implementiert.

1.6 Statische Methoden in Interfaces

Problem

Du möchtest eine Utility-Methode auf Klassenebene zu einer Schnittstelle hinzufügen, zusammen mit einer Implementierung.

Lösung

Mache die Methode static und stelle die Implementierung auf die übliche Weise zur Verfügung.

Diskussion

Statische Mitglieder von Java-Klassen sind auf Klassenebene angesiedelt, d.h. sie sind mit der Klasse als Ganzes und nicht mit einer bestimmten Instanz verbunden. Das macht ihre Verwendung in Schnittstellen vom Standpunkt des Designs aus gesehen problematisch. Einige Fragen dazu sind:

  • Was bedeutet ein Member auf Klassenebene, wenn die Schnittstelle von vielen verschiedenen Klassen implementiert wird?

  • Muss eine Klasse eine Schnittstelle implementieren, um eine statische Methode verwenden zu können?

  • Statische Methoden in Klassen werden über den Klassennamen aufgerufen. Wenn eine Klasse eine Schnittstelle implementiert, wird eine statische Methode dann über den Klassennamen oder den Schnittstellennamen aufgerufen?

Die Designer von Java hätten diese Fragen auf verschiedene Arten entscheiden können. Vor Java 8 war die Entscheidung, statische Mitglieder in Schnittstellen überhaupt nicht zuzulassen.

Leider führte das aber zur Schaffung von Utility-Klassen: Klassen, die nur statische Methoden enthalten. Ein typisches Beispiel ist java.util.Collections, das Methoden zum Sortieren und Suchen, zum Einpacken von Sammlungen in synchronisierte oder unveränderbare Typen und mehr enthält. Im NIO-Paket ist java.nio.file.Paths ein weiteres Beispiel. Es enthält nur statische Methoden, die Path Instanzen aus Strings oder URIs parsen.

In Java 8 kannst du jetzt statische Methoden zu Schnittstellen hinzufügen, wann immer du willst. Die Voraussetzungen sind:

  • Füge das Schlüsselwort static zu der Methode hinzu.

  • eine Implementierung bereitstellen (die nicht überschrieben werden kann). Auf diese Weise sind sie wie die Methoden von default und werden in der Standard-Registerkarte in den Javadocs aufgeführt.

  • Rufe die Methode über den Namen der Schnittstelle auf. Klassen müssen eine Schnittstelle nicht implementieren, um ihre statischen Methoden zu verwenden.

Ein Beispiel für eine praktische statische Methode in einer Schnittstelle ist die Methode comparing in java.util.Comparator, zusammen mit ihren primitiven Varianten comparingInt, compa⁠ringLong und comparingDouble. Die Schnittstelle Comparator hat auch statische Methoden naturalOrder und reverseOrder. Beispiel 1-28 zeigt, wie sie verwendet werden.

Beispiel 1-28. Zeichenketten sortieren
List<String> bonds = Arrays.asList("Connery", "Lazenby", "Moore",
    "Dalton", "Brosnan", "Craig");

List<String> sorted = bonds.stream()
    .sorted(Comparator.naturalOrder())                 1
    .collect(Collectors.toList());
// [Brosnan, Connery, Craig, Dalton, Lazenby, Moore]

sorted = bonds.stream()
    .sorted(Comparator.reverseOrder())                 2
    .collect(Collectors.toList());
// [Moore, Lazenby, Dalton, Craig, Connery, Brosnan]

sorted = bonds.stream()
    .sorted(Comparator.comparing(String::toLowerCase)) 3
    .collect(Collectors.toList());
// [Brosnan, Connery, Craig, Dalton, Lazenby, Moore]

sorted = bonds.stream()
    .sorted(Comparator.comparingInt(String::length))   4
    .collect(Collectors.toList());
// [Moore, Craig, Dalton, Connery, Lazenby, Brosnan]

sorted = bonds.stream()
    .sorted(Comparator.comparingInt(String::length)    5
        .thenComparing(Comparator.naturalOrder()))
    .collect(Collectors.toList());
// [Craig, Moore, Dalton, Brosnan, Connery, Lazenby]
1

Natürliche Ordnung (lexikografisch)

2

Umgekehrt lexikografisch

3

Nach Namen in Kleinbuchstaben sortieren

4

Nach Namenslänge sortieren

5

Nach Länge sortieren, dann gleiche Längen lexikografisch

Das Beispiel zeigt, wie man mehrere statische Methoden in Comparator verwendet, um die Liste der Schauspieler zu sortieren, die im Laufe der Jahre James Bond gespielt haben.9 Vergleicher werden in Rezept 4.1 näher erläutert.

Statische Methoden in Schnittstellen machen es überflüssig, separate Hilfsklassen zu erstellen, obwohl diese Option immer noch zur Verfügung steht, wenn ein Entwurf dies erfordert.

Die wichtigsten Punkte, an die du dich erinnern solltest, sind:

  • Statische Methoden müssen eine Implementierung haben

  • Du kannst eine statische Methode nicht außer Kraft setzen

  • Statische Methoden über den Schnittstellennamen aufrufen

  • Du brauchst eine Schnittstelle nicht zu implementieren, um ihre statischen Methoden zu nutzen

Siehe auch

Statische Methoden von Schnittstellen werden in diesem Buch durchgängig verwendet, aber Rezept 4.1 behandelt die hier verwendeten statischen Methoden von Comparator.

1 Der Begriff wurde von Gordon Moore, einem der Mitbegründer von Fairchild Semiconductor und Intel, geprägt und basiert auf der Beobachtung, dass sich die Anzahl der Transistoren, die in einem integrierten Schaltkreis untergebracht werden können, etwa alle 18 Monate verdoppelt. Weitere Informationen findest du im Wikipedia-Eintrag zu Moores Gesetz.

2 Es ist schwierig, Lambdas oder Methodenreferenzen zu besprechen, ohne auf Streams einzugehen, denen später ein eigenes Kapitel gewidmet ist. Es genügt zu sagen, dass ein Stream eine Reihe von Elementen nacheinander erzeugt, sie nirgendwo speichert und die ursprüngliche Quelle nicht verändert.

3 Ich will nicht respektlos sein, wenn ich Admiral Hopper wie ein Objekt behandle. Ich bezweifle nicht, dass sie mir immer noch in den Hintern treten könnte, und sie ist 1992 verstorben.

4 Zumindest bis Java 9, wo private Methoden auch in Interfaces erlaubt sind. Siehe Rezept 10.2 für Details.

5 "Ein prächtiges Pferd mit dem Gehirn eines Vogels." (Disneys Herkules-Film, der Spaß macht, wenn du so tust, als wüsstest du nichts über die griechische Mythologie und hättest noch nie von Herkules gehört).

6 Das kann durch virtuelle Vererbung gelöst werden, aber trotzdem.

7 Hier ist eine obskure Referenz für dich, aber Eiffel war eine der grundlegenden Sprachen der objektorientierten Programmierung. Siehe Bertrand Meyer's Object-Oriented Software Construction, Second Edition (Prentice Hall, 1997).

8 Predicate ist eine der neuen funktionalen Schnittstellen im Paket java.util.function, die in Rezept 2.3 ausführlich beschrieben werden.

9 Die Versuchung, Idris Elba auf die Liste zu setzen, ist fast überwältigend, aber das ist bisher nicht gelungen.

Get Moderne Java-Rezepte now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.