Texte zu Büchern

Effective Java – Zusammenfassung und Einordnung

Joshua Bloch, Effective Java (1. Auflage 2001, 2. Auflage 2008, 3. Auflage 2018)

Lesestand

Einleitung

Effective Java ist kein Buch über Design im Allgemeinen, sondern über das richtige Benutzen einer Sprache. Es sammelt 90 konkrete Ratschläge, wie man Java so schreibt, dass Code korrekt, robust, flexibel und verständlich ist. Von allen Büchern dieser Reihe ist es das praktischste und das am engsten an eine Technologie gebundene.

Autor und Kontext

Joshua Bloch hat bei Sun das Collections Framework entworfen, dazu java.math und Teile der Sprache wie Enums, Generics-Unterstützung in Bibliotheken, Annotationen und try-with-resources. Später war er Chief Java Architect bei Google und lehrt heute an der Carnegie Mellon University. Er schreibt also aus der Sicht von jemandem, der die Plattform selbst gebaut hat, und kommentiert mehrfach Fehler im JDK, an denen er beteiligt war.

AuflageJahrItemsJava-Stand
1.200157Java 1.4
2.200878Java 6, mit Generics, Enums, Annotationen
3.201890Java 9, mit Lambdas, Streams, Optional, Modulen

Diese Zusammenfassung bezieht sich auf die dritte Auflage.

Aufbau

Die 90 Items sind auf elf Kapitel verteilt: Objekte erzeugen und zerstören, Methoden aller Objekte, Klassen und Schnittstellen, Generics, Enums und Annotationen, Lambdas und Streams, Methoden, allgemeine Programmierung, Exceptions, Nebenläufigkeit und Serialisierung. Jedes Item ist ein eigenständiger kurzer Essay mit Begründung und Beispielen. Man kann das Buch von vorne lesen oder gezielt nachschlagen.

Grundhaltung

Drei Gedanken ziehen sich durch das ganze Buch:

  • API-Denken: Bloch schreibt aus der Perspektive eines Bibliotheksautors. Jede öffentliche Klasse ist eine API, die andere benutzen und auf die man sich festlegt. Was einmal öffentlich ist, lässt sich kaum zurücknehmen.
  • Korrektheit vor Performance: Erst richtig, dann klar, dann schnell. Optimierung nur nach Messung.
  • Fehler früh finden: Am besten zur Kompilierzeit, sonst so früh wie möglich zur Laufzeit. Daraus folgen Typsicherheit, Unveränderlichkeit, Prüfung von Parametern.

Bloch formuliert fast nie absolute Regeln. Viele Items enden mit Abwägungen und Ausnahmen, und der Titel vieler Items enthält Wörter wie „bevorzuge“, „erwäge“ oder „mit Bedacht“. Der Ton ist damit näher an Fowler und Ousterhout als an Martin.

Objekte erzeugen und zerstören (Items 1–9)

Wie Objekte entstehen, entscheidet über Lesbarkeit, Flexibilität und Speicherverhalten. Das bekannteste Kapitel des Buchs.

ItemRatschlag
1Statische Fabrikmethoden statt Konstruktoren erwägen
2Builder bei vielen Konstruktorparametern
3Singleton mit privatem Konstruktor oder als Enum
4Nicht instanziierbare Klassen mit privatem Konstruktor
5Dependency Injection statt fest verdrahteter Ressourcen
6Unnötige Objekte vermeiden
7Veraltete Objektreferenzen entfernen
8Finalizer und Cleaner vermeiden
9try-with-resources statt try-finally

Item 1: Statische Fabrikmethoden

Vorteile gegenüber Konstruktoren: Sie haben Namen, müssen nicht jedes Mal ein neues Objekt erzeugen, können einen Untertyp liefern und die konkrete Klasse je nach Eingabe wählen.

Optional<String> empty = Optional.empty();       // sprechender Name
List<String> list = List.of("a", "b");           // liefert unveränderliche, interne Implementierung
Boolean t = Boolean.valueOf(true);               // kein neues Objekt
EnumSet<Day> weekend = EnumSet.of(SAT, SUN);    // wählt je nach Größe eine von zwei Implementierungen

Bloch nennt auch übliche Namenskonventionen: of, from, valueOf, getInstance, newInstance, getType, newType. Der Nachteil: Statische Fabriken sind in der Javadoc schwerer zu finden als Konstruktoren.

Item 2: Builder

Bei vielen, teils optionalen Parametern sind Konstruktorketten unlesbar und JavaBeans mit Settern nicht unveränderlich. Der Builder kombiniert beides:

var pizza = new Pizza.Builder(Size.LARGE)
    .addTopping(Topping.HAM)
    .addTopping(Topping.MUSHROOM)
    .extraCheese()
    .build();          // Validierung hier, Objekt danach unveränderlich

Das ist die Variante des Builders, die heute in Java vorherrscht, und nicht die GoF-Form mit Director.

Item 3: Singleton als Enum

Blochs Empfehlung: ein Enum mit einem einzigen Element. Es ist automatisch threadsicher, serialisierungsfest und gegen Reflection-Angriffe geschützt. Er sagt aber auch, dass Singletons das Testen erschweren, und Item 5 empfiehlt ohnehin Dependency Injection.

Item 5: Dependency Injection

// schlecht: fest verdrahtet, untestbar
public class SpellChecker {
    private static final Lexicon dictionary = new GermanLexicon();
}

// besser: injiziert
public class SpellChecker {
    private final Lexicon dictionary;
    public SpellChecker(Lexicon dictionary) { this.dictionary = Objects.requireNonNull(dictionary); }
}

Genau Feathers‘ Parameterize Constructor, nur aus Design- statt aus Testsicht.

Items 6 und 7: Speicher

