🏛️ Design Principles & Patterns · Advanced

Observer in Java

Publish-subscribe and listener leaks.

🧩 The mysteryA dialog opens and closes a hundred times, and your app's memory climbs and never comes down. The leak hides in a pattern you use every day.

Subscribe and get notified

Observer lets a *subject* notify many *listeners* when something happens. Listeners subscribe; on each event the subject calls every one of them. Think newsletter: you sign up, and every issue arrives.

interface Listener { void onPrice(int p); }
List<Listener> ls = new ArrayList<>();
void subscribe(Listener l) { ls.add(l); }
void publish(int p) {
    for (var l : ls) l.onPrice(p);
}

Loose coupling

The subject only knows the listener interface, never the concrete classes behind it. A logger, a chart and an email alert can all subscribe, and new kinds of observers can be added without touching the subject. That's the whole point.

🔮 Predict it

Unsubscribing

What does this print?

interface L { void on(int p); }
List<L> ls = new ArrayList<>();
void main() {
    L log = p -> IO.println("log " + p);
    ls.add(log);
    ls.add(p -> IO.println("ui " + p));
    ls.remove(log);
    ls.forEach(l -> l.on(5));
}
  1. log 5 ui 5
  2. ui 5
  3. log 5
Show the answer

log was removed before the event, so only the UI listener hears it. Removing works because we kept the same lambda reference in a variable. Writing a fresh lambda in remove(...) would be a different object and remove nothing.

The lapsed-listener leak

Garbage collection frees objects nobody can reach. A long-lived subject (a global event bus) holds strong references to its listeners, and each listener's lambda captures the dialog that created it. Forget to unsubscribe, and every closed dialog stays reachable forever.

Dialogs and an event bus

✗ Leaks
void open() {
    bus.subscribe(e -> refresh());
    // never unsubscribed
}

Each open adds a listener that pins this dialog in memory.

✓ Cleans up
void open() {
    sub = e -> refresh();
    bus.subscribe(sub);
}
void close() { bus.unsubscribe(sub); }

Unregistering on close fixes the root cause.

⚠️ The trap

Don't wipe the list

A subject should remove a listener only when it unsubscribes. Here click() clears every subscription after the first notification, so all listeners go deaf after one click.

void click() {
    for (Runnable r : ls) r.run();
    ls.clear(); // bug
}
💼 In the real world

In real apps

UI events, domain events, message buses and reactive streams are all Observer. Lapsed listeners are a top cause of slow memory leaks in long-running apps; a heap dump full of thousands of identical listener objects is the telltale sign. Always offer and call unsubscribe.

Key takeaways

  1. The subject keeps a list of listeners and calls them on events
  2. Loose coupling: the publisher doesn't know concrete subscribers
  3. Always provide (and use) a way to unsubscribe
  4. Long-lived subject + forgotten listener = memory leak

💡 A newsletter: you subscribe, every issue arrives, and you must unsubscribe or it keeps coming forever.

🤯 Did you know?

Java shipped its own java.util.Observer and Observable in version 1.0. Both were deprecated in Java 9; modern code uses listener interfaces or reactive libraries instead.

Practice questions

What does this print?

interface L { void on(String e); }
List<L> ls = new ArrayList<>();
void main() {
    L a = e -> System.out.println("A:" + e);
    ls.add(a);
    ls.add(e -> System.out.println("B:" + e));
    ls.forEach(l -> l.on("x"));
    ls.remove(a);
    ls.forEach(l -> l.on("y"));
}
  1. A:x B:x A:y B:y
  2. A:x B:x
  3. A:x B:x B:y
  4. B:x B:y
Check your answer

A:x B:x B:y. Both listeners get event x. After A is unsubscribed, only B receives y.

A dialog registers a listener on a global EventBus every time it opens but never unregisters. After hours, memory keeps growing. Why?

  1. Lambdas are never garbage-collected in Java
  2. The bus holds strong references to every listener, and each listener captures its dialog, so none can be collected
  3. EventBus copies each event into every dialog
  4. The GC skips objects created on the UI thread
Check your answer

The bus holds strong references to every listener, and each listener captures its dialog, so none can be collected. Reachability is what matters: the long-lived bus references the listener, which references the closed dialog. That's the lapsed-listener leak.

Next: how does new BufferedReader(new FileReader(f)) stack features like toppings on a coffee?