Software-Muster jenseits von GoF

Erzeugung und Objektverwaltung

Software-Muster jenseits von GoF · Gruppe 1 von 7

Lesestand

Die fünf Erzeugungsmuster der GoF beantworten die Frage, wie ein Objekt entsteht. Diese Gruppe geht einen Schritt weiter: Wer erzeugt die Objekte, wann entstehen sie, und wie viele gibt es davon? Die beiden ersten Muster regeln, wie eine Klasse an ihre Abhängigkeiten kommt. Die anderen vier steuern, ob Objekte neu entstehen, wiederverwendet oder erst bei Bedarf erzeugt werden.

  • Anwendung / Objekt
  • Muster-Baustein
  • Speicher / Ressource

1. Dependency Injection · Abhängigkeiten übergeben

Composition Rootsteckt einmal beim Start alles zusammenBestellService(Zahlungsdienst, Benachrichtigung)KreditkarteE-MailübergebenAttrappe ZahlungPostfach (Test)im Betriebim Testdieselbe Klasse, unverändert
Der BestellService bekommt seine Dienste über den Konstruktor. Im Betrieb sind es die echten, im Test Attrappen.

Die Idee

Eine Klasse erzeugt ihre Abhängigkeiten nicht selbst, sondern bekommt sie von außen übergeben, am besten über den Konstruktor. Statt new Kreditkarte() im Inneren steht im Konstruktor nur: „Ich brauche einen Zahlungsdienst.“ Welcher das ist, entscheidet eine einzige Stelle beim Programmstart, die Composition Root.

Das Problem

Ein BestellService, der seinen Zahlungsdienst mit new selbst anlegt, ist fest an diese eine Implementierung gebunden. Ein Test würde jedes Mal echtes Geld abbuchen und echte E-Mails verschicken. Und ein Wechsel des Zahlungsanbieters bedeutet, in jeder Klasse zu suchen, die ihn erzeugt.

Im Code

Der BestellService kennt nur die Schnittstellen Zahlungsdienst und Benachrichtigung. In main wird er dreimal verwendet: mit echten Diensten, mit Attrappen für einen Test und mit zwei Lambdas, denn beide Schnittstellen haben nur eine Methode. Am BestellService ändert sich dabei keine Zeile.

Java-21-Code: DependencyInjection.java

Direkt ausführbar mit java DependencyInjection.java

// Dependency Injection
// Eine Klasse erzeugt ihre Abhängigkeiten nicht selbst, sondern bekommt sie von außen übergeben.
// Start: java DependencyInjection.java

import java.util.ArrayList;
import java.util.List;

public class DependencyInjection {

    // --- Abhängigkeiten als Schnittstellen ---
    interface Zahlungsdienst { boolean belaste(String kunde, int cent); }
    interface Benachrichtigung { void sende(String an, String text); }

    // --- Die Klasse mit Abhängigkeiten: kein "new" für Dienste, alles kommt über den Konstruktor ---
    static final class BestellService {
        private final Zahlungsdienst zahlung;
        private final Benachrichtigung nachricht;

        BestellService(Zahlungsdienst zahlung, Benachrichtigung nachricht) {   // Constructor Injection
            this.zahlung = zahlung;
            this.nachricht = nachricht;
        }

        boolean bestelle(String kunde, int cent) {
            if (!zahlung.belaste(kunde, cent)) {
                nachricht.sende(kunde, "Zahlung abgelehnt");
                return false;
            }
            nachricht.sende(kunde, "Danke für Ihre Bestellung");
            return true;
        }
    }

    // --- Echte Implementierungen ---
    static final class Kreditkarte implements Zahlungsdienst {
        public boolean belaste(String kunde, int cent) {
            System.out.printf("  [Kreditkarte] %s mit %d,%02d € belastet%n", kunde, cent / 100, cent % 100);
            return true;
        }
    }
    static final class Email implements Benachrichtigung {
        public void sende(String an, String text) { System.out.println("  [E-Mail an " + an + "] " + text); }
    }

    // --- Attrappen für Tests: ohne Netzwerk, ohne echtes Geld ---
    record ZahlungLehntAb() implements Zahlungsdienst {
        public boolean belaste(String kunde, int cent) { return false; }
    }
    static final class Postfach implements Benachrichtigung {
        final List<String> eingang = new ArrayList<>();
        public void sende(String an, String text) { eingang.add(an + ": " + text); }
    }