Item 6 warnt vor versehentlicher Objekterzeugung, etwa durch Autoboxing in Schleifen (Long sum statt long sum) oder wiederholtes Kompilieren regulärer Ausdrücke. Item 7 zeigt, dass Java trotz Garbage Collector Speicherlecks hat: Ein selbstgebauter Stack, der beim pop die Referenz nicht auf null setzt, hält Objekte am Leben. Typische Quellen sind eigene Collections, Caches und Listener, die nie abgemeldet werden.

Items 8 und 9: Ressourcen

Finalizer sind unzuverlässig, langsam und gefährlich, Cleaner kaum besser. Ressourcen schließt man mit try-with-resources, das Bloch selbst in die Sprache gebracht hat:

try (var in = Files.newBufferedReader(path);
     var out = Files.newBufferedWriter(target)) {
    in.transferTo(out);
}

Einordnung

Items 1, 2, 5 und 9 sind heute Standardwissen und in modernen Bibliotheken überall zu sehen. Item 3 ist durch DI-Container weitgehend überholt, Bloch deutet das selbst an. Item 8 ist inzwischen offizielle Plattformpolitik: Finalization ist seit Java 18 zur Entfernung vorgesehen.

Methoden aller Objekte (Items 10–14)

Die Methoden von Object haben Verträge, auf die sich das ganze JDK verlässt, vor allem die Collections. Wer sie falsch implementiert, bekommt subtile Fehler, die weit entfernt auftreten.

ItemRatschlag
10Beim Überschreiben von equals den allgemeinen Vertrag einhalten
11Mit equals immer auch hashCode überschreiben
12toString immer überschreiben
13clone mit Bedacht überschreiben
14Comparable implementieren, wenn es eine natürliche Ordnung gibt

Items 10 und 11: equals und hashCode

Der equals-Vertrag verlangt Reflexivität, Symmetrie, Transitivität, Konsistenz und dass nichts gleich null ist. Bloch zeigt, wie leicht man ihn bricht, etwa mit einer Unterklasse, die ein Feld hinzufügt und in equals berücksichtigt. Seine bekannte Folgerung: Es gibt keine Möglichkeit, eine instanziierbare Klasse zu erweitern, ein Wertfeld hinzuzufügen und den Vertrag zu erhalten. Der Ausweg ist Komposition statt Vererbung.

Vergisst man hashCode, funktionieren HashMap und HashSet nicht mehr: Zwei gleiche Objekte landen in verschiedenen Buckets.

// vor Java 16: Handarbeit nach Blochs Rezept
@Override public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof PhoneNumber pn)) return false;
    return area == pn.area && prefix == pn.prefix && line == pn.line;
}
@Override public int hashCode() { return Objects.hash(area, prefix, line); }

// ab Java 16: alles automatisch, korrekt, unveränderlich
public record PhoneNumber(short area, short prefix, short line) {}

Item 12: toString

Eine gute toString-Ausgabe erleichtert Debugging und Logging enorm. Bloch empfiehlt, alle interessanten Informationen auszugeben und zu dokumentieren, ob das Format verbindlich ist. Records erledigen auch das automatisch.

Item 13: clone

Blochs schärfste Kritik am JDK: Cloneable ist ein misslungenes Interface. Es enthält keine Methode, verändert aber das Verhalten einer geschützten Methode in Object, umgeht Konstruktoren und kollidiert mit finalen Feldern. Seine Empfehlung: Kopierkonstruktor oder Kopierfabrik.

public Invoice(Invoice original) { … }                     // Kopierkonstruktor
public static Invoice copyOf(Invoice original) { … }       // Kopierfabrik

Nur Arrays seien eine Ausnahme, bei denen clone sinnvoll funktioniert.

Item 14: Comparable

Wer eine natürliche Ordnung hat, sollte Comparable implementieren, damit sortierte Collections und Arrays.sort funktionieren. Bloch empfiehlt die Comparator-Konstruktionsmethoden aus Java 8 statt Subtraktion, die bei Überläufen falsche Ergebnisse liefert:

private static final Comparator<PhoneNumber> COMPARATOR =
    Comparator.comparingInt(PhoneNumber::area)
              .thenComparingInt(PhoneNumber::prefix)
              .thenComparingInt(PhoneNumber::line);

public int compareTo(PhoneNumber pn) { return COMPARATOR.compare(this, pn); }

Einordnung

Dieses Kapitel ist durch Records am stärksten gealtert, im guten Sinn: Für reine Datenträger sind Items 10 bis 12 automatisch erfüllt. Die Prinzipien bleiben aber relevant für Entities, die Gleichheit über Identität definieren, und für Klassen, die keine Records sein können. Die Kritik an clone hat sich vollständig durchgesetzt.

Klassen und Schnittstellen (Items 15–25)

Das Kapitel mit den meisten Design-Prinzipien. Hier deckt sich Bloch am stärksten mit GoF, Ousterhout und Fowler.

ItemRatschlag
15Zugriff auf Klassen und Member minimieren
16In öffentlichen Klassen Zugriffsmethoden statt öffentlicher Felder
17Veränderlichkeit minimieren
18Komposition statt Vererbung
19Für Vererbung entwerfen und dokumentieren, oder sie verbieten
20Interfaces statt abstrakter Klassen
21Interfaces für die Nachwelt entwerfen
22Interfaces nur zur Definition von Typen
23Klassenhierarchien statt Klassen mit Typkennung
24Statische Member-Klassen statt nicht-statischer
25Nur eine Top-Level-Klasse pro Quelldatei

Item 15: Zugriff minimieren

Information Hiding im Sinne von Parnas und Ousterhout: Jede Klasse und jedes Member so unzugänglich wie möglich. Öffentliche Klassen sollten fast nie öffentliche Felder haben, außer Konstanten. Seit Java 9 kommen Module als zusätzliche Ebene dazu: Pakete sind nur sichtbar, wenn ein Modul sie exportiert.

Item 17: Unveränderlichkeit

