Rheinwerk Computing < openbook >

Inhaltsverzeichnis
Materialien zum Buch
Vorwort
1 Java ist auch eine Sprache
2 Imperative Sprachkonzepte
3 Klassen und Objekte
4 Arrays und ihre Anwendungen
5 Der Umgang mit Zeichen und Zeichenketten
6 Eigene Klassen schreiben
7 Objektorientierte Beziehungsfragen
8 Schnittstellen, Aufzählungen, versiegelte Klassen, Records
9 Ausnahmen müssen sein
10 Geschachtelte Typen
11 Besondere Typen der Java SE
12 Generics<T>
13 Lambda-Ausdrücke und funktionale Programmierung
14 Architektur, Design und angewandte Objektorientierung
15 Java Platform Module System
16 Die Klassenbibliothek
17 Einführung in die nebenläufige Programmierung
18 Einführung in Datenstrukturen und Algorithmen
19 Einführung in grafische Oberflächen
20 Einführung in Dateien und Datenströme
21 Einführung ins Datenbankmanagement mit JDBC
22 Bits und Bytes, Mathematisches und Geld
23 Testen mit JUnit
24 Die Werkzeuge des JDK
A Java SE-Module und Paketübersicht
Stichwortverzeichnis


Buch bestellen
Ihre Meinung?



Spacer
<< zurück
Java ist auch eine Insel von Christian Ullenboom

Einführung, Ausbildung, Praxis
Buch: Java ist auch eine Insel


Java ist auch eine Insel

Pfeil 13 Lambda-Ausdrücke und funktionale Programmierung
Pfeil 13.1 Funktionale Schnittstellen und Lambda-Ausdrücke
Pfeil 13.1.1 Klassen implementieren Schnittstellen
Pfeil 13.1.2 Lambda-Ausdrücke implementieren Schnittstellen
Pfeil 13.1.3 Funktionale Schnittstellen
Pfeil 13.1.4 Der Typ eines Lambda-Ausdrucks ergibt sich durch den Zieltyp
Pfeil 13.1.5 Annotation @FunctionalInterface
Pfeil 13.1.6 Syntax für Lambda-Ausdrücke
Pfeil 13.1.7 Die Umgebung der Lambda-Ausdrücke und Variablenzugriffe
Pfeil 13.1.8 Ausnahmen in Lambda-Ausdrücken
Pfeil 13.1.9 Klassen mit einer abstrakten Methode als funktionale Schnittstelle? *
Pfeil 13.2 Methodenreferenz
Pfeil 13.2.1 Motivation
Pfeil 13.2.2 Methodenreferenzen mit ::
Pfeil 13.2.3 Varianten von Methodenreferenzen
Pfeil 13.3 Konstruktorreferenz
Pfeil 13.3.1 Parameterlose und parametrisierte Konstruktoren
Pfeil 13.3.2 Nützliche vordefinierte Schnittstellen für Konstruktorreferenzen
Pfeil 13.4 Funktionale Programmierung
Pfeil 13.4.1 Code = Daten
Pfeil 13.4.2 Programmierparadigmen: imperativ oder deklarativ
Pfeil 13.4.3 Das Wesen der funktionalen Programmierung
Pfeil 13.4.4 Funktionale Programmierung und funktionale Programmiersprachen
Pfeil 13.4.5 Funktionen höherer Ordnung am Beispiel von Comparator
Pfeil 13.4.6 Lambda-Ausdrücke als Abbildungen bzw. Funktionen betrachten
Pfeil 13.5 Funktionale Schnittstellen aus dem java.util.function-Paket
Pfeil 13.5.1 Blöcke mit Code und die funktionale Schnittstelle Consumer
Pfeil 13.5.2 Supplier
Pfeil 13.5.3 Prädikate und java.util.function.Predicate
Pfeil 13.5.4 Funktionen über die funktionale Schnittstelle java.util.function.Function
Pfeil 13.5.5 Ein bisschen Bi …
Pfeil 13.5.6 Funktionale Schnittstellen mit Primitiven
Pfeil 13.6 Optional ist keine Nullnummer
Pfeil 13.6.1 Einsatz von null
Pfeil 13.6.2 Der Optional-Typ
Pfeil 13.6.3 Erst mal funktional mit Optional
Pfeil 13.6.4 Primitiv-Optionales mit speziellen Optional*-Klassen
Pfeil 13.7 Was ist jetzt so funktional?
Pfeil 13.8 Zum Weiterlesen
 