    public static void main(String[] args) {
        // Composition Root: die EINE Stelle, an der alles zusammengesteckt wird
        System.out.println("Produktiv:");
        BestellService produktiv = new BestellService(new Kreditkarte(), new Email());
        produktiv.bestelle("Sven", 4_990);

        // Test: dieselbe Klasse, andere Abhängigkeiten, ohne eine Zeile im BestellService zu ändern
        System.out.println("\nTest mit Attrappen:");
        Postfach postfach = new Postfach();
        BestellService imTest = new BestellService(new ZahlungLehntAb(), postfach);
        boolean ergebnis = imTest.bestelle("Testkunde", 100);
        System.out.println("  Ergebnis: " + ergebnis + ", Postfach: " + postfach.eingang);

        // Abhängigkeiten als Lambda: beide Schnittstellen haben genau eine Methode
        System.out.println("\nMit Lambdas:");
        new BestellService((k, c) -> c < 10_000, (an, t) -> System.out.println("  [SMS an " + an + "] " + t))
                .bestelle("Anna", 25_000);
    }
}

Ausgabe

Produktiv:
  [Kreditkarte] Sven mit 49,90 € belastet
  [E-Mail an Sven] Danke für Ihre Bestellung

Test mit Attrappen:
  Ergebnis: false, Postfach: [Testkunde: Zahlung abgelehnt]

Mit Lambdas:
  [SMS an Anna] Zahlung abgelehnt

Worauf achten

Es gibt drei Formen: Konstruktor-Injektion (empfohlen, weil alle Abhängigkeiten sichtbar sind und die Felder final sein können), Setter-Injektion für optionale Abhängigkeiten und Feld-Injektion per Annotation, die bequem ist, aber Abhängigkeiten versteckt und Tests ohne Framework erschwert. Hat ein Konstruktor sehr viele Parameter, ist das kein Problem der Injektion, sondern ein Hinweis, dass die Klasse zu viel tut. Dependency Injection braucht kein Framework. Das Beispiel kommt ohne aus, Frameworks nehmen einem nur das Zusammenstecken ab.

In der Praxis

Spring (@Component, @Autowired, Konstruktor-Injektion ist dort Standard), Jakarta CDI (@Inject), Google Guice und Dagger, die Abhängigkeiten zur Compilezeit verdrahten. DI ist die Umsetzung des Dependency Inversion Principle, des „D“ in SOLID: Klassen hängen von Abstraktionen ab, nicht von konkreten Implementierungen.

2. Service Locator · Dienstverzeichnis

BestellServiceKonstruktor: ()Service LocatorZahlungsdienst → KreditkarteBenachrichtigung → E-MailLager → ?hole(...)KreditkarteE-MailAbhängigkeiten von außen nicht sichtbarfehlt ein Eintrag: Fehler erst zur Laufzeit
Der BestellService fragt die Registry nach seinen Diensten. Von außen sieht man nicht, was er braucht.

Die Idee

Ein Service Locator ist ein zentrales Verzeichnis, in dem beim Start alle Dienste registriert werden. Klassen, die einen Dienst brauchen, fragen dort nach: „Gib mir den Zahlungsdienst.“ Wie bei Dependency Injection kennen die Klassen nur Schnittstellen, aber die Richtung ist umgekehrt. Die Klasse holt sich ihre Abhängigkeiten, statt sie zu bekommen.

Das Problem, das er löst

Er entkoppelt Klassen von konkreten Implementierungen, ohne dass alle Abhängigkeiten durch Konstruktoren gereicht werden müssen. In älteren Systemen ohne DI-Framework, etwa bei JNDI-Lookups in frühen Java-EE-Anwendungen, war das der übliche Weg.

Im Code

Die Klasse Dienste ist eine Map von Typ auf Implementierung. Der BestellService hat einen leeren Konstruktor und holt sich seine Dienste im Methodenrumpf. Im zweiten Teil wird die Registry geleert. Der Code kompiliert weiterhin problemlos, scheitert aber zur Laufzeit.

Java-21-Code: ServiceLocator.java

Direkt ausführbar mit java ServiceLocator.java

