Streams und Collections in Java 8

Seite 3: Funktionale Interfaces

Inhaltsverzeichnis

Java 8 bringt im Package java.util.function eine Reihe funktionaler Interfaces mit. Der Beitrag konzentriert sich auf:

  • Function zur Abbildung von Funktionen
  • Consumer zur Abbildung von Prozeduren mit Seiteneffekten
  • Predicate zur Abbildung von Prädikaten

Das funktionale Interface Function<T,R> transformiert einen Eingabetyp T in einen Ausgabetyp R. Ein typisches Nutzungsszenario sind Mapper, die Eingabedaten in ein Ausgabedatum transformieren, konvertieren, berechnen. Eine solche Funktion kam im einleitenden Beispiel bei der Rabattierung als mapToDouble(price -> price * 0.9) zum Einsatz. Die Definition von Function sieht wie folgender Code-Ausschnitt aus:

@FunctionalInterface
public interface Function<T, R>
{
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
... }

Eine Anwendung könnte so aussehen:

// Definition
Function<Integer, Integer> square = n -> n*n;
// Nutzung (hier mit JUnit-API)
assertEquals(9, square.apply(3));

square ist eine Abbildung von Integer nach Integer und liefert das Quadrat der übergebenen Zahl zurück. Eine Anwendung mit Function.apply ist – wie in Zeile 4 gezeigt – möglich. Besser und im Sinne von "Code as Data" ist aber, die Function<T,R> als Lambda-Ausdruck an die generische Funktion Stream.map zu übergeben.

Eine Function wird als Lambda-Ausdruck an die generische map-Methode mit Daten ĂĽbergeben (Abb. 2).


Consumer<T> ist eine Funktion, die den Programmzustand durch Seiteneffekte verändert. Ein typisches Nutzungsszenario ist die Verarbeitung von Collections mit forEach. Die Definition von Consumer zeigt der folgende Code-Ausschnitt:

@FunctionalInterface
public interface Consumer<T>
{
/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
public void accept(T t);
... }

Das nächste Beispiel zeigt Definition und Anwendung eines Consumer:

Consumer<String> out = s -> System.out.println(s);
out.accept("Test");

Auch hier gilt wieder: Consumer<T>.accept lässt sich direkt (Zeile 2) aufrufen, im Sinne von "Code as Data" ist es jedoch besser, den Consumer<T> als Lambda-Ausdruck an Stream.foreach zu übergeben:

Ein Consumer wird als Lambda-Ausdruck an die generische forEach-Funktion mit Daten ĂĽbergeben (Abb. 3).

Predicate<T> ist eine Funktion, die ein Objekt vom Typ T einem logischen Testkriterium unterzieht und damit ein Spezialfall von Function mit RĂĽckgabeergebnis vom Typ boolean. Ein typisches Nutzungsszenario fĂĽr Predicate<T>-Instanzen ist die Filterung von Collections. Ein solches Predicate
enthielt bereits das einleitende Beispiel bei der Filterung als filter(price -> price >= 42). Die Definition von Predicate sieht wie folgt aus:

@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(...) { ... }
default Predicate<T> negate() { ... }
default Predicate<T> or(...) { ... }
default Predicate<T> xor(...) { ... }
}

Predicate kodiert also ein Testkriterium mit RĂĽckgabewert als boolesches Testergebnis. Man kann Instanzen ĂĽber booleschen Operatoren verknĂĽpfen, um komplexere Testkriterien zu erhalten, etwa wie folgt:

Predicate<Integer> isEven = n -> n % 2 == 0;
assertTrue(isEven.test(2));

Predicate<Integer> isOdd = isEven.negate();
assertFalse(isOdd.test(2));

Predicate<Integer> isPositive = n -> n > 0;
Predicate<Integer> isEvenAndPositive = isEven.and(isPositive);
assertTrue(isEvenAndPositive.test(2));
assertFalse(isEvenAndPositive.test(0));

Predicate<Integer> isZero = n -> n == 0;
Predicate<Integer> isZeroOrPositive = isPositive.or(isZero);
assertTrue(isZeroOrPositive.test(0));

Auch Predicate<T>.test lässt sich direkt aufrufen, empfehlenswert ist aber auch hier die Übergabe des Predicate<T> als Lambda-Ausdruck an Stream.filter.

Ein Predicate wird als Lambda-Ausdruck an die generische Filter-Funktion mit Daten ĂĽbergeben (Abb. 4).