Blochs fünf Regeln für unveränderliche Klassen: keine Mutatoren, Klasse nicht erweiterbar, alle Felder final, alle Felder private, exklusiver Zugriff auf veränderliche Komponenten. Unveränderliche Objekte sind einfach, threadsicher und frei teilbar. Sein Leitsatz: Klassen sollten unveränderlich sein, solange es keinen sehr guten Grund für das Gegenteil gibt.

public record Complex(double re, double im) {
    public Complex plus(Complex c)  { return new Complex(re + c.re, im + c.im); }
    public Complex times(Complex c) {
        return new Complex(re * c.re - im * c.im, re * c.im + im * c.re);
    }
}

Records setzen vier der fünf Regeln automatisch um. Die fünfte, keine veränderlichen Komponenten herauszugeben, bleibt Handarbeit, etwa mit List.copyOf im kompakten Konstruktor.

Items 18 und 19: Komposition und Vererbung

Blochs Beispiel ist ein InstrumentedHashSet, das zählen soll, wie viele Elemente hinzugefügt wurden. Es erbt von HashSet und überschreibt add und addAll. Weil HashSet.addAll intern add aufruft, wird doppelt gezählt. Die Unterklasse hängt von einem Implementierungsdetail der Oberklasse ab. Die Lösung ist eine Wrapper-Klasse, die das Set enthält statt davon zu erben, also ein Decorator.

Item 19 zieht die Konsequenz: Eine Klasse, die für Vererbung gedacht ist, muss dokumentieren, welche überschreibbaren Methoden sie selbst aufruft. Alle anderen Klassen sollte man final machen.

Items 20 und 21: Interfaces

Interfaces sind flexibler als abstrakte Klassen: Eine Klasse kann viele implementieren, und sie lassen sich nachträglich an bestehende Klassen anfügen. Für gemeinsame Implementierung kombiniert man ein Interface mit einer Skeletal Implementation wie AbstractList. Seit Java 8 helfen Default-Methoden.

Item 21 warnt aber: Default-Methoden können bestehende Implementierungen brechen. Bloch zeigt das an Collection.removeIf, das in synchronisierten Wrappern ohne Sperre läuft.

Item 23: Hierarchien statt Typkennung

// schlecht: "tagged class"
class Figure {
    enum Shape { RECTANGLE, CIRCLE }
    final Shape shape;
    double length, width;   // nur für RECTANGLE
    double radius;          // nur für CIRCLE
    double area() { return switch (shape) { case RECTANGLE -> length * width; case CIRCLE -> Math.PI * radius * radius; }; }
}

// Java 17+: sealed Hierarchie mit Records
sealed interface Figure permits Rectangle, Circle { double area(); }
record Rectangle(double length, double width) implements Figure { public double area() { return length * width; } }
record Circle(double radius) implements Figure { public double area() { return Math.PI * radius * radius; } }

Das ist Fowlers Replace Type Code with Subclasses, und mit sealed Types auch für den prozeduralen Stil mit switch sicher.

Einordnung

Items 15, 17, 18 und 23 gehören zum Wertvollsten im Buch und sind durch Records und sealed Types einfacher umzusetzen als je zuvor. Item 19 hat Kotlin direkt übernommen: Dort sind Klassen standardmäßig final.

Generics (Items 26–33)

Generics verschieben Typfehler von der Laufzeit zur Kompilierzeit. Weil Java sie per Type Erasure umsetzt, gibt es aber viele Fallstricke, und dieses Kapitel ist für viele Entwickler das schwierigste.

ItemRatschlag
26Keine Raw Types verwenden
27Unchecked-Warnungen beseitigen
28Listen statt Arrays
29Generische Typen bevorzugen
30Generische Methoden bevorzugen
31Bounded Wildcards für flexiblere APIs
32Generics und Varargs mit Bedacht kombinieren
33Typsichere heterogene Container erwägen

Items 26 und 27: Raw Types und Warnungen

List ohne Typparameter ist ein Überbleibsel aus der Zeit vor Java 5. Damit verliert man jede Typsicherheit. Unchecked-Warnungen soll man nicht ignorieren, sondern beseitigen. Wo das nicht geht und man sicher ist, dass der Code typsicher ist, unterdrückt man die Warnung mit @SuppressWarnings("unchecked") im kleinstmöglichen Gültigkeitsbereich und kommentiert, warum.

Item 28: Listen statt Arrays

Arrays sind kovariant und zur Laufzeit typisiert, Generics invariant und nur zur Kompilierzeit. Das passt schlecht zusammen:

Object[] arr = new Long[1];
arr[0] = "Text";            // kompiliert, wirft ArrayStoreException zur Laufzeit

List<Object> list = new ArrayList<Long>();   // kompiliert nicht, Fehler sofort

Item 31: PECS

Die wohl bekannteste Merkregel des Buchs: Producer Extends, Consumer Super. Liefert ein Parameter Werte, nimmt man ? extends T. Nimmt er Werte auf, ? super T.

// Quelle produziert T, Ziel konsumiert T
public static <T> void copy(Collection<? super T> target, Collection<? extends T> source) {
    for (T t : source) target.add(t);
}

// funktioniert mit List<Number> als Ziel und List<Integer> als Quelle
copy(numbers, integers);

// klassisches Beispiel aus dem JDK
public static <T extends Comparable<? super T>> T max(Collection<? extends T> coll)

Bloch betont: Wenn Nutzer einer API über Wildcards nachdenken müssen, ist die API wahrscheinlich falsch. Richtig eingesetzte Wildcards sind für Nutzer unsichtbar, sie funktionieren einfach.

Item 32: Generics und Varargs

Varargs erzeugen intern ein Array, was mit generischen Typen kollidiert und zu Heap Pollution führen kann. Für Methoden, die das sicher handhaben, gibt es @SafeVarargs. List.of(E... elements) ist so annotiert.

Item 33: Typsichere heterogene Container

Eine elegante Technik: Man verwendet das Class-Objekt als Schlüssel, um verschiedene Typen typsicher in einem Container zu speichern.