// Service Locator
// Eine zentrale Registry, bei der sich Klassen ihre Abhängigkeiten selbst holen.
// Start: java ServiceLocator.java

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ServiceLocator {

    interface Zahlungsdienst { boolean belaste(String kunde, int cent); }
    interface Benachrichtigung { void sende(String an, String text); }

    // --- Der Service Locator: Typ -> Implementierung ---
    static final class Dienste {
        private static final Map<Class<?>, Object> registry = new ConcurrentHashMap<>();

        static <T> void registriere(Class<T> typ, T dienst) { registry.put(typ, dienst); }

        static <T> T hole(Class<T> typ) {
            Object dienst = registry.get(typ);
            if (dienst == null) throw new IllegalStateException("Kein Dienst registriert für " + typ.getSimpleName());
            return typ.cast(dienst);
        }

        static void leeren() { registry.clear(); }
    }

    // --- Die Klasse holt sich ihre Abhängigkeiten selbst ---
    static final class BestellService {
        boolean bestelle(String kunde, int cent) {
            // Die Abhängigkeiten stehen NICHT im Konstruktor, sondern versteckt im Methodenrumpf
            Zahlungsdienst zahlung = Dienste.hole(Zahlungsdienst.class);
            Benachrichtigung nachricht = Dienste.hole(Benachrichtigung.class);
            boolean ok = zahlung.belaste(kunde, cent);
            nachricht.sende(kunde, ok ? "Danke für Ihre Bestellung" : "Zahlung abgelehnt");
            return ok;
        }
    }

    public static void main(String[] args) {
        // Beim Start registrieren
        Dienste.registriere(Zahlungsdienst.class, (k, c) -> {
            System.out.println("  [Kreditkarte] " + k + " belastet");
            return true;
        });
        Dienste.registriere(Benachrichtigung.class, (an, t) -> System.out.println("  [E-Mail an " + an + "] " + t));

        System.out.println("Mit Registrierung:");
        new BestellService().bestelle("Sven", 4_990);

        // Das typische Problem: fehlt eine Registrierung, merkt man es erst zur Laufzeit
        System.out.println("\nNach dem Leeren der Registry:");
        Dienste.leeren();
        try {
            new BestellService().bestelle("Sven", 4_990);   // kompiliert problemlos
        } catch (IllegalStateException e) {
            System.out.println("  Laufzeitfehler: " + e.getMessage());
        }
    }
}

Ausgabe

Mit Registrierung:
  [Kreditkarte] Sven belastet
  [E-Mail an Sven] Danke für Ihre Bestellung

Nach dem Leeren der Registry:
  Laufzeitfehler: Kein Dienst registriert für Zahlungsdienst

Worauf achten

Der Service Locator gilt heute weithin als Anti-Pattern, und das Beispiel zeigt warum. Die Abhängigkeiten einer Klasse sind versteckt: Am Konstruktor sieht man nicht, was sie braucht. Fehlende Registrierungen fallen erst zur Laufzeit auf. Tests müssen die globale Registry vorbereiten und danach aufräumen, und parallele Tests können sich gegenseitig stören. Dependency Injection löst dieselbe Aufgabe ohne diese Nachteile. Sinnvoll bleibt der Locator dort, wo man keinen Einfluss auf die Erzeugung von Objekten hat, etwa in Plugins.

In der Praxis

JNDI-Lookups in Java EE, java.util.ServiceLoader für Plugins, die über META-INF/services gefunden werden, ApplicationContext.getBean() in Spring, wenn man es direkt aufruft. Martin Fowler hat den Unterschied zwischen Locator und Injection 2004 im Artikel „Inversion of Control Containers and the Dependency Injection pattern“ beschrieben.

3. Object Pool · Objektpool

Aufgabe 1Aufgabe 2Aufgabe 3Aufgabe 4Aufgabe 5Pool (3 Verbindungen)Verbindung 1 · verliehenVerbindung 2 · verliehenVerbindung 3 · freiwarten, bis eine frei wirdleihen →benutzen →zurückgebennichts wird neu erzeugt
Fünf Aufgaben, drei Verbindungen. Wer keine freie bekommt, wartet, bis eine zurückgegeben wird.

Die Idee

Manche Objekte sind teuer in der Erzeugung: Datenbankverbindungen, Netzwerkverbindungen, große Puffer. Ein Object Pool erzeugt eine feste Anzahl davon im Voraus und verleiht sie. Wer eines braucht, leiht es aus, benutzt es und gibt es zurück. Danach kann es der Nächste verwenden. Wie Einkaufswagen im Supermarkt.

Das Problem

Für jede Datenbankabfrage eine neue Verbindung aufzubauen kostet Zeit für Netzwerk, Anmeldung und Verschlüsselung, oft mehr als die Abfrage selbst. Gleichzeitig verträgt die Datenbank nur eine begrenzte Zahl gleichzeitiger Verbindungen. Ein Pool löst beides: Verbindungen werden wiederverwendet, und ihre Zahl ist nach oben begrenzt.

