Programmierparadigmen
Imperativ, objektorientiert, funktional, deklarativ: vier Arten, über Programme nachzudenken. Dasselbe Problem in jedem Stil, warum geteilter veränderlicher Zustand so viele Fehler erzeugt – und warum modernes Java alle Stile beherrscht.
Ein Programmierparadigma ist eine Denkweise, wie man ein Programm gliedert. Imperativ: eine Folge von Anweisungen, die Zustand verändern. Objektorientiert: Zustand und Verhalten in Objekte gebündelt, die miteinander sprechen. Funktional: Funktionen, die aus Werten neue Werte berechnen, ohne etwas zu verändern. Deklarativ: beschreiben, was herauskommen soll, nicht wie.
Kein Stil ist der richtige. Moderne Sprachen mischen sie, und Java kann seit Lambdas, Streams, Records und Pattern Matching alle. Die wichtigste Lektion aus dem Vergleich: Viele schwer zu findende Fehler entstehen aus geteiltem, veränderlichem Zustand – und jeder Stil hat eine andere Antwort darauf.
Ein Problem, vier Stile
Die Aufgabe: Wie viel Umsatz haben alle versandten Bestellungen über 20 Euro gebracht?
Imperativ
Schritt für Schritt: Variable anlegen, Schleife, Bedingung, Variable ändern. Nah an der Maschine, die genau so arbeitet – Befehle holen, ausführen, Speicher ändern.
Objektorientiert
Objekte kapseln ihren Zustand und bieten Methoden an. Wer etwas wissen will, fragt das Objekt – statt in seine Felder zu greifen. Gliedert große Systeme nach Verantwortung.
Funktional
Funktionen ohne Nebenwirkungen, unveränderliche Werte, Funktionen als Bausteine, die man kombiniert. Leicht zu testen, leicht zu parallelisieren.
Der vierte, deklarative Stil steckt oft in eigenen Sprachen: SQL, HTML, CSS, reguläre Ausdrücke, Konfigurationsdateien. Man beschreibt das Ergebnis, und ein Programm darunter – der Abfrageoptimierer, der Browser – entscheidet den Weg. Javas Streams sind ein Schritt in diese Richtung: filter und map sagen, was passieren soll, nicht in welcher Reihenfolge die Elemente angefasst werden.
Das Kernproblem: geteilter, veränderlicher Zustand
In Java zeigen Variablen auf Objekte – sie enthalten sie nicht. Zwei Variablen können auf dasselbe Objekt zeigen. Verändert man es über die eine, ändert es sich auch für die andere. Spiel es durch:
Im Kleinen ist das harmlos. Im Großen ist es eine der häufigsten Fehlerquellen: Eine Methode sortiert „nur kurz“ eine Liste, die sie übergeben bekommen hat – und an ganz anderer Stelle im Programm ist eine Anzeige plötzlich falsch sortiert. Mit mehreren Threads wird daraus eine Race Condition. Die drei Stile antworten unterschiedlich:
- Imperativ: Disziplin und Dokumentation – „diese Methode verändert ihr Argument“.
- Objektorientiert: Kapselung. Ein Objekt gibt seinen inneren Zustand nicht heraus, sondern nur Kopien oder unveränderliche Sichten (
List.copyOf,Collections.unmodifiableList). - Funktional: Zustand gar nicht erst verändern. Jede Operation erzeugt einen neuen Wert. Was sich nie ändert, kann man bedenkenlos teilen – auch zwischen Threads.
Reine Funktionen
Eine Funktion ist rein, wenn ihr Ergebnis nur von ihren Argumenten abhängt und sie nichts anderes bewirkt – nichts schreibt, nichts ausgibt, nichts verändert. Math.max(a, b) ist rein. list.add(x), System.out.println, LocalDate.now() und jede Datenbankabfrage sind es nicht. Reine Funktionen sind trivial zu testen, man kann ihre Ergebnisse zwischenspeichern und sie gefahrlos parallel ausführen. Funktionale Programme sind deshalb meist so gebaut: ein großer Kern aus reinen Funktionen, außen eine dünne Schicht, die mit der Welt redet.
Was ist leichter zu erweitern?
Ein klassischer Unterschied zwischen objektorientiertem und funktionalem Stil zeigt sich beim Erweitern. Ein Programm rechnet mit geometrischen Formen: Kreis und Rechteck, mit Fläche und Umfang. Jetzt soll etwas dazukommen. Welche Stellen muss man anfassen?
Das ist das sogenannte Expression Problem: Objektorientierung macht neue Typen billig, funktionaler Stil neue Operationen. Welcher Fall häufiger vorkommt, entscheidet, welcher Stil an einer Stelle besser passt. Eine Benutzeroberfläche bekommt ständig neue Elemente, aber selten neue Grundoperationen – klassisch objektorientiert. Ein Compiler hat eine feste Menge von Sprachkonstrukten, aber ständig neue Analysen darüber – klassisch funktional.
Woher die Stile kommen
- 1957Fortran – imperativ, die erste weitverbreitete Hochsprache.
- 1958Lisp – funktional, Funktionen als Werte. Die Idee ist so alt wie die Programmierung selbst.
- 1967Simula aus Norwegen führt Klassen und Objekte ein; Smalltalk macht in den 1970ern alles zum Objekt.
- 1970erSQL und Prolog – deklarativ: Man beschreibt Ergebnis oder Regeln, nicht den Weg.
- 1995Java – konsequent objektorientiert.
- 2014Java 8 bringt Lambdas und Streams. Seitdem kommen immer mehr funktionale Bausteine dazu: Records (Java 16), sealed Interfaces (17), Pattern Matching im switch (21).
Für Entwickler übersetzt
// Funktional-algebraisch in modernem Java: Daten + Fallunterscheidung
sealed interface Form permits Kreis, Rechteck {}
record Kreis(double r) implements Form {}
record Rechteck(double b, double h) implements Form {}
static double flaeche(Form f) {
return switch (f) {
case Kreis k -> Math.PI * k.r() * k.r();
case Rechteck r -> r.b() * r.h();
}; // kein default nötig: Der Compiler kennt alle Fälle –
} // und meldet einen Fehler, sobald jemand eine neue Form hinzufügt| Stil | Werkzeuge in Java |
|---|---|
| Imperativ | Schleifen, Zuweisungen, Arrays – immer noch der schnellste Weg für heiße innere Schleifen |
| Objektorientiert | Klassen, Interfaces, Kapselung mit private, Polymorphie über überschriebene Methoden. Faustregel: Komposition vor Vererbung |
| Funktional | Lambdas, Methodenreferenzen, Function und Co., Streams, Optional, Records als unveränderliche Werte, List.of und toList() liefern unveränderliche Listen |
| Algebraische Datentypen | sealed Interfaces + Records + Pattern Matching: „ein Wert ist genau eines von diesen“ – mit Prüfung durch den Compiler |
| Deklarativ | Annotationen wie @Transactional oder @NotNull, Spring-Konfiguration, SQL und JPQL, Stream-Pipelines |
Häufige Irrtümer
parallelStream() hilft nur bei großen Datenmengen mit teurer Arbeit pro Element.