Funktionale Interfaces

  • Interfaces mit genau einer abstrakten Methode
  • Ausnahme: Überschreiben von konkreten Methoden der Klasse Object (z.B. equals)
  • Werden mit @FunctionalInterface annotiert
  • Ziel: Wird zu Lambda-Ausdruck

Lambda-Ausdrücke

Syntax

( Parameterliste ) -> { Anweisungen }
  • Die Parameter können sowohl mit, als auch ohne Typen angegeben werden
  • Bei einem Parameter können die runden Klammern weggelassen werden
  • Wenn im Codeblock nur eine Anweisung steht, können die geschwiffenen Klammern weggelassen werden
  • Lambda-Ausdrücke können auch Methodenreferenzen sein, z.B. System.out::println

Beispiele

() -> 5
x -> 2 * x
(x, y) -> x + y
(int x, int y) -> x + y
String::compareTo

Anwendung

  • Lambda-Austrücke können überall dort verwendet werden, wo funktionale Interfaces erwartet werden.
  • Der Compiler ersetzt den Lambda-Ausdruck durch das Objekt einer anonymen Klasse (= das funktionale Interface mit implementierter Funktion duch den Lambda-Ausdruck), die im Kontext erwartet wird (Zieltyp).

Funktionsweise

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
 
// Gegeben ist dieser Lambda-Ausdruck:
numbers.forEach(number -> System.out.println(number));
 
// Die vollständige Methodensignatur von forEach lautet:
void forEach(Consumer<? super E> action)
 
// Daher wird der Lambda-Ausdruck wie folgt ersetzt:
numbers.forEach(new Consumer<Integer>() {
    @Override
    public void accept(Integer number) {
        System.out.println(number);
    }
});

Scope

  • Lambda-Ausdrücke können auf die lokalen Variablen des umgebenden Blocks zugreifen, aber nur, wenn diese final oder effectively final sind
  • Lambda-Ausdrücke haben keinen eigenen Sichtbarkeitsbereich, daher müssen sich die Namen der Lambda-Parameter von den Variablennamen im umgebenden Block unterscheiden.

Das Paket java.util.function

Consumer

Ein oder mehrere Inputs, kein Output.

  • Mit einem Argument:
    • Consumer<T>, IntConsumer, LongConsumer, DoubleConsumer
    • Lambda: T -> void, bzw. int -> void, …
  • Mit zwei Argumenten:
    • BiConsumer<T,U> - Lambda: (T, U ) -> void
    • ObjIntConsumer<T> - Lambda: (T, int ) -> void
    • ObjLongConsumer<T> - s.o.
    • ObjDoubleConsumer<T> - s.o.
// Beispiel:
Consumer<String> c = (String s) -> System.out.println(s);
c.accept("Hallo");

Supplier

Kein Input, nur ein Output.

  • Supplier<T>, IntSupplier, LongSupplier, DoubleSupplier, BooleanSupplier
  • Lambda: () -> T, bzw. () -> int, …
// Beispiel:
Supplier<Random> randomSupplier = () -> new Random(4711);
IntSupplier randomIntSupplier = () -> new Random().nextInt();

Function

Wandelt einen oder zwei Inputs in einen Output um

  • Function<T,R>, IntFunction<R>, …
  • ToIntFunction<T>, ToLongFunction<T>, …
  • IntToLongFunction, IntToDoubleFunction, LongToIntFunction, …
  • BiFunction<T,U,R>, ToIntBiFunction<T,U>, ToLongBiFunction<T,U>, …
// Beispiel:
ToIntFunction<String> f1 = string -> string.length();
IntFunction<double[]> f2 = länge -> new double[länge];

Operator

Funktionen, bei denen Input und Output vom selben Typ sind

  • UnaryOperator<T>, IntUnaryOperator, … Lambda: T -> T
  • BinaryOperator<T>, IntBinaryOperator, … Lambda: (T, T) -> T
// Beispiel: 
IntBinaryOperator add = (a, b) -> a + b;