Im Code

Der Pool ist eine BlockingQueue mit drei Verbindungen. Die Ausleihe ist ein Record, der AutoCloseable implementiert. So gibt try-with-resources die Verbindung automatisch zurück, auch im Fehlerfall. 30 Abfragen laufen in virtuellen Threads, und wer keine Verbindung bekommt, wartet. Am Ende wurden nur drei Verbindungen erzeugt statt 30.

Java-21-Code: ObjectPool.java

Direkt ausführbar mit java ObjectPool.java

// Object Pool
// Teure Objekte werden einmal erzeugt, verliehen, zurückgegeben und wiederverwendet.
// Start: java ObjectPool.java

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class ObjectPool {

    // --- Teures Objekt ---
    static final class Verbindung {
        private static final AtomicInteger erzeugt = new AtomicInteger();
        final int nr;

        Verbindung() {
            nr = erzeugt.incrementAndGet();
            schlafe(200);                                  // Aufbau dauert (Netzwerk, Anmeldung ...)
        }
        String abfrage(String sql) { schlafe(50); return "Ergebnis von " + sql; }
    }

    // --- Der Pool: feste Anzahl Objekte in einer Warteschlange ---
    static final class VerbindungsPool {
        private final BlockingQueue<Verbindung> frei;

        VerbindungsPool(int groesse) {
            frei = new ArrayBlockingQueue<>(groesse);
            for (int i = 0; i < groesse; i++) frei.add(new Verbindung());
        }

        // Ausleihe als AutoCloseable: Rückgabe passiert automatisch am Ende von try-with-resources
        record Leihe(Verbindung verbindung, VerbindungsPool pool) implements AutoCloseable {
            public void close() { pool.frei.offer(verbindung); }
        }

        Leihe leihe() throws InterruptedException {
            Verbindung v = frei.poll(2, TimeUnit.SECONDS);   // wartet, wenn alle verliehen sind
            if (v == null) throw new IllegalStateException("Pool erschöpft");
            return new Leihe(v, this);
        }
    }

    public static void main(String[] args) throws Exception {
        long start = System.nanoTime();
        VerbindungsPool pool = new VerbindungsPool(3);
        System.out.printf("Pool mit 3 Verbindungen aufgebaut in ca. %d ms%n", rund(start));

        AtomicInteger erledigt = new AtomicInteger();
        start = System.nanoTime();
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 1; i <= 30; i++) {
                int nr = i;
                executor.submit(() -> {
                    try (var leihe = pool.leihe()) {
                        leihe.verbindung().abfrage("SELECT * FROM bestellung WHERE id = " + nr);
                        erledigt.incrementAndGet();
                    }
                    return null;
                });
            }
        }
        System.out.printf("30 Abfragen erledigt: %d, Dauer ca. %d ms%n", erledigt.get(), rund(start));
        System.out.println("Insgesamt erzeugte Verbindungen: " + Verbindung.erzeugt.get());
        System.out.println("Ohne Pool wären es 30 gewesen, mit je 200 ms Aufbauzeit.");
    }

    static long rund(long start) { return Math.round((System.nanoTime() - start) / 1e8) * 100; }

    static void schlafe(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }
}

Ausgabe

Pool mit 3 Verbindungen aufgebaut in ca. 600 ms
30 Abfragen erledigt: 30, Dauer ca. 500 ms
Insgesamt erzeugte Verbindungen: 3
Ohne Pool wären es 30 gewesen, mit je 200 ms Aufbauzeit.

Worauf achten

Die größte Gefahr ist das Vergessen der Rückgabe: Dann leert sich der Pool, bis alle warten. Deshalb die Rückgabe immer über try-with-resources oder finally. Zurückgegebene Objekte müssen in einen sauberen Zustand gebracht werden, etwa offene Transaktionen beendet. Defekte Objekte müssen erkannt und ersetzt werden. Für billige Objekte lohnt sich ein Pool nicht mehr: Moderne JVMs erzeugen kleine Objekte so schnell, dass ein Pool eher schadet. Auch Threads muss man mit virtuellen Threads in Java 21 nicht mehr poolen.

In der Praxis

Datenbank-Verbindungspools wie HikariCP (Standard in Spring Boot), Thread-Pools in ExecutorService, HTTP-Client-Verbindungspools, Apache Commons Pool als allgemeine Bibliothek.