Zum Seitenanfang

13.5    Funktionale Schnittstellen aus dem java.util.function-Paket Zur vorigen ÜberschriftZur nächsten Überschrift

Funktionen realisieren Abbildungen, und da es verschiedene Arten von Abbildungen geben kann, bietet die Java-Standardbibliothek im Paket java.util.function für die häufigsten Fälle funktionale Schnittstellen an. Tabelle 13.12 bietet einen ersten Überblick:

Schnittstelle

Abbildung

Consumer<T>

(T) → void

DoubleConsumer

(double) → void

BiConsumer<T,U>

(T, U) → void

Supplier<T>

() → T

BooleanSupplier

() → boolean

Predicate<T>

(T) → boolean

LongPredicate

(long) → boolean

BiPredicate<T,U>

(T, U) → boolean

Function<T,R>

(T) → R

LongToDoubleFunction

(long) → double

BiFunction<T,U,R>

(T, U) → R

UnaryOperator<T>

(T) → T

DoubleBinaryOperator

(double, double) → double

Tabelle 13.12     Beispiele einiger vordefinierter funktionaler Schnittstellen

 

Zum Seitenanfang

13.5.1    Blöcke mit Code und die funktionale Schnittstelle Consumer Zur vorigen ÜberschriftZur nächsten Überschrift

Anweisungen von Code lassen sich in eine Methode eines Objekts setzen und auf diese Weise weitergeben. Das ist eine häufige Notwendigkeit, für die das Paket java.util.function eine einfache funktionale Schnittstelle Consumer vorgibt, die einen Konsumenten repräsentiert, der Daten annimmt und dann »verbraucht« (konsumiert) und nichts zurückgibt.

interface java.util.function.Consumer<T>
  • void accept(T t)

    Führt Operationen mit der Übergabe t durch.

  • default Consumer<T> andThen(Consumer<? super T> after)

    Liefert einen neuen Consumer, der erst den aktuellen Consumer ausführt und danach after.

Die accept(…)-Methode bekommt ein Argument – wobei die Implementierung natürlich nicht zwingend darauf zurückgreifen muss – und liefert keine Rückgabe. Transformationen sind damit nicht möglich, denn nur über Umwege kann der Konsument die Ergebnisse speichern, und dafür ist die Schnittstelle nicht gedacht. Consumer-Typen sind eher als Endglied einer Kette gedacht, in der zum Beispiel Daten in eine Datei geschrieben werden, die vorher verarbeitet wurden. Diese Seiteneffekte sind beabsichtigt, da sie nach einer Kette von seiteneffektfreien Operationen stehen.

Typ Consumer in der API

In der Java-API zeigt sich der Typ Consumer in der Regel als Argument einer Methode forEach(Consumer), die Datenquellen abläuft und für jedes Element accept(…) aufruft. Interessant ist die Methode am Typ Iterable, denn die wichtigen Collection-Datenstrukturen wie ArrayList implementieren diese Schnittstelle. So lässt sich einfach über alle Daten laufen und ein Stück Code für jedes Element ausführen. Auch Iterator hat eine vergleichbare Methode. Da heißt sie forEachRemaining(Consumer) – das »Remaining« macht deutlich, dass der Iterator schon ein paar next()-Aufrufe erlebt haben könnte und die Konsumenten daher nicht zwingend die ersten Elemente mitbekommen.

[zB]  Beispiel

Gib jedes Element einer Liste auf der Konsole aus:

Arrays.asList( 1, 2, 3, 4 ).forEach( System.out::println );

Gegenüber einem normalen Durchiterieren ist die funktionale Variante ein wenig kürzer im Code, aber sonst gibt es keinen Unterschied. Auch forEach(…) macht auf dem Iterable nichts anderes, als alle Elemente über den Iterator zu holen.

Einen eigenen Wrapper-Consumer schreiben

Immer repräsentieren Konsumenten Code, und eine API kann nun einfach einen Codeblock nach der Art doSomethingWith(myConsumer) annehmen, um ihn etwa in einem Hintergrund-Thread abzuarbeiten oder wiederholend auszuführen. Sie kann ihn auch nach einer erlaubten Maximaldauer abbrechen oder die Zeit messen oder, oder, oder …

