Funktionale Programmierung: Lambda-Ausdrücke & Functional Interfaces
Dieser Beitrag ist eine umfassende Erläuterung der funktionalen Programmierung – inklusive Lambda-Ausdrücken, Functional Interfaces und Higher-Order Functions mit praktischen Beispielen.
In a Nutshell
Funktionale Programmierung fokussiert auf Berechnung über Funktionen statt Zustandsänderungen. Lambda-Ausdrücke sind Inline-Funktionsliterale, die über Functional Interfaces gebunden werden.
Kompakte Fachbeschreibung
Funktionale Programmierung ist ein Paradigma, das Funktionen als primäre Bausteine behandelt. Im Gegensatz zur imperativen Programmierung werden Zustandsänderungen vermieden.
Lambda-Ausdrücke beschreiben anonymes Verhalten mit Parametern, Rumpf und optionalem Rückgabetyp. Der Typ wird aus dem Zielkontext (Target Type) hergeleitet.
Functional Interfaces in Java haben genau eine abstrakte Methode und ermöglichen Higher-Order Functions:
- Predicate:
boolean test(T t)- Test-Bedingungen - Function:
R apply(T t)- Transformationen - Consumer:
void accept(T t)- Konsumieren - Supplier:
T get()- Liefern
Zentrale Prinzipien:
- Pure Functions: Unveränderlicher Input → deterministischer Output
- Immutability: Daten sind unveränderlich
- Referentielle Transparenz: Aufruf kann durch Ergebnis ersetzt werden
- Higher-Order Functions: Funktionen als Parameter/Rückgabewerte
Prüfungsrelevante Stichpunkte
- Lambda-Ausdrücke: Anonyme Funktionen mit kompakter Syntax
- Functional Interfaces: Genau eine abstrakte Methode
- Higher-Order Functions: Funktionen als Parameter/Rückgabewerte
- Pure Functions: Keine Seiteneffekte, deterministisch
- Immutability: Unveränderliche Datenstrukturen
- Streams API: Declarative Datenverarbeitung
- Method References: Kompakter Verweis auf Methoden
- IHK-relevant: Modernes Java, funktionale Ansätze
Kernkomponenten
- Lambda-Ausdrücke:
(x, y) -> x + y - Functional Interfaces:
Predicate<T>,Function<T,R> - Pure Functions: Keine Seiteneffekte
- Immutability: Unveränderliche Objekte
- Higher-Order Functions:
map(),filter(),reduce() - Streams: Sequenzielle Datenverarbeitung
- Method References:
String::length - Closures: Zugriff auf äußere Variablen
Praxisbeispiele
1. Lambda-Ausdrücke und Functional Interfaces in Java
import java.util.*;
import java.util.function.*;
import java.util.stream.Collectors;
public class FunctionalProgrammingDemo {
public static void main(String[] args) {
List<String> namen = Arrays.asList("Alice", "Bob", "Charlie", "Diana");
List<Integer> zahlen = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Lambda mit Predicate (Filter)
Predicate<String> laengerAlsVier = name -> name.length() > 4;
List<String> langeNamen = namen.stream()
.filter(laengerAlsVier)
.collect(Collectors.toList());
System.out.println("Lange Namen: " + langeNamen);
// Lambda mit Function (Map/Transformation)
Function<String, Integer> stringLaenge = String::length; // Method Reference
List<Integer> laengen = namen.stream()
.map(stringLaenge)
.collect(Collectors.toList());
System.out.println("Namenlängen: " + laengen);
// Lambda mit Consumer (ForEach)
Consumer<String> drucker = name -> System.out.println("Hallo " + name);
namen.forEach(drucker);
// Lambda mit Supplier (Erzeugen)
Supplier<Double> zufallszahl = () -> Math.random();
System.out.println("Zufallszahl: " + zufallszahl.get());
// Komplexe Lambda-Ausdrücke
Predicate<Integer> istGerade = n -> n % 2 == 0;
Predicate<Integer> istGroesserAlsFuenf = n -> n > 5;
// Predicate kombinieren
Predicate<Integer> istGeradeUndGroesser = istGerade.and(istGroesserAlsFuenf);
List<Integer> gefilterteZahlen = zahlen.stream()
.filter(istGeradeUndGroesser)
.collect(Collectors.toList());
System.out.println("Gerade und >5: " + gefilterteZahlen);
// Higher-Order Function
Function<Integer, Predicate<Integer>> groesserAls = grenzwert ->
zahl -> zahl > grenzwert;
Predicate<Integer> groesserAlsDrei = groesserAls.apply(3);
List<Integer> groessereZahlen = zahlen.stream()
.filter(groesserAlsDrei)
.collect(Collectors.toList());
System.out.println(">3: " + groessereZahlen);
}
}
2. Pure Functions und Immutability
// Imperative Approach (mit Seiteneffekten)
class ImperativeRechner {
private int summe = 0;
public void addiere(int wert) {
this.summe += wert; // Seiteneffekt: Zustand ändert sich
}
public int getSumme() {
return summe;
}
}
// Functional Approach (Pure Functions)
class FunctionalRechner {
// Pure Function: keine Seiteneffekte, deterministisch
public static int addiere(int a, int b) {
return a + b;
}
// Pure Function mit unveränderlichen Daten
public static List<Integer> filtereGerade(List<Integer> zahlen) {
return zahlen.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
}
// Pure Function mit Transformation
public static List<Integer> quadriere(List<Integer> zahlen) {
return zahlen.stream()
.map(n -> n * n)
.collect(Collectors.toList());
}
// Higher-Order Function
public static List<Integer> verarbeite(List<Integer> zahlen,
Function<Integer, Integer> operation) {
return zahlen.stream()
.map(operation)
.collect(Collectors.toList());
}
// Pure Function mit Komposition
public static Function<Integer, Integer> multipliziereMit(int faktor) {
return zahl -> zahl * faktor;
}
public static Function<Integer, Integer> addiereZu(int wert) {
return zahl -> zahl + wert;
}
}
// Unveränderliche Datenklasse
public final class Person {
private final String name;
private final int alter;
public Person(String name, int alter) {
this.name = name;
this.alter = alter;
}
// Pure Function für Änderungen (erzeugt neues Objekt)
public Person mitNeuemAlter(int neuesAlter) {
return new Person(this.name, neuesAlter);
}
public Person mitNeuemName(String neuerName) {
return new Person(neuerName, this.alter);
}
// Getter (keine Setter für Immutability)
public String getName() { return name; }
public int getAlter() { return alter; }
@Override
public String toString() {
return name + " (" + alter + ")";
}
}
// Verwendung
public class PureFunctionDemo {
public static void main(String[] args) {
// Imperative Approach
ImperativeRechner imperativ = new ImperativeRechner();
imperativ.addiere(5);
imperativ.addiere(3);
System.out.println("Imperativ: " + imperativ.getSumme()); // 8
// Functional Approach
int ergebnis1 = FunctionalRechner.addiere(5, 3);
int ergebnis2 = FunctionalRechner.addiere(5, 3); // Immer gleiches Ergebnis
List<Integer> zahlen = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> gerade = FunctionalRechner.filtereGerade(zahlen);
List<Integer> quadrate = FunctionalRechner.quadriere(zahlen);
System.out.println("Gerade: " + gerade);
System.out.println("Quadrate: " + quadrate);
// Higher-Order Function
Function<Integer, Integer> verdoppeln = n -> n * 2;
List<Integer> verdoppelt = FunctionalRechner.verarbeite(zahlen, verdoppeln);
System.out.println("Verdoppelt: " + verdoppelt);
// Funktionskomposition
Function<Integer, Integer> multiplizieren = FunctionalRechner.multipliziereMit(2);
Function<Integer, Integer> addieren = FunctionalRechner.addiereZu(10);
Function<Integer, Integer> kombiniert = multiplizieren.andThen(addieren);
List<Integer> kombiniertErgebnis = FunctionalRechner.verarbeite(zahlen, kombiniert);
System.out.println("Kombiniert (x*2+10): " + kombiniertErgebnis);
// Immutability
Person alice = new Person("Alice", 25);
Person aliceAelter = alice.mitNeuemAlter(26);
System.out.println("Original: " + alice); // Alice (25)
System.out.println("Verändert: " + aliceAelter); // Alice (26)
}
}
3. Streams API und deklarative Programmierung
import java.util.*;
import java.util.stream.*;
public class StreamAPIDemo {
public static void main(String[] args) {
List<Person> personen = Arrays.asList(
new Person("Alice", 25, "Entwicklung"),
new Person("Bob", 30, "Marketing"),
new Person("Charlie", 35, "Entwicklung"),
new Person("Diana", 28, "Vertrieb"),
new Person("Eve", 32, "Entwicklung")
);
// Declarative Datenverarbeitung mit Streams
// 1. Filtern und Transformieren
List<String> entwicklerNamen = personen.stream()
.filter(p -> p.getAbteilung().equals("Entwicklung")) // Filter
.map(Person::getName) // Transformieren
.sorted() // Sortieren
.collect(Collectors.toList()); // Sammeln
System.out.println("Entwickler: " + entwicklerNamen);
// 2. Komplexe Pipeline mit mehreren Operationen
Map<String, Double> durchschnittsAlterProAbteilung = personen.stream()
.collect(Collectors.groupingBy(
Person::getAbteilung,
Collectors.averagingInt(Person::getAlter)
));
System.out.println("Durchschnittsalter: " + durchschnittsAlterProAbteilung);
// 3. Reduce für Aggregation
int gesamtesAlter = personen.stream()
.mapToInt(Person::getAlter)
.reduce(0, Integer::sum); // Alternative: .sum()
System.out.println("Gesamtes Alter: " + gesamtesAlter);
// 4. Optional für sichere Verarbeitung
Optional<Person> aeltestePerson = personen.stream()
.max(Comparator.comparing(Person::getAlter));
aeltestePerson.ifPresent(p ->
System.out.println("Älteste Person: " + p.getName()));
// 5. Custom Collector
String alleNamen = personen.stream()
.map(Person::getName)
.collect(Collectors.joining(", "));
System.out.println("Alle Namen: " + alleNamen);
// 6. Parallel Streams für Performance
List<Integer> grosseZahlen = IntStream.range(1, 1_000_000)
.boxed()
.collect(Collectors.toList());
long anzahlPrime = grosseZahlen.parallelStream()
.filter(StreamAPIDemo::istPrimzahl)
.count();
System.out.println("Anzahl Primzahlen: " + anzahlPrime);
}
// Pure Function für Primzahlprüfung
private static boolean istPrimzahl(int n) {
if (n <= 1) return false;
if (n <= 3) return true;
if (n % 2 == 0 || n % 3 == 0) return false;
for (int i = 5; i * i <= n; i += 6) {
if (n % i == 0 || n % (i + 2) == 0) return false;
}
return true;
}
}
// Person-Klasse für Beispiele
class Person {
private final String name;
private final int alter;
private final String abteilung;
public Person(String name, int alter, String abteilung) {
this.name = name;
this.alter = alter;
this.abteilung = abteilung;
}
public String getName() { return name; }
public int getAlter() { return alter; }
public String getAbteilung() { return abteilung; }
@Override
public String toString() {
return name + " (" + alter + ", " + abteilung + ")";
}
}
4. Higher-Order Functions und Closures
import java.util.function.*;
import java.util.*;
public class HigherOrderFunctionsDemo {
// Higher-Order Function: Nimmt Funktion als Parameter
public static <T, R> List<R> mappe(List<T> liste, Function<T, R> mapper) {
List<R> ergebnis = new ArrayList<>();
for (T element : liste) {
ergebnis.add(mapper.apply(element));
}
return ergebnis;
}
// Higher-Order Function: Gibt Funktion zurück
public static Function<Integer, Integer> multiplizierer(int faktor) {
return zahl -> zahl * faktor; // Closure: faktor ist gebunden
}
// Higher-Order Function: Gibt Predicate zurück
public static Predicate<String> laengerAls(int mindestlaenge) {
return text -> text.length() > mindestlaenge;
}
// Higher-Order Function mit mehreren Funktionen
public static <T> List<T> verarbeiteKette(List<T> liste,
List<Function<T, T>> funktionen) {
List<T> ergebnis = new ArrayList<>(liste);
for (Function<T, T> funktion : funktionen) {
ergebnis = mappe(ergebnis, funktion);
}
return ergebnis;
}
// Currying (vereinfacht)
public static Function<Integer, Function<Integer, Integer>> addiereCurried() {
return a -> b -> a + b;
}
// Function Composition
public static <T> Function<T, T> komponiere(Function<T, T> f, Function<T, T> g) {
return x -> f.apply(g.apply(x));
}
public static void main(String[] args) {
List<String> woerter = Arrays.asList("Java", "Python", "JavaScript", "C++");
List<Integer> zahlen = Arrays.asList(1, 2, 3, 4, 5);
// Higher-Order Function verwenden
List<Integer> laengen = mappe(woerter, String::length);
System.out.println("Längen: " + laengen);
// Funktion zurückgeben und verwenden
Function<Integer, Integer> verdoppeln = multiplizierer(2);
Function<Integer, Integer> verdreifachen = multiplizierer(3);
List<Integer> verdoppelt = mappe(zahlen, verdoppeln);
List<Integer> verdreifacht = mappe(zahlen, verdreifachen);
System.out.println("Verdoppelt: " + verdoppelt);
System.out.println("Verdreifacht: " + verdreifacht);
// Predicate Higher-Order Function
Predicate<String> laengerAlsDrei = laengerAls(3);
List<String> langeWoerter = woerter.stream()
.filter(laengerAlsDrei)
.collect(Collectors.toList());
System.out.println("Lange Wörter: " + langeWoerter);
// Funktionskette
List<Function<Integer, Integer>> funktionen = Arrays.asList(
n -> n * 2, // verdoppeln
n -> n + 10, // addieren
n -> n / 3 // teilen
);
List<Integer> verarbeitet = verarbeiteKette(zahlen, funktionen);
System.out.println("Verarbeitete Zahlen: " + verarbeitet);
// Currying
Function<Integer, Function<Integer, Integer>> addiere = addiereCurried();
Function<Integer, Integer> addiereFuenf = addiere.apply(5);
int ergebnis = addiereFuenf.apply(3); // 5 + 3 = 8
System.out.println("Currying Ergebnis: " + ergebnis);
// Function Composition
Function<Integer, Integer> quadrieren = n -> n * n;
Function<Integer, Integer> inkrementieren = n -> n + 1;
Function<Integer, Integer> quadrierenDannInkrementieren = komponiere(inkrementieren, quadrieren);
Function<Integer, Integer> inkrementierenDannQuadrieren = komponiere(quadrieren, inkrementieren);
System.out.println("3²+1: " + quadrierenDannInkrementieren.apply(3)); // 10
System.out.println("(3+1)²: " + inkrementierenDannQuadrieren.apply(3)); // 16
}
}
5. Funktionale Programmierung in Python
from typing import List, Callable, Optional
from functools import reduce
import operator
# Pure Functions
def addiere(a: int, b: int) -> int:
return a + b
def filtere_gerade(zahlen: List[int]) -> List[int]:
return [n for n in zahlen if n % 2 == 0]
def quadriere(zahlen: List[int]) -> List[int]:
return [n * n for n in zahlen]
# Higher-Order Functions
def verarbeite(zahlen: List[int], operation: Callable[[int], int]) -> List[int]:
return [operation(n) for n in zahlen]
def multiplizierer(faktor: int) -> Callable[[int], int]:
return lambda x: x * faktor
# Function Composition
def komponiere(f: Callable, g: Callable) -> Callable:
return lambda x: f(g(x))
# Currying
def addiere_curried(a: int):
return lambda b: a + b
# Unveränderliche Datenklasse
from dataclasses import dataclass
@dataclass(frozen=True)
class Person:
name: str
alter: int
abteilung: str
def mit_neuem_alter(self, neues_alter: int) -> 'Person':
return Person(self.name, neues_alter, self.abteilung)
# Verwendung
def funktionale_demo():
# Pure Functions
zahlen = [1, 2, 3, 4, 5]
gerade = filtere_gerade(zahlen)
quadrate = quadriere(zahlen)
print(f"Gerade: {gerade}")
print(f"Quadrate: {quadrate}")
# Higher-Order Functions
verdoppeln = multiplizierer(2)
verdreifachen = multiplizierer(3)
verdoppelt = verarbeite(zahlen, verdoppeln)
verdreifacht = verarbeite(zahlen, verdreifachen)
print(f"Verdoppelt: {verdoppelt}")
print(f"Verdreifacht: {verdreifacht}")
# Function Composition
quadrieren = lambda x: x * x
inkrementieren = lambda x: x + 1
quadrieren_dann_inkrementieren = komponiere(inkrementieren, quadrieren)
inkrementieren_dann_quadrieren = komponiere(quadrieren, inkrementieren)
print(f"3²+1: {quadrieren_dann_inkrementieren(3)}") # 10
print(f"(3+1)²: {inkrementieren_dann_quadrieren(3)}") # 16
# Currying
addiere_fuenf = addiere_curried(5)
ergebnis = addiere_fuenf(3) # 8
print(f"Currying Ergebnis: {ergebnis}")
# Reduce für Aggregation
summe = reduce(operator.add, zahlen, 0)
produkt = reduce(operator.mul, zahlen, 1)
print(f"Summe: {summe}")
print(f"Produkt: {produkt}")
# Immutability
alice = Person("Alice", 25, "Entwicklung")
alice_aelter = alice.mit_neuem_alter(26)
print(f"Original: {alice}")
print(f"Verändert: {alice_aelter}")
if __name__ == "__main__":
funktionale_demo()
Lambda-Syntax im Vergleich
Java Lambda-Ausdrücke
// Verschiedene Lambda-Formen
Predicate<String> leer = s -> s.isEmpty();
Predicate<String> leer2 = String::isEmpty; // Method Reference
Function<Integer, String> toString = i -> i.toString();
Function<Integer, String> toString2 = Object::toString;
Consumer<String> drucker = s -> System.out.println(s);
Consumer<String> drucker2 = System.out::println;
Supplier<Integer> zufall = () -> (int)(Math.random() * 100);
Python Lambda-Ausdrücke
# Lambda-Ausdrücke
leer = lambda s: len(s) == 0
verdoppeln = lambda x: x * 2
# Higher-Order Functions mit Lambda
zahlen = [1, 2, 3, 4, 5]
verdoppelt = list(map(lambda x: x * 2, zahlen))
gerade = list(filter(lambda x: x % 2 == 0, zahlen))
JavaScript Lambda-Ausdrücke
// Arrow Functions
const leer = s => s.length === 0;
const verdoppeln = x => x * 2;
// Higher-Order Functions
const zahlen = [1, 2, 3, 4, 5];
const verdoppelt = zahlen.map(x => x * 2);
const gerade = zahlen.filter(x => x % 2 === 0);
Vorteile und Nachteile
Vorteile der Funktionalen Programmierung
- Testbarkeit: Pure Functions sind einfach zu testen
- Parallelisierung: Keine Seiteneffekte ermöglichen sichere Parallelverarbeitung
- Wiederverwendbarkeit: Higher-Order Functions sind flexibel
- Lesbarkeit: Deklarative Code ist oft verständlicher
- Fehleranfälligkeit: Weniger Bugs durch Zustandsänderungen
Nachteile
- Lernkurve: Funktionales Denken erfordert Übung
- Performance: Funktionale Abstraktionen können Overhead haben
- Speicher: Immutability kann mehr Speicher benötigen
- Debugging: Stack-Traces können komplexer sein
Häufige Prüfungsfragen
-
Was ist der Unterschied zwischen Lambda-Ausdruck und anonymer Klasse? Lambda-Ausdruck ist kompakter Syntax für Functional Interface, anonyme Klasse hat mehr Boilerplate.
-
Erklären Sie Pure Functions! Funktionen ohne Seiteneffekte, die bei gleicher Eingabe immer gleiche Ausgabe liefern.
-
Was ist ein Higher-Order Function? Funktion, die andere Funktionen als Parameter nimmt oder zurückgibt.
-
Warum ist Immutability wichtig? Verhindert unerwartete Zustandsänderungen und erleichtert Parallelisierung.
Wichtigste Quellen
- https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html
- https://docs.oracle.com/javase/tutorial/collections/streams/
- https://www.python.org/doc/essays/list2str.html