Skip to content
IRC-Coding IRC-Coding
Functional Programming Lambda Expressions Functional Interfaces Higher Order Functions Streams

Functional Programming: Lambda Expressions & Interfaces

Master functional programming with lambda expressions, functional interfaces, and higher-order functions. Pure functions, immutability, streams.

S

schutzgeist

2 min read
Functional Programming: Lambda Expressions & Interfaces

Functional Programming: Lambda Expressions & Functional Interfaces

This article is a comprehensive explanation of functional programming – including lambda expressions, functional interfaces, and higher-order functions with practical examples.

In a Nutshell

Functional programming focuses on computation through functions rather than state changes. Lambda expressions are inline function literals that are bound via functional interfaces.

Concise Technical Description

Functional programming is a paradigm that treats functions as primary building blocks. Unlike imperative programming, state changes are avoided.

Lambda expressions describe anonymous behavior with parameters, body, and optional return type. The type is inferred from the target type context.

Functional interfaces in Java have exactly one abstract method and enable higher-order functions:

  • Predicate: boolean test(T t) - Test conditions
  • Function: R apply(T t) - Transformations
  • Consumer: void accept(T t) - Consuming
  • Supplier: T get() - Providing

Core principles:

  • Pure Functions: Immutable input → deterministic output
  • Immutability: Data is immutable
  • Referential Transparency: Function call can be replaced by its result
  • Higher-Order Functions: Functions as parameters/return values

Exam-Relevant Key Points

  • Lambda expressions: Anonymous functions with compact syntax
  • Functional interfaces: Exactly one abstract method
  • Higher-Order functions: Functions as parameters/return values
  • Pure functions: No side effects, deterministic
  • Immutability: Immutable data structures
  • Streams API: Declarative data processing
  • Method references: Compact reference to methods
  • IHK-relevant: Modern Java, functional approaches

Core Components

  1. Lambda expressions: (x, y) -> x + y
  2. Functional interfaces: Predicate<T>, Function<T,R>
  3. Pure functions: No side effects
  4. Immutability: Immutable objects
  5. Higher-order functions: map(), filter(), reduce()
  6. Streams: Sequential data processing
  7. Method references: String::length
  8. Closures: Access to outer variables

Practical Examples

1. Lambda Expressions and 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 and Immutability

// Imperative Approach (with side effects)
class ImperativeRechner {
    private int summe = 0;
    
    public void addiere(int wert) {
        this.summe += wert; // Side effect: state changes
    }
    
    public int getSumme() {
        return summe;
    }
}

// Functional Approach (Pure Functions)
class FunctionalRechner {
    
    // Pure function: no side effects, deterministic
    public static int addiere(int a, int b) {
        return a + b;
    }
    
    // Pure function with immutable data
    public static List<Integer> filtereGerade(List<Integer> zahlen) {
        return zahlen.stream()
            .filter(n -> n % 2 == 0)
            .collect(Collectors.toList());
    }
    
    // Pure function with 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 with composition
    public static Function<Integer, Integer> multipliziereMit(int faktor) {
        return zahl -> zahl * faktor;
    }
    
    public static Function<Integer, Integer> addiereZu(int wert) {
        return zahl -> zahl + wert;
    }
}

// Immutable data class
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 for modifications (creates new object)
    public Person mitNeuemAlter(int neuesAlter) {
        return new Person(this.name, neuesAlter);
    }
    
    public Person mitNeuemName(String neuerName) {
        return new Person(neuerName, this.alter);
    }
    
    // Getter (no setter for immutability)
    public String getName() { return name; }
    public int getAlter() { return alter; }
    
    @Override
    public String toString() {
        return name + " (" + alter + ")";
    }
}

// Usage
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); // Always same result
        
        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);
        
        // Function composition
        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 and Declarative Programming

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 data processing with Streams
        
        // 1. Filter and Transform
        List<String> entwicklerNamen = personen.stream()
            .filter(p -> p.getAbteilung().equals("Entwicklung")) // Filter
            .map(Person::getName)                                // Transform
            .sorted()                                            // Sort
            .collect(Collectors.toList());                       // Collect
        
        System.out.println("Developers: " + entwicklerNamen);
        
        // 2. Complex pipeline with multiple operations
        Map<String, Double> durchschnittsAlterProAbteilung = personen.stream()
            .collect(Collectors.groupingBy(
                Person::getAbteilung,
                Collectors.averagingInt(Person::getAlter)
            ));
        
        System.out.println("Average age: " + durchschnittsAlterProAbteilung);
        
        // 3. Reduce for aggregation
        int gesamtesAlter = personen.stream()
            .mapToInt(Person::getAlter)
            .reduce(0, Integer::sum); // Alternative: .sum()
        
        System.out.println("Total age: " + gesamtesAlter);
        
        // 4. Optional for safe processing
        Optional<Person> aeltestePerson = personen.stream()
            .max(Comparator.comparing(Person::getAlter));
        
        aeltestePerson.ifPresent(p -> 
            System.out.println("Oldest person: " + p.getName()));
        
        // 5. Custom Collector
        String alleNamen = personen.stream()
            .map(Person::getName)
            .collect(Collectors.joining(", "));
        
        System.out.println("All names: " + alleNamen);
        
        // 6. Parallel Streams for 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("Number of primes: " + anzahlPrime);
    }
    
    // Pure Function for prime number check
    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 class for examples
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 and Closures