4. Lazy Initialization · Verzögerte Erzeugung

ZeitShop erzeugtKatalog: noch nicht da1. Zugriffjetzt laden (teuer)2. Zugriffaus dem SpeicherStart ist schnelljeder weitere Zugriff ist schnell
Der Katalog wird nicht beim Start, sondern erst beim ersten Zugriff geladen, und danach nie wieder.

Die Idee

Ein Objekt oder Wert wird erst dann erzeugt, wenn er zum ersten Mal gebraucht wird, und danach wiederverwendet. Wird er nie gebraucht, entstehen auch keine Kosten.

Das Problem

Eine Anwendung, die beim Start alles lädt, was sie irgendwann brauchen könnte, startet langsam und belegt Speicher für Dinge, die oft nie verwendet werden. Ein Produktkatalog, der nur in der Suche gebraucht wird, muss nicht geladen werden, wenn ein Kunde nur seine Bestellungen ansieht.

Im Code

Drei Varianten: ein einfaches Feld, das beim ersten Zugriff gefüllt wird (nur für einen Thread geeignet), eine wiederverwendbare Klasse Lazy<T>, die mit volatile und Double-Checked Locking threadsicher ist und hier von 1.000 virtuellen Threads gleichzeitig aufgerufen wird, und das Holder-Idiom, bei dem die JVM selbst für das verzögerte und threadsichere Laden sorgt. In allen drei Fällen wird der Katalog genau einmal geladen.

Java-21-Code: LazyInitialization.java

Direkt ausführbar mit java LazyInitialization.java

// Lazy Initialization
// Ein Objekt oder Wert wird erst erzeugt, wenn er zum ersten Mal gebraucht wird.
// Start: java LazyInitialization.java

import java.util.List;
import java.util.function.Supplier;

public class LazyInitialization {

    // --- Teure Berechnung ---
    static List<String> ladeKatalog() {
        System.out.println("    (Katalog wird geladen ... teuer!)");
        return List.of("Buch", "Lampe", "Tasse");
    }

    // --- Variante 1: einfaches Lazy-Feld (nur für einen Thread) ---
    static final class Shop {
        private List<String> katalog;                       // noch null

        List<String> katalog() {
            if (katalog == null) katalog = ladeKatalog();   // erst beim ersten Zugriff
            return katalog;
        }
    }

    // --- Variante 2: wiederverwendbarer, threadsicherer Lazy-Wert ---
    static final class Lazy<T> implements Supplier<T> {
        private final Supplier<T> erzeuger;
        private volatile T wert;

        private Lazy(Supplier<T> erzeuger) { this.erzeuger = erzeuger; }
        static <T> Lazy<T> von(Supplier<T> erzeuger) { return new Lazy<>(erzeuger); }

        public T get() {
            T w = wert;
            if (w == null) {
                synchronized (this) {
                    w = wert;
                    if (w == null) wert = w = erzeuger.get();   // Double-Checked Locking mit volatile
                }
            }
            return w;
        }
        boolean geladen() { return wert != null; }
    }

    public static void main(String[] args) throws Exception {
        System.out.println("Variante 1: Lazy-Feld");
        Shop shop = new Shop();
        System.out.println("  Shop erzeugt, Katalog noch nicht geladen");
        System.out.println("  1. Zugriff: " + shop.katalog());
        System.out.println("  2. Zugriff: " + shop.katalog());

        System.out.println("\nVariante 2: Lazy<T>, von 1.000 virtuellen Threads gleichzeitig abgefragt");
        Lazy<List<String>> katalog = Lazy.von(LazyInitialization::ladeKatalog);
        System.out.println("  geladen? " + katalog.geladen());
        try (var ex = java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 1_000; i++) ex.submit(katalog::get);
        }
        System.out.println("  geladen? " + katalog.geladen() + " -> " + katalog.get());

        System.out.println("\nVariante 3: Holder-Idiom (siehe Singleton), die JVM lädt erst bei Bedarf");
        System.out.println("  vor dem Zugriff");
        System.out.println("  " + Holder.KATALOG);
    }

    static final class Holder {
        static final List<String> KATALOG = ladeKatalog();
    }
}

Ausgabe

Variante 1: Lazy-Feld
  Shop erzeugt, Katalog noch nicht geladen
    (Katalog wird geladen ... teuer!)
  1. Zugriff: [Buch, Lampe, Tasse]
  2. Zugriff: [Buch, Lampe, Tasse]

