Skip to content
IRC-Coding IRC-Coding
Funktionale Programmierung Lambda Ausdrücke Functional Interfaces Higher Order Functions Streams

Funktionale Programmierung: Lambda-Ausdrücke & Functional Interfaces

Funktionale Programmierung mit Lambda-Ausdrücken, Functional Interfaces, Higher-Order Functions. Pure Functions, Immutability, Streams und deklarative Programmierung mit Beispielen.

S

schutzgeist

2 min read

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

  1. Lambda-Ausdrücke: (x, y) -> x + y
  2. Functional Interfaces: Predicate<T>, Function<T,R>
  3. Pure Functions: Keine Seiteneffekte
  4. Immutability: Unveränderliche Objekte
  5. Higher-Order Functions: map(), filter(), reduce()
  6. Streams: Sequenzielle Datenverarbeitung
  7. Method References: String::length
  8. 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

  1. Was ist der Unterschied zwischen Lambda-Ausdruck und anonymer Klasse? Lambda-Ausdruck ist kompakter Syntax für Functional Interface, anonyme Klasse hat mehr Boilerplate.

  2. Erklären Sie Pure Functions! Funktionen ohne Seiteneffekte, die bei gleicher Eingabe immer gleiche Ausgabe liefern.

  3. Was ist ein Higher-Order Function? Funktion, die andere Funktionen als Parameter nimmt oder zurückgibt.

  4. Warum ist Immutability wichtig? Verhindert unerwartete Zustandsänderungen und erleichtert Parallelisierung.

Wichtigste Quellen

  1. https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html
  2. https://docs.oracle.com/javase/tutorial/collections/streams/
  3. https://www.python.org/doc/essays/list2str.html
Zurück zum Blog
Share:

Ähnliche Beiträge