public class Favorites {
    private final Map<Class<?>, Object> favorites = new HashMap<>();
    public <T> void put(Class<T> type, T instance) { favorites.put(Objects.requireNonNull(type), type.cast(instance)); }
    public <T> T get(Class<T> type) { return type.cast(favorites.get(type)); }
}

favorites.put(String.class, "Java");
String s = favorites.get(String.class);   // kein Cast nötig

Einordnung

Das Kapitel ist zeitlos, weil sich die Generics in Java seit 2004 kaum geändert haben. Project Valhalla, das Generics über primitive Typen ermöglichen soll, ist noch in Arbeit. PECS ist bis heute die beste Kurzformel für Wildcards. Die Komplexität des Kapitels ist zugleich eine indirekte Kritik an Javas Generics-Design: Kotlin mit Deklarationsvarianz über in und out oder C# mit Reified Generics lösen dieselben Probleme einfacher.

Enums und Annotationen (Items 34–41)

Java-Enums sind viel mächtiger als in den meisten Sprachen: vollwertige Klassen mit Feldern, Methoden und eigenem Verhalten pro Konstante. Bloch hat sie mit entworfen und zeigt hier, was sie können.

ItemRatschlag
34Enums statt int-Konstanten
35Instanzfelder statt ordinal()
36EnumSet statt Bitfelder
37EnumMap statt Indexierung über ordinal()
38Erweiterbare Enums über Interfaces nachbilden
39Annotationen statt Namenskonventionen
40@Override konsequent verwenden
41Marker-Interfaces für Typen verwenden

Item 34: Enums mit Verhalten

public enum Operation {
    PLUS("+")   { public double apply(double x, double y) { return x + y; } },
    MINUS("-")  { public double apply(double x, double y) { return x - y; } },
    TIMES("*")  { public double apply(double x, double y) { return x * y; } };

    private final String symbol;
    Operation(String symbol) { this.symbol = symbol; }
    public abstract double apply(double x, double y);
    @Override public String toString() { return symbol; }
}

Das ist im Kleinen GoFs Strategy oder State: Jede Konstante hat ihr eigenes Verhalten, und der Compiler erzwingt, dass jede apply implementiert. Alternativ geht es kompakter mit einem Lambda im Konstruktor: PLUS("+", (x, y) -> x + y).

Items 35 bis 37: Weg von ordinal()

ordinal() gibt die Position einer Konstante zurück. Bloch rät dringend davon ab, darauf Logik aufzubauen, weil sich die Reihenfolge ändern kann. Stattdessen:

// Item 36: EnumSet statt Bitfeld
EnumSet<Style> styles = EnumSet.of(Style.BOLD, Style.ITALIC);

// Item 37: EnumMap statt Array mit ordinal()-Index
Map<Lifecycle, Set<Plant>> byLifecycle = new EnumMap<>(Lifecycle.class);

// mit Streams
Map<Lifecycle, Set<Plant>> grouped = garden.stream()
    .collect(groupingBy(Plant::lifecycle, () -> new EnumMap<>(Lifecycle.class), toSet()));

EnumSet und EnumMap sind intern Bitvektoren bzw. Arrays, also genauso schnell wie die Handarbeit, aber typsicher.

Item 38: Erweiterbare Enums

Enums lassen sich nicht vererben. Wo Erweiterbarkeit nötig ist, etwa bei Operationen, die Nutzer ergänzen sollen, definiert man ein Interface, das das Enum implementiert. Andere Enums können es ebenfalls implementieren.

Items 39 bis 41: Annotationen

Item 39 begründet, warum JUnit 4 mit @Test besser war als JUnit 3, das Testmethoden am Namenspräfix test erkannte: Tippfehler fallen sofort auf, und Annotationen können Parameter tragen. Item 40 ist kurz und eindeutig: @Override an jede überschreibende Methode, damit der Compiler Fehler wie eine falsche Signatur meldet. Item 41 grenzt Marker-Interfaces wie Serializable von Marker-Annotationen ab: Interfaces definieren einen Typ, den der Compiler prüfen kann, Annotationen nicht.

Einordnung

Das Kapitel ist sehr gut gealtert. Enums mit Verhalten sind eine der stärksten Eigenschaften von Java. Mit sealed Interfaces gibt es seit Java 17 eine Ergänzung: Enums für eine feste Menge gleichartiger Konstanten, sealed Types für eine feste Menge verschiedenartiger Fälle mit unterschiedlichen Daten. Beide funktionieren mit erschöpfendem switch.

Lambdas und Streams (Items 42–48)

Das Kapitel ist neu in der dritten Auflage. Bloch ist dabei auffällig vorsichtig: Er begrüßt Lambdas und Streams, warnt aber ausdrücklich vor Übertreibung.

ItemRatschlag
42Lambdas statt anonymer Klassen
43Methodenreferenzen statt Lambdas, wo sie klarer sind
44Standard-Funktionsinterfaces bevorzugen
45Streams mit Bedacht einsetzen
46In Streams Funktionen ohne Seiteneffekte bevorzugen
47Collection statt Stream als Rückgabetyp
48Parallele Streams mit Vorsicht

Items 42 bis 44

Lambdas sind kürzer und klarer als anonyme Klassen, sollen aber kurz bleiben: Bloch nennt eine bis höchstens drei Zeilen. Ein Lambda hat keinen Namen und keine Dokumentation. Wird es länger, gehört es in eine benannte Methode, auf die man per Methodenreferenz verweist.

Item 44 empfiehlt, die 43 Interfaces aus java.util.function zu nutzen, statt eigene zu definieren. Ein eigenes Funktionsinterface lohnt sich nur, wenn es einen klaren Namen, einen eigenen Vertrag oder nützliche Default-Methoden hat, wie Comparator. Eigene Funktionsinterfaces sollen @FunctionalInterface tragen.

Item 45: Streams mit Bedacht