Schreiben wir einen Consumer-Wrapper, der die Ausführungszeit eines anderen Konsumenten loggt:

Listing 13.11     src/main/java/com/tutego/insel/lambda/Consumers.java, Ausschnitt

class Consumers {

public static <T> Consumer<T> executionTimeLogger( Consumer<T> block ) {

return t -> {

long start = System.nanoTime();

block.accept( t );

long duration = System.nanoTime() - start;

Logger.getAnonymousLogger().info( "Ausführungszeit (ns): " + duration );

};

}

}

Folgender Aufruf zeigt die Nutzung:

Arrays.asList( 1, 2, 3, 4 )

.forEach( executionTimeLogger( System.out::println ) );

Was wir hier implementiert haben, ist ein Beispiel für das Execute-around-Method-Muster, bei dem wir um einen Block Code noch etwas anderes legen.

 

Zum Seitenanfang

13.5.2    Supplier Zur vorigen ÜberschriftZur nächsten Überschrift

Ein Supplier (auch Provider genannt) ist eine Fabrik und sorgt für Objekte. In Java deklariert das Paket java.util.function die funktionale Schnittstelle Supplier für Objektgeber:

interface java.util.function.Supplier<T>
  • T get()

    Führt Operationen mit der Übergabe t durch.

Weitere statische oder Default-Methoden deklariert Supplier nicht. Was get() nun genau liefert, ist Aufgabe der Implementierung und ein Internum. Es können neue Objekte sein, immer die gleichen Objekte (Singleton) oder Objekte aus einem Cache.

 

Zum Seitenanfang

13.5.3    Prädikate und java.util.function.Predicate Zur vorigen ÜberschriftZur nächsten Überschrift

Ein Prädikat ist eine Aussage über einen Gegenstand, die wahr oder falsch ist. Die Frage "tutego".isEmpty(), ob also die Zeichenfolge »tutego« leer ist oder nicht, wird mit falsch beantwortet – isEmpty ist also ein Prädikat, weil es über einen Gegenstand (eine Zeichenfolge in unserem Fall) eine Wahrheitsaussage fällen kann.

Prädikate als Objekte sind flexibel, denn Objekte lassen sich an unterschiedliche Stellen weitergeben. So kann ein Prädikat bestimmen, was aus einer Sammlung gelöscht werden soll oder ob mindestens ein Element in einer Sammlung ist.

Das java.util.function-Paket[ 226 ](Achtung, in javax.sql.rowset gibt es ebenfalls eine Schnittstelle Predicate. ) deklariert eine flexible funktionale Schnittstelle Predicate auf folgende Weise:

interface java.util.function.Predicate<T>
  • boolean test(T t)

    Führt einen Test auf t durch und liefert true, wenn das Kriterium erfüllt ist, sonst false.

[zB]  Beispiel

Der Test, ob ein Zeichen eine Ziffer ist, kann durch Prädikat-Objekte nun auch anders durchgeführt werden:

Predicate<Character> isDigit = 

c -> Character.isDigit( c ); // kurz: Character::isDigit

System.out.println( isDigit.test('a') ); // false

Hätte es die Schnittstelle Predicate schon früher in Java 1.0 gegeben, hätte es der Methode Character.isDigit(…) gar nicht bedurft, es hätte auch ein Predicate<Character> als statische Variable in der Klasse Character geben können, sodass ein Test dann als Character.IS_DIGIT.test(…) geschrieben würde oder als Rückgabe von einer Methode Predicate<Character> isDigit() mit der Nutzung Character.isDigit().test(…). Es ist daher gut möglich, dass sich in Zukunft die API dahingehend verändert, dass Aussagen auf Gegenständen mit Wahrheitsrückgabe nicht mehr als Methoden bei den Klassen realisiert werden, sondern als Prädikat-Objekte angeboten werden. Aber Methodenreferenzen geben uns zum Glück die Flexibilität, existierende Methoden problemlos als Lambda-Ausdrücke zu nutzen, und so kommen wir wieder von Methoden zu Funktionen.

Der Typ Predicate in der API

