1. Verhalten statt Daten übergeben
Warum überhaupt eine Funktion als Parameter übergeben?
Stellen Sie sich vor: Sie haben eine Methode, die eine Liste sortiert. Woher weiss sie, wie genau sortiert werden soll? Nach Alphabet? Nach Länge? Nach Geburtsdatum? Man könnte für jeden Fall eine eigene Methode schreiben, aber das wird schnell zu nerviger Copy-&-Paste-Hölle.
Stattdessen ermöglicht Java, Verhalten zu übergeben (also eine Funktion oder Lambda), die festlegt, wie sortiert, gefiltert, transformiert und so weiter wird. Das macht den Code flexibel und wiederverwendbar.
Beispiel: Sortieren mit Comparator
List<String> names = List.of("Anna", "Boris", "Vika");
// Verhalten übergeben: wie Elemente verglichen werden (nach String-Länge)
names.stream()
.sorted((a, b) -> a.length() - b.length())
.forEach(System.out::println);
Beispiel: Filtern mit Predicate
List<String> names = List.of("Anna", "Boris", "Vika");
// Verhalten übergeben: wen behalten (Name ist länger als 4 Zeichen)
names.stream()
.filter(name -> name.length() > 4)
.forEach(System.out::println);
In beiden Fällen haben Sie die Logik nicht „hart verdrahtet“, sondern der Methode ein Stück Verhalten gegeben, das sie auf jedes Element anwendet.
Vorteile: weniger Duplizierung, mehr Flexibilität
Statt dutzender Methoden mit ähnlichem Code, aber unterschiedlicher Logik schreiben Sie eine universelle Methode, die das „Was tun“ als Funktion annimmt. Das spart Zeit, reduziert Fehler und macht den Code leichter testbar.
2. Syntax der Funktionsübergabe
Lambda-Ausdrücke als Argumente
Der häufigste Weg – direkt an der Aufrufstelle eine Lambda mit Pfeil -> schreiben:
list.forEach(item -> System.out.println(item));
Oder sogar kürzer, wenn es bereits eine passende Methode gibt — eine Methodenreferenz (::):
list.forEach(System.out::println);
Methodenreferenzen (method references)
Wenn es bereits eine passende Methode gibt, kann man sie als Referenz :: übergeben:
// Normale Methode
public static boolean isLongName(String name) {
return name.length() > 4;
}
// Methodenreferenz übergeben
names.stream()
.filter(MyClass::isLongName)
.forEach(System.out::println);
Das funktioniert, wenn die Methodensignatur der erwarteten im funktionalen Interface entspricht (Predicate<T>, Function<T, R>, Comparator<T> usw.).
3. Beispiele aus der Standardbibliothek
Collections.sort und Comparator
List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Vika"));
// Sortieren nach Namenslänge
names.sort((a, b) -> a.length() - b.length());
System.out.println(names); // [Vika, Anna, Boris]
Stream.filter und Predicate
List<String> names = List.of("Anna", "Boris", "Vika");
// Nur Namen behalten, die mit 'V' beginnen
names.stream()
.filter(name -> name.startsWith("V"))
.forEach(System.out::println); // Vika
Stream.map und Function
List<String> names = List.of("Anna", "Boris", "Vika");
// Namen in Großbuchstaben umwandeln
names.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
Optional.ifPresent und Consumer
Optional<String> opt = Optional.of("Hallo!");
// Wenn ein Wert vorhanden ist, ausgeben
opt.ifPresent(s -> System.out.println("Im String: " + s));
4. Praxis: eigene Methoden mit Funktionsparametern schreiben
Zeit, das Wissen in die Praxis umzusetzen! Schreiben wir einige Methoden, die eine Funktion als Parameter annehmen und sie intern verwenden.
Beispiel 1: Methode zur Verarbeitung von Listenelementen (forEach auf unsere Art)
Angenommen, wir haben eine Liste von Benutzern (User). Wir wollen für jeden Benutzer eine Aktion ausführen — z. B. den Namen ausgeben, eine E‑Mail senden, Boni gutschreiben usw. Anstatt die Aktion fest zu „verdrahten“, übergeben wir sie als Parameter — Consumer<User>!
import java.util.List;
import java.util.function.Consumer;
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
}
public class UserProcessor {
// Methode, die einen Consumer<User> annimmt
public static void processUsers(List<User> users, Consumer<User> action) {
for (User user : users) {
action.accept(user);
}
}
public static void main(String[] args) {
List<User> users = List.of(
new User("Anna", 25),
new User("Boris", 30),
new User("Vika", 22)
);
// Alle Benutzernamen ausgeben
processUsers(users, user -> System.out.println("Name: " + user.name));
// Bonus gutschreiben (hier nur ausgeben)
processUsers(users, user -> System.out.println(user.name + " hat einen Bonus erhalten!"));
}
}
Beispiel 2: Methode zum Filtern von Elementen (Predicate)
Wir schreiben eine Methode, die nur die Benutzer zurückgibt, die einer Bedingung entsprechen (z. B. nur Volljährige):
import java.util.List;
import java.util.ArrayList;
import java.util.function.Predicate;
public class UserFilter {
public static List<User> filterUsers(List<User> users, Predicate<User> condition) {
List<User> result = new ArrayList<>();
for (User user : users) {
if (condition.test(user)) {
result.add(user);
}
}
return result;
}
public static void main(String[] args) {
List<User> users = List.of(
new User("Anna", 25),
new User("Boris", 17),
new User("Vika", 22)
);
// Nur Volljährige filtern
List<User> adults = filterUsers(users, user -> user.age >= 18);
adults.forEach(user -> System.out.println(user.name)); // Anna, Vika
}
}
Beispiel 3: Abbildungs-/Mapping-Methode (Function)
Nun eine Methode, die aus der Benutzerliste eine Liste ihrer Namen macht (oder beliebige andere Transformationen):
import java.util.List;
import java.util.ArrayList;
import java.util.function.Function;
public class UserMapper {
public static <R> List<R> mapUsers(List<User> users, Function<User, R> mapper) {
List<R> result = new ArrayList<>();
for (User user : users) {
result.add(mapper.apply(user));
}
return result;
}
public static void main(String[] args) {
List<User> users = List.of(
new User("Anna", 25),
new User("Boris", 17),
new User("Vika", 22)
);
// Liste der Namen erhalten
List<String> names = mapUsers(users, user -> user.name);
System.out.println(names); // [Anna, Boris, Vika]
// Liste der Alter erhalten
List<Integer> ages = mapUsers(users, user -> user.age);
System.out.println(ages); // [25, 17, 22]
}
}
Beispiel 4: Generator-Methode (Supplier)
Manchmal muss man einen Wert „auf Abruf“ erhalten — z. B. eine Zufallszahl generieren, ein Objekt erzeugen, die aktuelle Zeit holen. Dafür eignet sich das Interface Supplier<T>.
import java.util.function.Supplier;
public class ValueGenerator {
public static int getValue(Supplier<Integer> supplier) {
return supplier.get();
}
public static void main(String[] args) {
// Zufallszahl erhalten
int random = getValue(() -> (int)(Math.random() * 100));
System.out.println("Zufallszahl: " + random);
// Aktuelle Zeit in Millisekunden erhalten
long time = getValue(System::currentTimeMillis);
System.out.println("Zeit: " + time);
}
}
5. Einheitliche Anwendung: Beispiele zusammenführen
Stellen wir uns vor, wir entwickeln eine einfache Anwendung „Benutzerliste“. Wir können bereits:
- Benutzer nach einer Bedingung filtern (z. B. nur Volljährige);
- Benutzer in etwas transformieren (Namen, E‑Mail, Alter);
- Aktionen für jeden Benutzer ausführen (Ausgabe, Bonus gutschreiben);
- Werte bei Bedarf generieren (z. B. neue Benutzer anlegen).
Nun können wir mit all diesen Methoden flexible Datenverarbeitungsszenarien aufbauen, ohne den Code für jede neue Aufgabe neu zu schreiben.
import java.util.*;
import java.util.function.*;
public class UserApp {
static class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + " (" + age + ")";
}
}
// Universelle Methode zur Verarbeitung von Benutzern
static void processUsers(List<User> users, Consumer<User> action) {
for (User user : users) action.accept(user);
}
// Universeller Filter
static List<User> filterUsers(List<User> users, Predicate<User> condition) {
List<User> result = new ArrayList<>();
for (User user : users) if (condition.test(user)) result.add(user);
return result;
}
// Universeller Mapper
static <R> List<R> mapUsers(List<User> users, Function<User, R> mapper) {
List<R> result = new ArrayList<>();
for (User user : users) result.add(mapper.apply(user));
return result;
}
public static void main(String[] args) {
List<User> users = List.of(
new User("Anna", 25),
new User("Boris", 17),
new User("Vika", 22)
);
// 1. Alle Benutzer ausgeben
processUsers(users, user -> System.out.println("Benutzer: " + user));
// 2. Nur Erwachsene finden
List<User> adults = filterUsers(users, user -> user.age >= 18);
System.out.println("Volljährige: " + adults);
// 3. Namen der Erwachsenen erhalten
List<String> adultNames = mapUsers(adults, user -> user.name);
System.out.println("Namen der Erwachsenen: " + adultNames);
// 4. Erwachsenen einen Bonus gutschreiben
processUsers(adults, user -> System.out.println(user.name + " hat einen Bonus erhalten!"));
}
}
Warum ist das besser als der „gewöhnliche“ Ansatz?
Dieser Ansatz bietet Flexibilität: Dieselben Methoden können in ganz unterschiedlichen Szenarien verwendet werden, indem einfach andere Funktionen übergeben werden. Der Code wächst dabei nicht zu endlosen Varianten wie „so filtern“, „so drucken“ — wir haben allgemeine Werkzeuge, die überall funktionieren. Dadurch wird er kompakter, verständlicher und weniger fehleranfällig. Ausserdem passt dieser Stil hervorragend zum modernen Stream-API, sodass beim Umstieg auf Streams kein Umlernen nötig ist — der Ansatz bleibt derselbe.
6. Typische Fehler bei der Übergabe von Funktionen als Parameter
Fehler Nr. 1: Die Signatur passt nicht zum erwarteten Interface.
Wenn die Methode Predicate<User> annimmt, Sie aber eine Lambda übergeben, die nicht boolean, sondern z. B. String zurückgibt, sagt der Compiler sofort „oje!“ und lässt das Projekt nicht bauen. Prüfen Sie, dass Rückgabewert und Parameter den Erwartungen entsprechen.
Fehler Nr. 2: Die Lambda verwendet Variablen, die sich ändern können.
In Java dürfen Lambda-Ausdrücke nur final oder effectively final Variablen aus dem äusseren Kontext verwenden. Wenn Sie versuchen, eine solche Variable innerhalb der Lambda zu ändern — gibt es einen Kompilierfehler.
Fehler Nr. 3: Verwechslung der Interfaces.
Manchmal möchte man z. B. einen Consumer<T> übergeben, schreibt aber versehentlich eine Funktion, die etwas zurückgibt (z. B. Function<T, R>). Prüfen Sie, dass Ihre Lambda genau das Richtige zurückgibt (oder nichts — für Consumer).
Fehler Nr. 4: Zu komplexe Lambdas direkt in den Parametern.
Wenn eine Lambda mehr als ein bis zwei Zeilen umfasst, lagern Sie sie besser in eine eigene Variable oder Methode aus. Andernfalls wird der Code unleserlich, und am Ende steigen nur Sie darin durch (und das nicht immer).
GO TO FULL VERSION