Predicate

Funktionen, bei denen Output vom Typ boolean ist

  • Predicate<T>, IntPredicate, …
  • BiPredicate<T,U>. Dieses hat keine primitiven Varianten.
// Beispiel:
IntPredicate p = c -> Character.isAlphabetic(c);

Lambdas und Exceptions

Checked Exceptions müssen direkt vom Lambda verarbeitet werden, da das Lambda (aka die Funktion im funktionalen Interface) keine throws-Klausel hat:

Predicate<Path> empty = datei -> {
    try {
        return Files.size(datei) == 0;
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
};

Hier wird die checked IoException in eine unchecked RuntimeExcption eingewickelt, die auch ohne throws weitergegeben werden kann.

Beispiele

Iteration über List

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.forEach(number -> System.out.println(number));

Iteration über Map

Map<Integer, String> students = ...;
students.forEach((key, value) ->
    System.out.println(key + ": " + value));

Iteration mit System.out::println

List<String> namen = List.of("Clara", "Raphael", "Florian");
namen.forEach(System.out::println);

Kurzform von Comparatoren

Arrays.sort(w, new Comparator<>() {
    @Override public int compare(String s1, String s2) {
        return Integer.compare(s1.length(), s2.length());
    }
});

Dieser Code lässt sich mit Lambda-Ausdrücken vereinfachen:

Arrays.sort(w, (s1, s2) -> Integer.compare(s1.length(), s2.length()));

Und dafür gibt es dann wieder eine schon definierte Methode (zwar kein Lambda-Ausdruck mehr aber trotzdem wissenswert):

Arrays.sort(w, Comparator.comparingInt(String::length));

Der Container Atomic Integer

Funktionale Interfaces anderswo

In Arrays-Mathoden

<T> void setAll(T[] array, IntFunction<T> generator)
  • Setzt array auf Werte, die generator liefert.
  • Dafür übergibt setAll an den generator den Index des Elements.
// Beispiel: Befülle Array mit Quadratwurzeln vom Index
double[] a = new double[5];
Arrays.setAll(a, Math::sqrt);
System.out.println(Arrays.toString(a));
// Ausgabe: 0.0, 1.0, 1.41421, 1.73205, 2.0
// Beispiel: Wandle alle Elemente zu Uppercase um:
String[] namen = {"Clara", "Raphael", "Florian", "Julia"};
Arrays.setAll(namen, i -> namen[i].toUpperCase());
System.out.println(Arrays.toString(namen));
// Ausgabe: CLARA, RAPHAEL, FLORIAN, JULIA

In Comparator-Methoden

  • Comparatoren haben hilfreiche statische und default-Methoden, die das Coden erleichtern:
// Extrahiert mit hilfe von keyExtractor einen int-Schlüssel
// aus T, der dann zum vergleichen verwendet wird.
static <T> Comparator<T> comparingInt(ToIntFunction<T> keyExtractor)
 
// Liefert einen Comparator, der im gleichheitsfall des ersten
// den zweiten verwendet.
default Comparator<T> thenComparing(Comparator<T> other)
 
// Liefert einen Comparator, der im Gleichheitsfall nach 
// dem extrahierten int-Schlüssel vergleicht.
default Comparator<T> thenComparingInt(ToIntFunction<T> keyExtractor)

Anwendung:

Arrays.sort(namen, (s, t) -> 
            Integer.compare(s.length(), t.length())
);
// wird zu:
Arrays.sort(namen, Comparator.comparingInt(string -> 
                                           string.length())
);
// dafür gibt es auch wieder einen shorthand:
Arrays.sort(namen, Comparator.comparingInt(String::length));
// mit Hilfe von statischen Imports lässt sich der Klassenname
// auch noch wegkürzen:
Arrays.sort(namen, comparingInt(String::length));
 
// Beispiel mit thenComparing:
Arrays.sort(a, comparingInt(String::length)
               .thenComparing(naturalOrder())
);