Es gibt in der Java-API einige Stellen, an denen Predicate-Objekte genutzt werden:

  • als Argument für Löschmethoden, um in Sammlungen Elemente zu spezifizieren, die gelöscht werden sollen oder nach denen gefiltert werden soll

  • bei den Default-Methoden der Predicate-Schnittstelle selbst, um Prädikate zu verknüpfen

  • bei regulären Ausdrücken; auf einem Pattern liefern asPredicate() und asMatchPredicate() (ab Java 11) ein Predicate für Tests.

  • in der Stream-API, bei der Objekte beim Durchlaufen des Stroms über ein Prädikat identifiziert werden, um sie etwa auszufiltern

[zB]  Beispiel

Lösche aus einer Liste mit beliebigen Zeichen alle heraus, die Ziffern sind:

Predicate<Character> isDigit = Character::isDigit;

List<Character> list = new ArrayList<>( Arrays.asList( 'a', '1' ) );

list.removeIf( isDigit );

Auf diese Weise steht nicht die Schleife, sondern das Löschen im Vordergrund.

Default-Methoden von Predicate

Es gibt eine Reihe von Default-Methoden, die die funktionale Schnittstelle Predicate anbietet:

interface java.util.function.Predicate<T>
  • default Predicate<T> negate()

    Liefert vom aktuellen Prädikat eine Negation. Implementiert als return t -> ! test(t);.

  • static <T> Predicate<T> not(Predicate<? super T> target)

    Liefert target.negate(). Neu seit Java 11.

  • default Predicate<T> and(Predicate<? super T> p)

  • default Predicate<T> or(Predicate<? super T> p)

    Verknüpft das aktuelle Prädikat mit einem anderen Prädikat mit einem logischen Und/Oder.

  • static <T> Predicate<T> isEqual(Object targetRef)

    Liefert ein neues Prädikat, das einen Gleichwertigkeitstest mit targetRef vornimmt, im Grunde return ref -> Objects.equals(ref, targetRef).

[zB]  Beispiel

Lösche aus einer Liste mit Zeichen diejenigen, die keine Ziffern sind:

Predicate<Character> isDigit = Character::isDigit;

Predicate<Character> isNotDigit = isDigit.negate();

List<Character> list = new ArrayList<>( Arrays.asList( 'a', '1' ) );

list.removeIf( isNotDigit );

// alternativ: list.removeIf( Predicate.not( isDigit ) );
 

Zum Seitenanfang

13.5.4    Funktionen über die funktionale Schnittstelle java.util.function.Function Zur vorigen ÜberschriftZur nächsten Überschrift

Funktionen im Sinne der funktionalen Programmierung können in verschiedenen Bauarten vorkommen: mit Parameterliste/Rückgabe oder ohne. Doch im Grunde sind es Spezialformen, und die funktionale Schnittstelle java.util.function.Function ist die allgemeinste, die zu einem Argument ein Ergebnis liefert.

interface java.util.function.Function<T,R>
  • R apply(T t)

    Wendet eine Funktion an und liefert zur Eingabe t eine Rückgabe.

[zB]  Beispiel

Eine Funktion zur Bestimmung des Absolutwerts:

Function<Double,Double> abs = a -> Math.abs( a );  // alternativ Math::abs

System.out.println( abs.apply( -12. ) ); // 12.0

Auch bei Funktionen ergibt sich für das API-Design ein Spannungsfeld, denn im Grunde müssen »Funktionen« nun gar nicht mehr als Methoden angeboten werden, sondern Klassen könnten sie auch als Function-Objekte anbieten. Doch da Methodenreferenzen problemlos die Brücke von Methodennamen zu Objekten schlagen, fahren Entwickler mit klassischen Methoden ganz gut.

Der Typ Function in der API

Die Stream-API ist der größte Nutznießer des Function-Typs. Es finden sich einige wenige Beispiele bei Objektvergleichen (Comparator), im Paket für Nebenläufigkeiten und bei Assoziativspeichern. Im Abschnitt über die Stream-API werden wir daher viele weitere Beispiele kennenlernen.

[zB]  Beispiel

Ein Assoziativspeicher soll als Cache realisiert werden, der zu Dateinamen den Inhalt assoziiert. Ist zu dem Schlüssel (dem Dateinamen) noch kein Inhalt vorhanden, soll der Dateiinhalt gelesen und in den Assoziativspeicher gelegt werden.

Listing 13.12     src/main/java/com/tutego/insel/lambda/FileCache.java, Ausschnitt

class FileCache {

private Map<String,byte[]> map = new HashMap<>();

public byte[] getContent( String filename ) {

return map.computeIfAbsent( filename, file -> {

try {

return Files.readAllBytes( Paths.get( file ) );

} catch ( IOException e ) { throw new UncheckedIOException( e ); }

} );

}

}