Das wichtigste Item des Kapitels. Bloch zeigt drei Versionen eines Anagramm-Programms: iterativ, als langer Stream und als Mischung. Die Mischung ist am lesbarsten.

// zu viel Stream
words.collect(groupingBy(w -> w.chars().sorted()
        .collect(StringBuilder::new, (sb, c) -> sb.append((char) c), StringBuilder::append).toString()))
    .values().stream().filter(g -> g.size() >= minSize)
    .map(g -> g.size() + ": " + g).forEach(System.out::println);

// besser: Hilfsmethode mit Namen, Stream nur für den Fluss
words.collect(groupingBy(Anagrams::alphabetize))
    .values().stream()
    .filter(group -> group.size() >= minSize)
    .forEach(group -> System.out.println(group.size() + ": " + group));

Seine Faustregeln: Streams eignen sich gut für Transformieren, Filtern, Zusammenfassen und Gruppieren. Schleifen sind besser, wenn man lokale Variablen ändern, vorzeitig mit return aussteigen, geprüfte Exceptions werfen oder auf mehrere Zwischenwerte gleichzeitig zugreifen muss. Und: char-Werte nicht mit Streams verarbeiten, weil chars() einen IntStream liefert.

Item 46: Keine Seiteneffekte

// falsch: forEach als Schleife mit Seiteneffekt
Map<String, Long> freq = new HashMap<>();
words.forEach(w -> freq.merge(w.toLowerCase(), 1L, Long::sum));

// richtig: Collector
Map<String, Long> freq = words.collect(groupingBy(String::toLowerCase, counting()));

forEach soll nur das Ergebnis ausgeben, nicht die Berechnung durchführen.

Item 47: Collection statt Stream zurückgeben

Ein Stream lässt sich nur einmal durchlaufen und nicht mit for-each verwenden. Wer eine API schreibt, sollte deshalb meist eine Collection oder List zurückgeben, aus der Aufrufer bei Bedarf selbst einen Stream machen.

Item 48: Parallele Streams

.parallel() ist kein Turbo. Bloch zeigt ein Beispiel, bei dem ein paralleler Stream das Programm nie beenden lässt. Parallelisierung lohnt nur bei großen Datenmengen, gut teilbaren Quellen wie ArrayList oder Arrays, unabhängigen Operationen und messbarem Gewinn.

Einordnung

Dieses Kapitel ist eine nötige Bremse gegen den Stream-Enthusiasmus nach Java 8 und deckt sich mit der Gegenstimme zu Fowlers Replace Loop with Pipeline. Seit 2018 kamen nützliche Ergänzungen hinzu, etwa Stream.toList() in Java 16, mapMulti und Stream Gatherers für eigene Zwischenoperationen, final seit Java 24.

Methoden (Items 49–56)

Dieses Kapitel behandelt Methoden als kleinste Einheit einer API: Parameter, Signaturen, Rückgabewerte, Dokumentation.

ItemRatschlag
49Parameter auf Gültigkeit prüfen
50Defensive Kopien, wo nötig
51Methodensignaturen sorgfältig entwerfen
52Überladen mit Bedacht
53Varargs mit Bedacht
54Leere Collections oder Arrays statt null zurückgeben
55Optional mit Bedacht zurückgeben
56Dokumentationskommentare für alle öffentlichen API-Elemente

Items 49 und 50: Prüfen und Kopieren

public final class Period {
    private final Instant start, end;

    public Period(Instant start, Instant end) {
        this.start = Objects.requireNonNull(start);   // Item 49
        this.end = Objects.requireNonNull(end);
        if (start.isAfter(end)) throw new IllegalArgumentException(start + " nach " + end);
    }
}

Blochs ursprüngliches Beispiel nutzt das veränderliche java.util.Date. Weil Aufrufer das Datum nach der Übergabe ändern konnten, brauchte es defensive Kopien in Konstruktor und Gettern. Mit java.time ist das Problem verschwunden, weil die Typen unveränderlich sind. Für Listen gilt es weiter: List.copyOf(input).

Item 51: Signaturen

Methodennamen nach Konventionen wählen, nicht zu viele Komfortmethoden anbieten, höchstens vier Parameter, Parameter mit gleichem Typ vermeiden, Interfaces statt Klassen als Parametertypen, Enums mit zwei Werten statt boolean. Der letzte Punkt entspricht Martins und Fowlers Kritik an Flag-Argumenten.

Items 54 und 55: Kein null, Optional mit Maß

Leere Collections statt null zurückzugeben ist unumstritten: List.of() kostet nichts. Bei Optional ist Bloch vorsichtig: gut als Rückgabewert, wenn ein Ergebnis fehlen kann und der Aufrufer das behandeln muss. Aber nicht als Feld, nicht als Parameter, nicht in Collections und nicht für Collections, die stattdessen leer sein können.

Item 56: Dokumentation

Jede öffentliche Klasse, Schnittstelle, Methode und jedes Feld bekommt Javadoc. Der Kommentar beschreibt den Vertrag zwischen Methode und Aufrufer: was sie tut, nicht wie. Dazu Vorbedingungen, Nachbedingungen, Seiteneffekte und Threadsicherheit. Das ist fast wörtlich Ousterhouts Schnittstellenkommentar und ein deutlicher Kontrast zu Martins Kommentarskepsis.

Allgemeine Programmierung (Items 57–68)

Ein Sammelkapitel mit vielen kleinen, aber nützlichen Regeln.