Variante 2: Lazy<T>, von 1.000 virtuellen Threads gleichzeitig abgefragt
  geladen? false
    (Katalog wird geladen ... teuer!)
  geladen? true -> [Buch, Lampe, Tasse]

Variante 3: Holder-Idiom (siehe Singleton), die JVM lädt erst bei Bedarf
  vor dem Zugriff
    (Katalog wird geladen ... teuer!)
  [Buch, Lampe, Tasse]

Worauf achten

Lazy Initialization verschiebt die Kosten nur: Der erste Zugriff wird langsamer. Bei einem Webdienst trifft das den ersten Kunden nach dem Start, was unerwünscht sein kann. Die einfache Variante ist in nebenläufigen Programmen fehlerhaft, weil zwei Threads gleichzeitig null sehen und doppelt laden können. Fehler beim Laden müssen behandelt werden: Wird die Erzeugung beim nächsten Zugriff erneut versucht oder der Fehler gespeichert? Ab Java 25 gibt es mit StableValue eine eingebaute Lösung, in Java 21 nimmt man das Holder-Idiom oder eine eigene Klasse wie im Beispiel.

In der Praxis

Lazy Loading von Beziehungen in JPA/Hibernate, @Lazy für Beans in Spring, Lazy-Initialisierung von Singletons, Bilder auf Webseiten, die erst beim Scrollen geladen werden (loading="lazy").

5. Multiton · Eine Instanz pro Schlüssel

von("EUR")von("eur")von("JPY")Registry (Map)EURJPYUSDEuro, 2 StellenYen, 0 StellenDollar, 2 StellenEUR und eur: dieselbe Instanz
Für jeden Währungscode gibt es genau ein Objekt. Wer zweimal nach EUR fragt, bekommt zweimal dasselbe.

Die Idee

Das Multiton ist die Verallgemeinerung des Singletons: Statt genau einer Instanz gibt es genau eine Instanz pro Schlüssel. Eine zentrale Registry legt die Instanz beim ersten Zugriff an und gibt danach immer dieselbe zurück.

Das Problem

Für jede Währung, jedes Lager oder jede Datenbankverbindung zu einem bestimmten Mandanten soll es genau ein Objekt geben, damit Einstellungen nicht doppelt geladen werden und Vergleiche mit == zuverlässig funktionieren. Welche Schlüssel es gibt, steht aber nicht immer im Voraus fest.

Im Code

Zwei Varianten. Ist die Menge der Schlüssel offen, etwa Währungscodes, verwaltet eine ConcurrentHashMap die Instanzen. computeIfAbsent erzeugt jede Instanz threadsicher genau einmal. Der Schlüssel wird normalisiert, damit „EUR“ und „eur“ dasselbe Objekt liefern. Ist die Menge fest und schon beim Programmieren bekannt, ist ein enum die einfachste und sicherste Form eines Multitons.

Java-21-Code: Multiton.java

Direkt ausführbar mit java Multiton.java

// Multiton
// Genau eine Instanz pro Schlüssel, zentral verwaltet.
// Start: java Multiton.java

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class Multiton {

    // --- Variante 1: offene Menge von Schlüsseln, Instanzen bei Bedarf ---
    static final class Waehrung {
        private static final Map<String, Waehrung> instanzen = new ConcurrentHashMap<>();

        final String code;
        final int nachkommastellen;

        private Waehrung(String code, int nachkommastellen) {      // privat: kein "new" von außen
            this.code = code;
            this.nachkommastellen = nachkommastellen;
            System.out.println("    (neue Instanz für " + code + ")");
        }

        static Waehrung von(String code) {
            String key = code.toUpperCase();
            return instanzen.computeIfAbsent(key, c -> new Waehrung(c, c.equals("JPY") ? 0 : 2));
        }

        static int anzahl() { return instanzen.size(); }
        public String toString() { return code + "(" + nachkommastellen + " Nachkommastellen)"; }
    }

    // --- Variante 2: feste Menge von Schlüsseln -> einfach ein enum ---
    enum Lager {
        HAMBURG("Hafenstraße 7"), MUENCHEN("Isarweg 12"), WIEN("Donaugasse 3");

        final String adresse;
        Lager(String adresse) { this.adresse = adresse; }
    }

    public static void main(String[] args) {
        System.out.println("Variante 1: Map mit computeIfAbsent");
        Waehrung a = Waehrung.von("EUR");
        Waehrung b = Waehrung.von("eur");
        Waehrung c = Waehrung.von("JPY");
        System.out.println("  " + a + ", " + c);
        System.out.println("  EUR == eur: " + (a == b));
        System.out.println("  Instanzen insgesamt: " + Waehrung.anzahl());

        System.out.println("\nVariante 2: enum");
        Lager l = Lager.valueOf("HAMBURG");
        System.out.println("  " + l + " -> " + l.adresse + ", gleiche Instanz: " + (l == Lager.HAMBURG));
        System.out.println("  alle: " + java.util.Arrays.toString(Lager.values()));
    }
}