Auf die Methode kommen wir in Kapitel 18, »Einführung in Datenstrukturen und Algorithmen«, noch einmal zurück, das Beispiel soll nur eine Vorstellung davon geben, dass Funktionen an andere Funktionen übergeben werden – hier eine Function<String,byte[]) an computeIfAbsent(…). Sobald an den Datei-Cache der Aufruf getContent(String) geht, wird dieser die Map fragen, und wenn diese zu dem Schlüssel keinen Wert hat, wird sie den Lambda-Ausdruck auswerten, um zu dem Dateinamen den Inhalt zu liefern.

Getter-Methoden als Function über Methodenreferenzen

Methodenreferenzen gehören zu den syntaktisch knappsten Sprachmitteln von Java. In Kombination mit Gettern ist ein Muster abzulesen, das oft in Code zu sehen ist. Zunächst noch einmal zur Wiederholung von Function und der Nutzung bei Methodenreferenzen:

Function<String,String> func1a = (String s) -> s.toUpperCase();

Function<String,String> func1b = String::toUpperCase;

Function<Point,Double> func2a = (Point p) -> p.getX();

Function<Point,Double> func2b = Point::getX;

System.out.println( func1b.apply( "jocelyn" ) ); // JOCELYN

System.out.println( func2b.apply( new Point( 9, 0 ) ) ); // 9.0

Dass Function auf die gegebene Methodenreferenz passt, ist auf den ersten Blick unverständlich, da die Signaturen von toUpperCase() und getX() keinen Parameter deklarieren, also im üblichen Sinne keine Funktionen sind, in die etwas hineinkommt und aus denen wieder etwas herauskommt. Wir haben es hier aber mit einem speziellen Fall zu tun, denn die in der Methodenreferenz genannten Methoden sind

  • nichtstatisch – wie Math::max –, und

  • es ist auch keine Referenz – wie System.out::print – im Spiel,

sondern hier wird der Compiler eine Objektmethode auf genau dem Objekt aufrufen, das als erstes Argument der funktionalen Schnittstelle übergeben wurde. (Diesen Satz bitte zweimal lesen!)

Damit ist Function ein praktischer Typ in allen Szenarien, in denen irgendwie über Getter Zustände erfragt werden, wie es etwa bei einem Comparator öfter vorkommt. Hier ist eine statische Methode (die Generics einmal ausgelassen) Comparator<…> Comparator.comparing(Function<…> keyExtractor) sehr nützlich.

[zB]  Beispiel

Besorge eine Liste von Paketen, die vom Klassenlader zugänglich sind, und sortiere sie nach Namen:

List<Package> list = Arrays.asList( Package.getPackages() );

Collections.sort( list, Comparator.comparing( Package::getName ) );

System.out.println( list ); // [package java.io, ... sun.util.locale ...

Default-Methoden in Function

Die funktionale Schnittstelle schreibt nur eine Methode apply(…) vor, deklariert jedoch noch drei zusätzliche Default-Methoden:

interface java.util.function.Function<T,R>
  • static <T> Function<T,T> identity()

    Liefert eine Funktion, die immer die Eingabe als Ergebnis liefert, implementiert als return t -> t;.

  • default <V> Function<T,V> andThen(Function<? super R,? extends V> after)

    Entspricht t -> after.apply(apply(t)).

  • default <V> Function<V,R> compose(Function<? super V,? extends T> before)

    Entspricht v -> apply(before.apply(v)).

identity() scheint auf den ersten Blick sinnlos zu sein, erfüllt aber einen Zweck, wenn in der API eine Function erforderlich ist, bei der sich nichts verändern, die Eingabe also die Ausgabe sein soll.

Die Methoden andThen(…) und compose(…) unterscheiden sich darin, in welcher Reihenfolge die Funktionen aufgerufen werden. Das Gute ist, dass die Parameternamen (before, after) klarmachen, was hier in welcher Reihenfolge aufgerufen wird.

[zB]  Beispiel
Function<String, String> f1 = s -> "~" + s + "~";

Function<String, String> f2 = s -> "<" + s + ">";

System.out.println( f1.andThen( f2 ).apply( ":)" ) ); // <~:)~>