ItemRatschlagKern
57Gültigkeitsbereich lokaler Variablen minimierenan der Stelle der ersten Verwendung deklarieren
58for-each statt klassischer for-Schleifeweniger Fehler, klarer
59Bibliotheken kennen und nutzenetwa ThreadLocalRandom, String.repeat, Files
60float und double meiden, wenn exakte Werte nötig sindfür Geld BigDecimal, int oder long in Cent
61Primitive statt Wrapper-Typen== auf Integer vergleicht Referenzen
62Strings meiden, wo andere Typen passenkein String als Schlüssel für zusammengesetzte Werte
63String-Verkettung in Schleifen vermeidenStringBuilder
64Auf Objekte über ihre Interfaces verweisenList<X> x = new ArrayList<>()
65Interfaces statt ReflectionReflection nur zum Erzeugen, dann über Interface nutzen
66Native Methoden mit Bedachtheute eher Foreign Function API
67Mit Bedacht optimierengute Programme statt schneller Programme schreiben, dann messen
68Allgemeine Namenskonventionen einhaltenJava-Standardkonventionen

Items 60 und 61 in Code

System.out.println(1.03 - 0.42);                    // 0.6100000000000001
System.out.println(new BigDecimal("1.03").subtract(new BigDecimal("0.42")));   // 0.61

Integer a = 1000, b = 1000;
System.out.println(a == b);          // false: Referenzvergleich außerhalb des Caches
System.out.println(a.equals(b));     // true

Item 64 gegen Ousterhout

Item 64 ist genau der Punkt, den Ousterhout kritisiert: List<X> x = new ArrayList<>() verberge die tatsächliche Klasse. Bloch argumentiert umgekehrt: Wer über das Interface verweist, kann die Implementierung später austauschen. Mit var für lokale Variablen hat sich der Streit im Alltag teilweise erledigt, bei Feldern, Parametern und Rückgabetypen gilt weiter Blochs Regel.

Item 67: Optimierung

Bloch zitiert hier mehrere Klassiker zur verfrühten Optimierung, etwa Knuth, und fasst zusammen: Gute Architektur und gute APIs sind wichtiger als Mikrooptimierung, weil sich schlechte Designentscheidungen später kaum korrigieren lassen. Ein öffentlicher veränderlicher Typ, der defensive Kopien erzwingt, kostet für immer Leistung. Gemessen wird mit einem Profiler oder JMH, nicht nach Gefühl.

Exceptions (Items 69–77)

Exceptions sind für Ausnahmefälle, nicht für normalen Kontrollfluss. Innerhalb dieser Regel unterscheidet Bloch sorgfältig, welche Art von Exception wofür passt.

ItemRatschlag
69Exceptions nur für Ausnahmesituationen
70Checked Exceptions für behebbare Situationen, Runtime Exceptions für Programmierfehler
71Unnötige Checked Exceptions vermeiden
72Standard-Exceptions bevorzugen
73Exceptions passend zur Abstraktion werfen
74Alle geworfenen Exceptions dokumentieren
75Fehlerinformationen in die Meldung aufnehmen
76Fehleratomarität anstreben
77Exceptions nicht ignorieren

Item 69: Nicht für Kontrollfluss

// missbraucht: Ende des Arrays über Exception erkennen
try {
    int i = 0;
    while (true) range[i++].climb();
} catch (ArrayIndexOutOfBoundsException e) { }

// richtig
for (Mountain m : range) m.climb();

Die erste Variante ist langsamer, schwerer zu lesen und verschluckt echte Fehler, die zufällig dieselbe Exception werfen.

Items 70 und 71: Checked oder unchecked

Blochs Unterscheidung:

ArtWannBeispiel
Checked ExceptionDer Aufrufer kann sinnvoll reagieren, und die Situation ist nicht durch Prüfung vermeidbar.IOException beim Lesen einer Datei
Runtime ExceptionProgrammierfehler, VertragsverletzungIllegalArgumentException, NullPointerException
ErrorReserviert für die JVMOutOfMemoryError

Item 71 schränkt das ein: Checked Exceptions belasten jeden Aufrufer. Lohnt sich die Behandlung nicht, oder kann der Aufrufer nichts anderes tun als abbrechen, dann ist eine Runtime Exception oder ein Optional besser.

Kontrast zu Clean Code: Feathers lehnt Checked Exceptions in Clean Code fast grundsätzlich ab, weil sie das Open-Closed-Prinzip verletzen. Bloch verteidigt sie für echte, behebbare Fehler, warnt aber vor Übermaß. In der Praxis hat sich eher Feathers‘ Sicht durchgesetzt: Frameworks wie Spring wickeln Checked Exceptions fast überall in Runtime Exceptions ein, und keine neuere JVM-Sprache hat sie übernommen. Lambdas und Streams haben das Problem verschärft, weil Funktionsinterfaces keine Checked Exceptions erlauben.

Item 72: Standard-Exceptions

Eigene Exception-Klassen nur, wenn die Standards nicht passen. Die wichtigsten:

ExceptionAnlass
IllegalArgumentExceptionungültiger Parameterwert
IllegalStateExceptionObjektzustand erlaubt den Aufruf nicht
NullPointerExceptionnull, wo es verboten ist
IndexOutOfBoundsExceptionIndex außerhalb des Bereichs
ConcurrentModificationExceptiongleichzeitige Änderung erkannt
UnsupportedOperationExceptionOperation nicht unterstützt

Item 73: Exception Translation

Höhere Schichten fangen Exceptions niedriger Schichten und werfen eigene, die zur eigenen Abstraktion passen, mit der Ursache als cause.

try {
    return repository.load(id);
} catch (SQLException e) {
    throw new OrderNotFoundException(id, e);   // Abstraktion des Aufrufers, Ursache erhalten
}

Das ist im Kleinen Evans‘ Anticorruption Layer und Feathers‘ Rat aus Clean Code, Exceptions nach den Bedürfnissen des Aufrufers zu definieren.

Items 75 bis 77

  • Meldungen mit Kontext: alle Parameter und Felder, die zum Fehler beigetragen haben, aber keine Passwörter oder Schlüssel.
  • Fehleratomarität: Nach einer Exception soll ein Objekt im Zustand vor dem Aufruf sein. Am einfachsten mit unveränderlichen Objekten, sonst durch Prüfen vor dem Ändern.
  • Nicht ignorieren: Ein leerer catch-Block ist fast immer ein Fehler. Wenn Ignorieren richtig ist, heißt die Variable ignored und ein Kommentar erklärt, warum.