Ausgabe

Variante 1: Map mit computeIfAbsent
    (neue Instanz für EUR)
    (neue Instanz für JPY)
  EUR(2 Nachkommastellen), JPY(0 Nachkommastellen)
  EUR == eur: true
  Instanzen insgesamt: 2

Variante 2: enum
  HAMBURG -> Hafenstraße 7, gleiche Instanz: true
  alle: [HAMBURG, MUENCHEN, WIEN]

Worauf achten

Das Multiton erbt die Nachteile des Singletons: Es ist globaler Zustand und erschwert Tests. Dazu kommt ein eigenes Problem: Die Registry wächst mit jedem neuen Schlüssel und gibt Instanzen nie wieder frei. Bei vielen oder unkontrollierten Schlüsseln, etwa aus Benutzereingaben, wird daraus ein Speicherleck. Die Instanzen sollten unveränderlich sein, weil sie geteilt werden.

In der Praxis

Im JDK: java.util.Currency.getInstance("EUR"), Charset.forName("UTF-8") und ZoneId.of("Europe/Berlin") liefern für denselben Schlüssel dieselbe oder eine gleichwertige Instanz. Logger-Frameworks wie SLF4J halten einen Logger pro Name. Das Multiton ist eng verwandt mit dem Flyweight der GoF.

6. Static Factory Method · Statische Fabrikmethode

Geldbetragprivate Geldbetrag(...)ausCent(1299)ausEuro(12)ausText("7,5 €")nichts()Aufrufer AAufrufer BAufrufer Cneue Instanzgeteilte Instanz NULL"new" ist nicht möglich
Der Konstruktor ist privat. Erzeugt wird über Methoden, deren Namen sagen, was gemeint ist, und die auch vorhandene Instanzen zurückgeben dürfen.

Die Idee

Statt eines öffentlichen Konstruktors bietet eine Klasse statische Methoden mit sprechenden Namen an, die Instanzen liefern: Geldbetrag.ausEuro(12) statt new Geldbetrag(1200). Joshua Bloch stellt dieses Vorgehen in Effective Java gleich an den Anfang („Item 1“). Wichtig: Es ist nicht das GoF-Muster Factory Method, das auf Vererbung beruht.

Was Konstruktoren nicht können

  • Namen tragen: new Geldbetrag(12) ist mehrdeutig, Cent oder Euro? ausCent(12) und ausEuro(12) sind es nicht.
  • Vorhandene Objekte zurückgeben: ausCent(0) liefert immer dieselbe Instanz NULL, ganz ohne neues Objekt.
  • Einen Untertyp zurückgeben: Rabatt.prozent(10) liefert einen Prozent-Record, Rabatt.prozent(0) die Konstante KEIN. Der Aufrufer kennt nur Rabatt.
  • Vorher Arbeit leisten: ausText("7,5 €") parst und rundet, bevor das Objekt entsteht.

Im Code

Der Record Geldbetrag zeigt die benannten Methoden und die geteilte Instanz. Das sealed Interface Rabatt zeigt Fabrikmethoden in einem Interface, die je nach Parameter verschiedene Implementierungen liefern.

Java-21-Code: StaticFactoryMethod.java

Direkt ausführbar mit java StaticFactoryMethod.java

// Static Factory Method
// Benannte statische Methoden statt öffentlicher Konstruktoren.
// Start: java StaticFactoryMethod.java

import java.math.BigDecimal;
import java.math.RoundingMode;

public class StaticFactoryMethod {

    // --- Geldbetrag: privater Konstruktor, mehrere sprechende Erzeugungsmethoden ---
    record Geldbetrag(long cent) {
        static final Geldbetrag NULL = new Geldbetrag(0);                 // vorgefertigte, geteilte Instanz