System.out.println( f2.andThen( f1 ).apply( ":)" ) ); // ~<:)>~

System.out.println( f1.compose( f2 ).apply( ":)" ) ); // ~<:)>~

System.out.println( f2.compose( f1 ).apply( ":)" ) ); // <~:)~>

Function versus Consumer/Predicate

Im Grunde lässt sich alles als Function darstellen, denn

  • ein Consumer<T> lässt sich auch als Function<T,Void> verstehen (es geht etwas hinein, aber nichts kommt heraus),

  • ein Predicate<T> als Function<T,Boolean> und

  • ein Supplier<T> als Function<Void,T>.

Dennoch erfüllen diese speziellen Typen ihren Zweck, denn je genauer der Typ ist, desto besser.

Function ist kein Basistyp von Consumer oder Supplier, da sich »keine Rückgabe« oder »kein Parameter« nicht durch einen generischen Typ ausdrücken kann, der bei Function<T,R> möglich wäre.

UnaryOperator

Es gibt auch eine weitere Schnittstelle im java.util.function-Paket, die Function spezialisiert, und zwar UnaryOperator. Ein UnaryOperator ist eine spezielle Function, bei der die Typen für »Eingang« und »Ausgang« gleich sind.

interface java.util.function.UnaryOperator<T>

extends Function<T,T>
  • static <T> UnaryOperator<T> identity()

    Liefert den Identitätsoperator, der alle Eingaben auf die Ausgaben abbildet.

Die generischen Typen machen deutlich, dass der Typ des Methodenparameters gleich dem Ergebnistyp ist. Bis auf identity() gibt es keine weitere Funktionalität; die Schnittstelle dient lediglich zur Typdeklaration.

An einigen Stellen der Java-Bibliothek kommt dieser Typ auch vor, etwa bei der Methode replaceAll(UnaryOperator) der List-Typen.

[zB]  Beispiel

Verdopple jeden Eintrag in der Liste:

List<Integer> list = Arrays.asList( 1, 2, 3 );

list.replaceAll( e -> e * 2 );

System.out.println( list ); // [2, 4, 6]
 

Zum Seitenanfang

13.5.5    Ein bisschen Bi … Zur vorigen ÜberschriftZur nächsten Überschrift

»Bi« ist eine bekannte lateinische Vorsilbe für »zwei«, was übertragen auf die Typen aus dem Paket java.util.function bedeutet, dass statt eines Arguments zwei übergeben werden können.

Typ

Schnittstelle

Operation

Konsument

Consumer<T>

void accept(T t)

BiConsumer<T,U>

void accept(T t, U u)

Funktion

Function<T,R>

R apply(T t)

BiFunction<T,U,R>

R apply(T t, U u)

Prädikat

Predicate<T>

boolean test(T t)

BiPredicate<T,U>

boolean test(T t, U u)

Tabelle 13.13     Ein- und Zwei-Argument-Methoden im Vergleich

Die Bi-Typen haben mit den Nicht-Bi-Typen keine Typbeziehung.

BiConsumer

Der BiConsumer deklariert die Methode accept(T, U) mit zwei Parametern, die jeweils unterschiedliche Typen tragen können. Haupteinsatzpunkt des Typs in der Java-Standardbibliothek sind Assoziativspeicher, die Schlüssel und Werte an accept(…) übergeben. So deklariert Map die Methode:

interface java.util.Map<K,V>
  • default void forEach(BiConsumer<? super K,? super V> action)

    Läuft den Assoziativspeicher ab und ruft auf jedem Schlüssel-Wert-Paar die accept(…)-Methode vom übergebenen BiConsumer auf.

[zB]  Beispiel

Gib die Temperaturen der Städte aus:

Map<String,Integer> map = new HashMap<>();

map.put( "Manila", 25 );

map.put( "Leipzig", -5 );

map.forEach( (k, v) -> System.out.printf("%s hat %d Grad%n", k, v) );

Ein BiConsumer besitzt eine Default-Methode andThen(…), wie auch der Consumer sie zur Verkettung deklariert.

interface java.util.function.BiConsumer<T,U>
  • default BiConsumer<T,U> andThen(BiConsumer<? super T,? super U> after)

    Verknüpft den aktuellen BiConsumer mit after zu einem neuen BiConsumer.

BiFunction und BinaryOperator

Eine BiFunction ist eine Funktion mit zwei Argumenten, während eine normale Function nur ein Argument annimmt.