Nebenläufigkeit (Items 78–84)

Blochs Kapitel ist knapp, aber präziser als das entsprechende Kapitel in Clean Code. Es setzt auf java.util.concurrent statt auf eigene Synchronisation.

ItemRatschlag
78Zugriff auf gemeinsam genutzte veränderliche Daten synchronisieren
79Übermäßige Synchronisation vermeiden
80Executors, Tasks und Streams statt eigener Threads
81Concurrency-Utilities statt wait und notify
82Threadsicherheit dokumentieren
83Lazy Initialization mit Bedacht
84Nicht vom Thread-Scheduler abhängig sein

Item 78: Sichtbarkeit

Synchronisation sorgt nicht nur für gegenseitigen Ausschluss, sondern auch dafür, dass Änderungen eines Threads für andere sichtbar werden. Blochs Beispiel: Ein Thread soll stoppen, wenn ein boolean-Feld gesetzt wird. Ohne volatile oder Synchronisation kann die JVM die Prüfung aus der Schleife herausoptimieren, und der Thread läuft ewig.

private static volatile boolean stopRequested;   // ohne volatile: möglicherweise Endlosschleife

var worker = new Thread(() -> { while (!stopRequested) work(); });
worker.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;

Sein Rat zuerst: Veränderliche Daten gar nicht teilen. Unveränderliche Daten teilen oder veränderliche auf einen Thread beschränken.

Item 79: Fremde Methoden nicht in synchronisierten Blöcken

Wer in einem synchronized-Block einen Callback oder eine überschreibbare Methode aufruft, riskiert Deadlocks und Exceptions, weil er nicht weiß, was dieser Code tut. Bloch zeigt das an einem beobachtbaren Set, also einem Observer, dessen Listener sich selbst abmelden. Die Lösung: Liste kopieren oder CopyOnWriteArrayList verwenden und die Callbacks außerhalb des Locks aufrufen.

Items 80 und 81: Höhere Abstraktionen

try (ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor()) {   // Java 21
    List<Future<Price>> prices = suppliers.stream()
        .map(s -> exec.submit(() -> s.quote(product)))
        .toList();
    …
}

Statt wait und notify nutzt man ConcurrentHashMap, BlockingQueue, CountDownLatch oder CompletableFuture. Bloch empfiehlt ConcurrentHashMap auch ausdrücklich statt Collections.synchronizedMap.

Item 82: Threadsicherheit dokumentieren

Bloch schlägt Stufen vor, die jede Klasse angeben sollte: unveränderlich, bedingungslos threadsicher, bedingt threadsicher, nicht threadsicher und threadfeindlich. Das Schlüsselwort synchronized in einer Signatur ist keine Dokumentation, weil es ein Implementierungsdetail ist.

Item 83: Lazy Initialization

Meist ist normale Initialisierung besser. Wenn Lazy Initialization nötig ist: für statische Felder das Holder-Class-Idiom, für Instanzfelder Double-Checked Locking mit volatile.

private static class FieldHolder { static final Config config = loadConfig(); }
static Config config() { return FieldHolder.config; }   // threadsicher, ohne Synchronisation

Serialisierung (Items 85–90)

Das letzte Kapitel ist im Kern eine Warnung. Java-Serialisierung ist eine der größten Sicherheitslücken der Plattform: Beim Deserialisieren werden Objekte ohne Konstruktor erzeugt, und manipulierte Byteströme können beliebigen Code ausführen.

Blochs Rat in Item 85 ist ungewöhnlich deutlich: Der beste Weg, Serialisierungsangriffe zu vermeiden, ist, niemals etwas zu deserialisieren. Stattdessen plattformübergreifende Formate wie JSON oder Protocol Buffers.

Die Items 86 bis 90 beschreiben, wie man Serialisierung absichert, wenn man sie doch braucht: eigene serialisierte Form, defensives readObject, Enums für Instanzkontrolle und vor allem das Serialization-Proxy-Pattern, bei dem eine private statische Klasse die logische Form des Objekts serialisiert und beim Lesen über den normalen Konstruktor wieder aufbaut.

Einordnung

Das Nebenläufigkeitskapitel ist weitgehend gültig, wird aber durch Virtual Threads (Java 21) und Structured Concurrency (in Arbeit) ergänzt. Bei Virtual Threads gilt teils anderes: Thread-Pools sind dafür nicht nötig, und synchronized konnte bis Java 24 Virtual Threads an ihren Trägerthread binden. Das Serialisierungskapitel hat sich vollständig bestätigt. Records werden ohnehin sicherer serialisiert, weil sie beim Deserialisieren den kanonischen Konstruktor aufrufen, was im Kern Blochs Proxy-Idee ist.

Was Java seit 2018 verändert hat

Die dritte Auflage beschreibt Java 9. Seither hat Java mehr neue Sprachmittel bekommen als in den 15 Jahren davor. Fast alle bestätigen Blochs Ratschläge, indem sie sie in die Sprache einbauen.

NeuerungJavaBetroffene ItemsWirkung
var1057, 64Lokale Variablen kürzer, Interface-Typ für lokale Variablen seltener nötig
Switch Expressions1434, 23Erschöpfende switch-Ausdrücke statt Anweisungen
Text Blocks1562, 63Mehrzeilige Strings ohne Verkettung
Records1610, 11, 12, 17, 50equals, hashCode, toString und Unveränderlichkeit automatisch
Pattern Matching für instanceof1610kürzere equals-Implementierungen
Stream.toList()1645–47unveränderliche Liste direkt aus Stream
Sealed Classes1719, 23, 38kontrollierte Hierarchien, Alternative zu Tagged Classes und Visitor
Finalization veraltet188Blochs Warnung wird offizielle Politik
Virtual Threads2180, 81, 83Millionen leichtgewichtiger Threads, Pools seltener nötig
Record Patterns, Pattern Matching für switch2123, 34Datenstrukturen direkt zerlegen und unterscheiden
Sequenced Collections2158, 59getFirst(), getLast(), reversed() einheitlich
Foreign Function & Memory API2266Ersatz für JNI und native Methoden
Stream Gatherers2445eigene Zwischenoperationen für Streams
Kein Pinning von Virtual Threads bei synchronized2478–81synchronized wieder unproblematisch mit Virtual Threads

