Observer in Java
Publish-subscribe and listener leaks.
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.
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));
}log 5 ui 5ui 5log 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
void open() {
bus.subscribe(e -> refresh());
// never unsubscribed
}Each open adds a listener that pins this dialog in memory.
void open() {
sub = e -> refresh();
bus.subscribe(sub);
}
void close() { bus.unsubscribe(sub); }Unregistering on close fixes the root cause.
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 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
- The subject keeps a list of listeners and calls them on events
- Loose coupling: the publisher doesn't know concrete subscribers
- Always provide (and use) a way to unsubscribe
- Long-lived subject + forgotten listener = memory leak
💡 A newsletter: you subscribe, every issue arrives, and you must unsubscribe or it keeps coming forever.
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"));
}- A:x B:x A:y B:y
- A:x B:x
- A:x B:x B:y
- 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?
- Lambdas are never garbage-collected in Java
- The bus holds strong references to every listener, and each listener captures its dialog, so none can be collected
- EventBus copies each event into every dialog
- 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.