[zB]  Beispiel

Nutzung von Function und BiFunction mit Methodenreferenzen:

Function<Double,Double> sign = Math::abs;

BiFunction<Double,Double,Double> max = Math::max;

Die Java-Bibliothek greift viel öfter auf Function zurück als auf BiFunction. Der häufigste Einsatz findet sich in der Standardbibliothek rund um Assoziativspeicher, bei denen Schlüssel und Wert an eine BiFunction übergeben werden.

[zB]  Beispiel

Konvertiere alle assoziierten Werte einer HashMap in Großbuchstaben:

Map<Integer,String> map = new HashMap<>();

map.put( 1, "eins" ); map.put( 2, "zwEi" );

System.out.println( map ); // {1=eins, 2=zwEi}

BiFunction<Integer,String,String> func = (k, v) -> v.toUpperCase();

map.replaceAll( func );

System.out.println( map ); // {1=EINS, 2=ZWEI}

Ist bei einer Function der Typ derselbe, bietet die Java-API dafür den spezielleren Typ UnaryOperator. Sind bei einer BiFunction alle drei Typen gleich, bietet sich BinaryOperator an – zum Vergleich:

  • interface UnaryOperator<T> extends Function<T,T>

  • interface BinaryOperator<T> extends BiFunction<T,T,T>

[zB]  Beispiel

BiFunction und BinaryOperator:

BiFunction<Double,Double,Double> max1 = Math::max;

BinaryOperator<Double> max2 = Math::max;

BinaryOperator spielt bei sogenannten Reduktionen eine große Rolle, wenn zum Beispiel aus zwei Werten einer wird, wie bei Math.max(…); auch das beleuchtet der Abschnitt über die Stream-API später genauer.

Die Schnittstelle BiFunction deklariert genau eine Default-Methode:

interface java.util.function.BiFunction<T,U,R>

extends Function<T,T>
  • default <V> BiFunction<T,U,V> andThen(Function<? super R,? extends V> after)

BinaryOperator dagegen wartet mit zwei statischen Methoden auf:

public interface java.util.function.BinaryOperator<T>

extends BiFunction<T,T,T>
  • static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator)

  • static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator)

    Liefert einen BinaryOperator, der das Maximum/Minimum bezüglich eines gegebenen comparator liefert.

BiPredicate

Ein BiPredicate testet zwei Argumente und verdichtet sie zu einem Wahrheitswert. Wie Predicate deklariert auch BiPredicate drei Default-Methoden and(…), or(…) und negate(…), wobei natürlich eine statische isEqual(…)-Methode wie bei Predicate in BiPredicate fehlt. Für BiPredicate gibt es in der Java-Standardbibliothek nur eine Verwendung bei einer Methode zum Finden von Dateien – der Gebrauch ist selten, zudem ja auch ein Prädikat immer eine Funktion mit boolean-Rückgabe ist, sodass es eigentlich für diese Schnittstelle keine zwingende Notwendigkeit gibt.

[zB]  Beispiel

Bei Bi* und zwei Argumenten hört im Übrigen die Spezialisierung auf, es gibt keine Typen Tri*, Quad* … Das ist in der Praxis auch nicht nötig, denn zum einen kann oftmals eine Reduktion stattfinden – so ist etwa max(1, 2, 3) gleich max(1, max(2, 3)) –, und zum anderen kann auch der Parametertyp eine Sammlung sein, wie in Function<List<Integer>, Integer> max.

 

Zum Seitenanfang

13.5.6    Funktionale Schnittstellen mit Primitiven Zur vorigen ÜberschriftZur nächsten Überschrift

Die bisher vorgestellten funktionalen Schnittstellen sind durch die generischen Typparameter sehr flexibel, aber was fehlt, sind Signaturen mit Primitiven – Java hat das »Problem«, dass Generics nur mit Referenztypen funktionieren, nicht aber mit primitiven Typen. Aus diesem Grund gibt es von fast allen Schnittstellen aus dem java.util.function-Paket vier Versionen: eine generische für beliebige Referenzen sowie Versionen für die Typen int, long und double. Die API-Designer wollten gerne die Wrapper-Typen außen vor lassen und gewisse primitive Typen unterstützen, auch aus Performance-Gründen, um nicht immer ein Boxing durchführen zu müssen.