Was gleich geblieben ist

Einige Kapitel sind praktisch unverändert gültig, weil sie Konzepte statt Sprachmittel betreffen: Generics (26–33), die meisten Exception-Items (69–77), Minimierung von Zugriff (15), Komposition statt Vererbung (18), Dokumentation (56) und Optimierung (67).

Was überholt ist

ItemWarum
3 (Singleton)DI-Container übernehmen die Rolle, Bloch deutet das selbst an
10–12 (equals, hashCode, toString)für Datenträger durch Records erledigt
13 (clone)Kritik hat sich durchgesetzt, kaum noch relevant
50 (defensive Kopien)für Datum und Zeit durch java.time erledigt, für Collections weiter nötig
86–90 (Serialisierung)die meisten Projekte serialisieren ohnehin JSON oder Protobuf

Eine vierte Auflage von Effective Java ist nach meinem Kenntnisstand nicht erschienen. Die Lücke füllen teils neuere Bücher wie Modern Java in Action und die JEP-Dokumente selbst, die oft auf Bloch Bezug nehmen.

Kritik und Einordnung

Effective Java gilt seit 25 Jahren als das wichtigste Java-Buch überhaupt. Kritik richtet sich weniger gegen die Inhalte als gegen den Zuschnitt.

Stärken

  • Autorität: Bloch hat große Teile der Plattform selbst gebaut und benennt offen Fehler im JDK, auch eigene.
  • Konkret und begründet: Jedes Item erklärt, warum, mit Beispiel und Gegenbeispiel.
  • Abwägend: Viele Items enden mit Ausnahmen. Der Ton ist sachlich, nicht moralisch.
  • API-Perspektive: Das Buch denkt konsequent aus Sicht der Nutzer einer Klasse. Das macht es auch für Anwendungsentwickler wertvoll.
  • Hohe Trefferquote: Fast alle Ratschläge haben sich bestätigt, viele sind später in die Sprache eingegangen.

Kritikpunkte

1. Bibliotheksperspektive. Bloch schreibt für Autoren öffentlicher APIs. In Anwendungscode, den nur das eigene Team nutzt, sind manche Ratschläge übertrieben, etwa ausführliche Dokumentation jeder öffentlichen Methode oder der Aufwand für Erweiterbarkeit.

2. Kein Designbuch. Effective Java sagt, wie man einzelne Klassen richtig baut, aber kaum, welche Klassen man braucht oder wie man ein System schneidet. Dafür braucht es Ousterhout oder Evans.

3. Veraltung. Die dritte Auflage kennt weder Records noch sealed Types noch Virtual Threads. Etwa ein Fünftel der Items ist heute durch Sprachmittel vereinfacht oder überholt.

4. Checked Exceptions. Blochs Verteidigung behebbarer Checked Exceptions hat sich in der Praxis nicht durchgesetzt, besonders seit Lambdas.

5. Wenig Tests. Testbarkeit ist kaum Thema, obwohl viele Ratschläge, etwa Dependency Injection oder Unveränderlichkeit, sie stark fördern.

Verhältnis zu den anderen Büchern

Bloch steht in vielen Punkten zwischen den anderen Autoren und liefert für Java die konkrete Umsetzung ihrer Prinzipien.

ThemaBlochAndere
Komposition statt VererbungItem 18, mit InstrumentedHashSet als BeweisGoF-Grundprinzip, Fowlers Replace Superclass with Delegate, Ousterhout
Kommentareausführliche Javadoc als Vertragnah bei Ousterhout, deutlich gegen Martin
Checked Exceptionsfür behebbare FehlerFeathers in Clean Code dagegen
UnveränderlichkeitItem 17, Klassen grundsätzlich unveränderlichEvans‘ Value Objects, Fowlers Mutable Data als Smell
Singletonals Enum, aber DI bevorzugtGoF, Gamma würde es streichen
Buildervereinfachte Form, heute StandardGoF-Builder mit Director
Interface-Typen für VariablenItem 64, dafürOusterhout dagegen
Streamsmit Bedacht, Mischung aus Schleifen und StreamsFowler deutlich enthusiastischer
Optimierungerst richtig, dann messenFowler und Ousterhout ähnlich

Diskussionsfragen

  1. Records: Nutzt du Records schon konsequent für Datenträger? Wo sind sie dir zu starr, etwa bei JPA-Entities?
  2. Checked Exceptions: Wer hat recht, Bloch oder Feathers? Wie geht ihr mit Checked Exceptions in Lambdas um?
  3. Optional: Hältst du dich an Blochs Grenzen, also nur als Rückgabewert? Oder nutzt ihr es auch als Parameter oder Feld?
  4. Streams: Wo ziehst du die Grenze zwischen Stream und Schleife? Hast du zu lange Streams in Codereviews erlebt?
  5. Bibliothek oder Anwendung: Welche Items gelten für dich auch in internem Anwendungscode, welche nur für öffentliche APIs?
  6. Item 64 gegen Ousterhout: List<X> x = new ArrayList<>() oder ArrayList<X> x = … oder einfach var?
  7. Virtual Threads: Hat Java 21 in euren Projekten schon etwas an der Nebenläufigkeit geändert?
  8. Vierte Auflage: Welche neuen Items würdest du für eine Neuauflage vorschlagen, etwa zu Records, sealed Types, Pattern Matching oder Virtual Threads?