import java.util.function.*;
import java.util.*;

public class HigherOrderFunctionsDemo {
    
    // Higher-Order Function: Takes function as 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: Returns function
    public static Function<Integer, Integer> multiplizierer(int faktor) {
        return zahl -> zahl * faktor; // Closure: faktor is bound
    }
    
    // Higher-Order Function: Returns Predicate
    public static Predicate<String> laengerAls(int mindestlaenge) {
        return text -> text.length() > mindestlaenge;
    }
    
    // Higher-Order Function with multiple functions
    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 (simplified)
    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);
        
        // Use Higher-Order Function
        List<Integer> laengen = mappe(woerter, String::length);
        System.out.println("Lengths: " + laengen);
        
        // Return function and use it
        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("Doubled: " + verdoppelt);
        System.out.println("Tripled: " + verdreifacht);
        
        // Predicate Higher-Order Function
        Predicate<String> laengerAlsDrei = laengerAls(3);
        List<String> langeWoerter = woerter.stream()
            .filter(laengerAlsDrei)
            .collect(Collectors.toList());
        
        System.out.println("Long words: " + langeWoerter);
        
        // Function chain
        List<Function<Integer, Integer>> funktionen = Arrays.asList(
            n -> n * 2,    // double
            n -> n + 10,   // add
            n -> n / 3     // divide
        );
        
        List<Integer> verarbeitet = verarbeiteKette(zahlen, funktionen);
        System.out.println("Processed numbers: " + 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 result: " + 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. Functional Programming 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

# Immutable data class
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)

# Usage
def funktionale_demo():
    # Pure Functions
    zahlen = [1, 2, 3, 4, 5]
    gerade = filtere_gerade(zahlen)
    quadrate = quadriere(zahlen)
    
    print(f"Even: {gerade}")
    print(f"Squares: {quadrate}")
    
    # Higher-Order Functions
    verdoppeln = multiplizierer(2)
    verdreifachen = multiplizierer(3)
    
    verdoppelt = verarbeite(zahlen, verdoppeln)
    verdreifacht = verarbeite(zahlen, verdreifachen)
    
    print(f"Doubled: {verdoppelt}")
    print(f"Tripled: {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 result: {ergebnis}")
    
    # Reduce for aggregation
    summe = reduce(operator.add, zahlen, 0)
    produkt = reduce(operator.mul, zahlen, 1)
    
    print(f"Sum: {summe}")
    print(f"Product: {produkt}")
    
    # Immutability
    alice = Person("Alice", 25, "Entwicklung")
    alice_aelter = alice.mit_neuem_alter(26)
    
    print(f"Original: {alice}")
    print(f"Modified: {alice_aelter}")

if __name__ == "__main__":
    funktionale_demo()

Lambda Syntax Comparison

Java Lambda Expressions

// Various lambda forms
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 Expressions

# Lambda expressions
leer = lambda s: len(s) == 0
verdoppeln = lambda x: x * 2

# Higher-Order Functions with 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 Expressions

// 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);

Advantages and Disadvantages

Advantages of Functional Programming

  • Testability: Pure Functions are easy to test
  • Parallelization: No side effects enable safe parallel processing
  • Reusability: Higher-Order Functions are flexible
  • Readability: Declarative code is often more understandable
  • Error Proneness: Fewer bugs from state mutations

Disadvantages

  • Learning Curve: Functional thinking requires practice
  • Performance: Functional abstractions can have overhead
  • Memory: Immutability can require more memory
  • Debugging: Stack traces can be more complex

Common Exam Questions

  1. What is the difference between a Lambda expression and an anonymous class? Lambda expression is more compact syntax for a Functional Interface, anonymous class has more boilerplate.

  2. Explain Pure Functions! Functions without side effects that always return the same output for the same input.

  3. What is a Higher-Order Function? A function that takes other functions as parameters or returns them.

  4. Why is Immutability important? Prevents unexpected state mutations and simplifies parallelization.

Most Important Sources

  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
Back to Blog
Share:

Related Posts