Tabelle 13.14 gibt einen Überblick über die funktionalen Schnittstellen, die alle keinerlei Vererbungsbeziehungen zu anderen Schnittstellen haben.

funktionale Schnittstelle

Funktionsdeskriptor

*Supplier

BooleanSupplier

boolean getAsBoolean()

IntSupplier

int getAsInt()

LongSupplier

long getAsLong()

DoubleSupplier

double getAsDouble()

*Consumer

IntConsumer

void accept(int value)

LongConsumer

void accept(long value)

DoubleConsumer

void accept(double value)

ObjIntConsumer<T>

void accept(T t, int value)

ObjLongConsumer<T>

void accept(T t, long value)

ObjDoubleConsumer<T>

void accept(T t, double value)

*Predicate

IntPredicate

boolean test(int value)

LongPredicate

boolean test(long value)

DoublePredicate

boolean test(double value)

*Function

DoubleToIntFunction

int applyAsInt(double value)

IntToDoubleFunction

double applyAsDouble(int value)

LongToIntFunction

int applyAsInt(long value)

IntToLongFunction

long applyAsLong(int value)

DoubleToLongFunction

long applyAsLong(double value)

LongToDoubleFunction

double applyAsDouble(long value)

IntFunction<R>

R apply(int value)

LongFunction<R>

R apply(long value)

DoubleFunction<R>

R apply(double value)

ToIntFunction<T>

int applyAsInt(T t)

ToLongFunction<T>

long applyAsLong(T t)

ToDoubleFunction<T>

double applyAsDouble(T t)

ToIntBiFunction<T,U>

int applyAsInt(T t, U u)

ToLongBiFunction<T,U>

long applyAsLong(T t, U u)

ToDoubleBiFunction<T,U>

double applyAsDouble(T t, U u)

*Operator

IntUnaryOperator

int applyAsInt(int operand)

LongUnaryOperator

long applyAsLong(long operand)

DoubleUnaryOperator

double applyAsDouble(double operand)

IntBinaryOperator

int applyAsInt(int left, int right)

LongBinaryOperator

long applyAsLong(long left, long right)

DoubleBinaryOperator

double applyAsDouble(double left, double right)

Tabelle 13.14     Spezielle funktionale Schnittstellen für primitive Werte

Statische und Default-Methoden

Einige generisch deklarierte funktionale Schnittstellen-Typen besitzen Default-Methoden bzw. statische Methoden, und Ähnliches findet sich auch bei den primitiven funktionalen Schnittstellen wieder:

  • Die *Consumer-Schnittstellen deklarieren default *Consumer andThen(*Consumer after), aber nicht die Obj*Consumer-Typen. Sie besitzen keine Default-Methode.

  • Die *Predicate-Schnittstellen deklarieren:

    • default *Predicate negate()

    • default *Predicate and(*Predicate other)

    • default *Predicate or(*Predicate other)

  • Jeder *UnaryOperator besitzt:

    • default *UnaryOperator andThen(*UnaryOperator after)

    • default *UnaryOperator compose(*UnaryOperator before)

    • static *UnaryOperator identity()

  • BinaryOperator hat zwei statische Methoden maxBy(…) und minBy(…), die es nicht in der primitiven Version *BinaryOperator gibt, da kein Comparator bei primitiven Vergleichen nötig ist.

  • Die *Supplier-Schnittstellen deklarieren keine statischen Methoden oder Default-Methoden, genauso wie es auch Supplier nicht tut.

 


Ihre Meinung?

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de

<< zurück
 Zum Rheinwerk-Shop
Zum Rheinwerk-Shop: Java ist auch eine Insel Java ist auch eine Insel

Jetzt Buch bestellen


 Buchempfehlungen
Zum Rheinwerk-Shop: Captain CiaoCiao erobert Java

Captain CiaoCiao erobert Java




Zum Rheinwerk-Shop: Algorithmen in Java

Algorithmen in Java




Zum Rheinwerk-Shop: Spring Boot 3 und Spring Framework 6

Spring Boot 3 und Spring Framework 6




Zum Rheinwerk-Shop: Java SE 9 Standard-Bibliothek

Java SE 9 Standard-Bibliothek




 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und in die Schweiz

InfoInfo




Copyright © Rheinwerk Verlag GmbH 2024

Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.

Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

 

[Rheinwerk Computing]



Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de



Cookie-Einstellungen ändern