        Geldbetrag {
            if (cent < 0) throw new IllegalArgumentException("negativ: " + cent);
        }

        // 1. Name erklärt die Bedeutung des Parameters
        static Geldbetrag ausCent(long cent)  { return cent == 0 ? NULL : new Geldbetrag(cent); }
        static Geldbetrag ausEuro(long euro)  { return ausCent(euro * 100); }

        // 2. Parsen mit eigener Logik
        static Geldbetrag ausText(String text) {
            BigDecimal euro = new BigDecimal(text.replace("€", "").replace(",", ".").strip());
            return ausCent(euro.setScale(2, RoundingMode.HALF_UP).movePointRight(2).longValueExact());
        }

        // 3. Ergebnis kann eine geteilte Instanz sein, kein neues Objekt
        static Geldbetrag nichts() { return NULL; }

        public String toString() { return "%d,%02d €".formatted(cent / 100, cent % 100); }
    }

    // --- Rückgabetyp darf ein Untertyp sein, der Aufrufer kennt nur die Schnittstelle ---
    sealed interface Rabatt permits Prozent, Fest, KeinRabatt {
        long wende(long cent);

        static Rabatt prozent(int p)   { return p == 0 ? KeinRabatt.KEIN : new Prozent(p); }
        static Rabatt fest(long cent)  { return cent == 0 ? KeinRabatt.KEIN : new Fest(cent); }
    }
    record Prozent(int p) implements Rabatt { public long wende(long c) { return c - c * p / 100; } }
    record Fest(long abzug) implements Rabatt { public long wende(long c) { return Math.max(0, c - abzug); } }
    enum KeinRabatt implements Rabatt { KEIN; public long wende(long c) { return c; } }

    public static void main(String[] args) {
        System.out.println("ausEuro(12):        " + Geldbetrag.ausEuro(12));
        System.out.println("ausCent(1299):      " + Geldbetrag.ausCent(1299));
        System.out.println("ausText(\"7,5 €\"):   " + Geldbetrag.ausText("7,5 €"));
        System.out.println("ausCent(0) == NULL: " + (Geldbetrag.ausCent(0) == Geldbetrag.nichts()));

        Geldbetrag preis = Geldbetrag.ausEuro(80);
        for (Rabatt r : new Rabatt[] { Rabatt.prozent(10), Rabatt.fest(1_500), Rabatt.prozent(0) }) {
            System.out.printf("%-26s -> %s%n", r, Geldbetrag.ausCent(r.wende(preis.cent())));
        }

        System.out.println("\nAus dem JDK: List.of(), Optional.empty(), Integer.valueOf(), Path.of(), LocalDate.of()");
    }
}

Ausgabe

ausEuro(12):        12,00 €
ausCent(1299):      12,99 €
ausText("7,5 €"):   7,50 €
ausCent(0) == NULL: true
Prozent[p=10]              -> 72,00 €
Fest[abzug=1500]           -> 65,00 €
KEIN                       -> 80,00 €

Aus dem JDK: List.of(), Optional.empty(), Integer.valueOf(), Path.of(), LocalDate.of()

Worauf achten

Statische Fabrikmethoden sind in der Dokumentation schwerer zu finden als Konstruktoren. Deshalb helfen übliche Namen: of, from, valueOf, getInstance, newInstance, parse. Ist der Konstruktor privat, kann man von der Klasse nicht erben. Das ist bei Records und Werttypen aber meist ohnehin gewollt.

In der Praxis

Das JDK ist voll davon: List.of(), Optional.empty(), Integer.valueOf(), Path.of(), LocalDate.of(), EnumSet.noneOf(), Duration.ofMinutes().

Im Überblick

MusterFrageAntwortHeute
Dependency InjectionWie kommt eine Klasse an ihre Dienste?Sie bekommt sie übergebenStandard
Service LocatorWie kommt eine Klasse an ihre Dienste?Sie holt sie aus einer Registrymeist Anti-Pattern
Object PoolWie viele teure Objekte gibt es?eine feste Anzahl, wiederverwendetfür Verbindungen üblich
Lazy InitializationWann entsteht ein Objekt?beim ersten Zugriffhäufig
MultitonWie viele Instanzen gibt es?genau eine pro Schlüsseloft als enum
Static Factory MethodWie ruft man die Erzeugung auf?über benannte statische MethodenStandard bei Werttypen

Zum Vergleich: die GoF-Erzeugungsmuster Singleton, Factory Method, Abstract Factory, Builder und Prototype.