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
- Lambda expressions:
(x, y) -> x + y - Functional interfaces:
Predicate<T>,Function<T,R> - Pure functions: No side effects
- Immutability: Immutable objects
- Higher-order functions:
map(),filter(),reduce() - Streams: Sequential data processing
- Method references:
String::length - 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
-
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.
-
Explain Pure Functions! Functions without side effects that always return the same output for the same input.
-
What is a Higher-Order Function? A function that takes other functions as parameters or returns them.
-
Why is Immutability important? Prevents unexpected state mutations and simplifies parallelization.
Most Important Sources
- 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