Как реализован Observer в Java
Подписка на YouTube-канал или новостную рассылку. Автор видео (Subject) не знает каждого зрителя лично. Зрители подписываются (subscribe), и когда выходит новый ролик, уведомлен...
🟢 Junior Level
Observer (Наблюдатель / Издатель-Подписчик) — это поведенческий паттерн проектирования, который создает механизм подписки, позволяющий одним объектам (наблюдателям / подписчикам) следить за событиями, происходящими в другом объекте (субъекте / издателе), и автоматически реагировать на них.
Бытовая аналогия
Подписка на YouTube-канал или новостную рассылку. Автор видео (Subject) не знает каждого зрителя лично. Зрители подписываются (subscribe), и когда выходит новый ролик, уведомление автоматически отправляется всем активным подписчикам (Observers).
Классическая реализация на чистой Java
// 1. Интерфейс наблюдателя
public interface EventListener {
void onEvent(String message);
}
// 2. Издатель (Subject)
public class EventPublisher {
private final List<EventListener> listeners = new ArrayList<>();
public void subscribe(EventListener listener) {
listeners.add(listener);
}
public void unsubscribe(EventListener listener) {
listeners.remove(listener);
}
public void notifyListeners(String data) {
for (EventListener listener : listeners) {
listener.onEvent(data);
}
}
}
// 3. Использование (включая лямбда-выражения Java 8+)
EventPublisher publisher = new EventPublisher();
publisher.subscribe(msg -> System.out.println("Email-сервис получил: " + msg));
publisher.subscribe(msg -> System.out.println("SMS-шлюз получил: " + msg));
publisher.notifyListeners("Заказ #1042 успешно оплачен");
🟡 Middle Level
Почему стандартные java.util.Observer и Observable объявлены Deprecated в Java 9?
Исторически в пакете java.util присутствовали класс Observable и интерфейс Observer. Начиная с Java 9, они официально помечены @Deprecated по следующим причинам:
Observable— это класс, а не интерфейс: Поскольку в Java запрещено множественное наследование классов, класс, уже имеющий родителя, физически не может статьObservable.- Отсутствие типобезопасности (Generics): Метод
update(Observable o, Object arg)принимает сыройObject, что требует постоянного небезопасного приведения типов (downcasting). - Неудобная модель флагов: Перед вызовом
notifyObservers()программист обязан вручную вызвать методsetChanged(). Забытый вызовsetChanged()приводил к молчаливому отсутствию уведомлений. - Не потокобезопасен и не сериализуем: Внутренняя синхронизация
Vectorустарела и не гарантирует порядок в современных конкурентных сценариях.
Альтернатива из Java SE: PropertyChangeSupport
В стандартную библиотеку Java Desktop/Beans входит встроенный и поддерживаемый механизм отслеживания изменений свойств:
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
public class OrderAccount {
private final PropertyChangeSupport support = new PropertyChangeSupport(this);
private String status = "NEW";
public void addListener(PropertyChangeListener listener) {
support.addPropertyChangeListener(listener);
}
public void setStatus(String newStatus) {
String oldStatus = this.status;
this.status = newStatus;
// Автоматически передает имя свойства, старое значение и новое значение
support.firePropertyChange("status", oldStatus, newStatus);
}
}
Типичные производственные ловушки паттерна Observer
1. Ошибка ConcurrentModificationException при отписке
Если слушатель внутри метода onEvent() решает отписаться (publisher.unsubscribe(this)), классический цикл for (EventListener l : listeners) выбросит ConcurrentModificationException.
- Решение: Использование потокобезопасной коллекции
CopyOnWriteArrayListдля хранения подписчиков, либо создание защитной копии списка перед итерацией.
2. Падение одного слушателя блокирует остальных
Если один из подписчиков выбрасывает необработанное исключение (RuntimeException), цикл прерывается, и последующие подписчики не получат уведомление.
- Решение: Изоляция вызовов в блоке
try-catchвнутри издателя.
3. Утечка памяти (Проблема «Lapsed Listener»)
Издатель удерживает сильную ссылку (Strong Reference) на зарегистрированного слушателя. Если жизненный цикл слушателя короче жизненного цикла издателя, сборщик мусора GC не сможет удалить слушатель, пока тот явно не отпишется.
- Решение: Использование слабых ссылок
WeakReference<EventListener>или обязательная явная отписка при завершении жизненного цикла объекта.
🔴 Senior Level
Enterprise Observer в Spring: @EventListener и @TransactionalEventListener
В корпоративных приложениях прямое написание паттерна Observer заменяется встроенным брокером событий Spring Application Context:
// Событие (иммутабельный record)
public record OrderPaidEvent(Long orderId, BigDecimal amount) {}
// Издатель
@Service
public class PaymentProcessingService {
private final ApplicationEventPublisher eventPublisher;
public PaymentProcessingService(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
@Transactional
public void processPayment(Long orderId, BigDecimal amount) {
// Бизнес-логика транзакции
eventPublisher.publishEvent(new OrderPaidEvent(orderId, amount));
}
}
// Слушатели событий
@Component
public class OrderNotificationListener {
// Асинхронная обработка (не блокирует основной HTTP-поток)
@Async
@EventListener
public void sendReceipt(OrderPaidEvent event) {
// Отправка чека по почте
}
// Вызов СТРОГО ПОСЛЕ успешного коммита транзакции БД
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void updateWarehouse(OrderPaidEvent event) {
// Отправка запроса на склад только если деньги реально списались и закоммичены в БД!
}
}
Фазы @TransactionalEventListener:
BEFORE_COMMIT— до фиксации транзакции.AFTER_COMMIT(по умолчанию) — только в случае успешного коммита. Предотвращает ложную отправку писем/сообщений при откате (rollback) транзакции!AFTER_ROLLBACK— в случае отката (для компенсационных действий).AFTER_COMPLETION— в любом исходе транзакции.
Reactive Streams и Java 9 Flow API (Реактивный Observer)
Классический Observer имеет фатальный недостаток: Push-модель без контроля скорости. Если издатель генерирует 1 000 000 сообщений в секунду, а слушатель успевает обработать лишь 100, у слушателя переполняется память (OOM: Java heap space).
Спецификация Reactive Streams (реализованная в java.util.concurrent.Flow в Java 9+, а также в Project Reactor / RxJava) расширяет Observer концепцией Backpressure (обратного давления):
Publisher (Издатель) <─── Subscriber.onSubscribe(Subscription)
│
│ ◄─── Subscription.request(10) [Запрос: дай мне ровно 10 элементов!]
├───── Subscriber.onNext(data_1)
├───── Subscriber.onNext(data_2)
│ ... (ровно 10 элементов)
│
│ ◄─── Subscription.request(5) [Слушатель освободился и просит еще 5]
Опасность Deadlock: Вызов «чужих методов» (Alien Methods) под блокировкой
Джошуа Блох в Effective Java (Item 79) предостерегает от вызова методов слушателей внутри синхронизированных блоков:
// ❌ СМЕРТЕЛЬНО ОПАСНО: Вызов Alien Method внутри synchronized!
public synchronized void notifyObservers(Event event) {
for (Listener l : listeners) {
l.onEvent(event); // Если реализация слушателя внутри попытается захватить другой лок,
// возникнет взаимная блокировка (Deadlock)!
}
}
// ✅ ПРАВИЛЬНО: Открытые вызовы (Open Calls)
public void notifyObservers(Event event) {
List<Listener> snapshot;
synchronized (this) {
snapshot = new ArrayList<>(listeners); // Снимок под кратким локом
}
for (Listener l : snapshot) {
l.onEvent(event); // Вызов без удержания монитора!
}
}
🎯 Шпаргалка для интервью
30-секундный ответ (Elevator Pitch)
«Паттерн Observer (Наблюдатель) реализует слабую связность (loose coupling) между издателем событий и множеством подписчиков. В Java стандартный класс
java.util.Observableпризнан устаревшим (@Deprecatedв Java 9), поэтому в чистой Java используют интерфейсы со списком слушателей (на базеCopyOnWriteArrayList), а в Enterprise-среде — Spring Events (@EventListener,@Async,@TransactionalEventListener). Для высоконагруженных асинхронных систем развитием паттерна Observer стала спецификация Reactive Streams / Java Flow API, которая добавляет механизм Backpressure для предотвращения переполнения памяти медленных подписчиков».
3–4 каверзных вопроса с ответами
- Почему класс
java.util.Observableбыл помечен как Deprecated в Java 9?- Ответ:
Observableреализован как класс, а не интерфейс, что делает невозможным его использование классами, уже имеющими родителя (ограничение одиночного наследования в Java). Кроме того, он не поддерживает Generics, требует ручного вызоваsetChanged()и не гарантирует потокобезопасность в современных многопоточных средах.
- Ответ:
- Что такое проблема «Lapsed Listener» и как она приводит к утечкам памяти?
- Ответ: Издатель хранит сильные ссылки (Strong References) на зарегистрированных слушателей. Если клиентский код закончил работу со слушателем, но забыл явно вызвать
unsubscribe(), сборщик мусора GC не сможет утилизировать объект слушателя, так как он достижим из корня GC через коллекцию подписчиков издателя.
- Ответ: Издатель хранит сильные ссылки (Strong References) на зарегистрированных слушателей. Если клиентский код закончил работу со слушателем, но забыл явно вызвать
- В чем польза аннотации
@TransactionalEventListener(phase = AFTER_COMMIT)в Spring?- Ответ: Обычный
@EventListenerсрабатывает синхронно прямо во время выполнения транзакции. Если после генерации события транзакция базы данных откатится (Rollback), внешний мир уже получит уведомление (например, пользователю уйдет письмо об успешной оплате, которая по факту сорвалась). ФазаAFTER_COMMITгарантирует запуск слушателя строго после успешной фиксации данных в БД.
- Ответ: Обычный
- Что такое Backpressure в реактивной реализации Observer (Flow API)?
- Ответ: Это механизм обратной связи, при котором подписчик сам регулирует объем входящих данных с помощью вызова
subscription.request(n). Издатель отправляет порцию данных только тогда, когда подписчик явно сообщает о готовности обработать очередное количество элементов, что исключает OutOfMemoryError при разнице скоростей генерации и потребления.
- Ответ: Это механизм обратной связи, при котором подписчик сам регулирует объем входящих данных с помощью вызова
Типичные ошибки и Red Flags
- 🚩 Использование
java.util.ObservableиObserverв новом коде. - 🚩 Итерация по
ArrayListбез защиты от конкурентной модификации (удаление слушателя внутри вызоваonEventприведет кConcurrentModificationException). - 🚩 Вызов методов подписчиков внутри блока
synchronized— гарантированный риск получить Deadlock при сложной иерархии блокировок в слушателях. - 🚩 Отправка внешних уведомлений (SMS/Email) из синхронного слушателя без аннотации
@Asyncили очереди задач, блокирующая транзакции и HTTP-потоки приложения.
Связанные темы
- Что такое Iterator pattern — перебор элементов коллекций
- Какие категории паттернов существуют — классификация поведенческих паттернов
- Когда использовать Strategy — сравнение поведенческих шаблонов
- CompletableFuture и асинхронность — реактивное и асинхронное выполнение
- Что такое транзакции — контекст для
@TransactionalEventListener