🧩 Раздел 2 · Вопрос #11

Как реализован 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 по следующим причинам:

  1. Observable — это класс, а не интерфейс: Поскольку в Java запрещено множественное наследование классов, класс, уже имеющий родителя, физически не может стать Observable.
  2. Отсутствие типобезопасности (Generics): Метод update(Observable o, Object arg) принимает сырой Object, что требует постоянного небезопасного приведения типов (downcasting).
  3. Неудобная модель флагов: Перед вызовом notifyObservers() программист обязан вручную вызвать метод setChanged(). Забытый вызов setChanged() приводил к молчаливому отсутствию уведомлений.
  4. Не потокобезопасен и не сериализуем: Внутренняя синхронизация 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 каверзных вопроса с ответами

  1. Почему класс java.util.Observable был помечен как Deprecated в Java 9?
    • Ответ: Observable реализован как класс, а не интерфейс, что делает невозможным его использование классами, уже имеющими родителя (ограничение одиночного наследования в Java). Кроме того, он не поддерживает Generics, требует ручного вызова setChanged() и не гарантирует потокобезопасность в современных многопоточных средах.
  2. Что такое проблема «Lapsed Listener» и как она приводит к утечкам памяти?
    • Ответ: Издатель хранит сильные ссылки (Strong References) на зарегистрированных слушателей. Если клиентский код закончил работу со слушателем, но забыл явно вызвать unsubscribe(), сборщик мусора GC не сможет утилизировать объект слушателя, так как он достижим из корня GC через коллекцию подписчиков издателя.
  3. В чем польза аннотации @TransactionalEventListener(phase = AFTER_COMMIT) в Spring?
    • Ответ: Обычный @EventListener срабатывает синхронно прямо во время выполнения транзакции. Если после генерации события транзакция базы данных откатится (Rollback), внешний мир уже получит уведомление (например, пользователю уйдет письмо об успешной оплате, которая по факту сорвалась). Фаза AFTER_COMMIT гарантирует запуск слушателя строго после успешной фиксации данных в БД.
  4. Что такое Backpressure в реактивной реализации Observer (Flow API)?
    • Ответ: Это механизм обратной связи, при котором подписчик сам регулирует объем входящих данных с помощью вызова subscription.request(n). Издатель отправляет порцию данных только тогда, когда подписчик явно сообщает о готовности обработать очередное количество элементов, что исключает OutOfMemoryError при разнице скоростей генерации и потребления.

Типичные ошибки и Red Flags

  • 🚩 Использование java.util.Observable и Observer в новом коде.
  • 🚩 Итерация по ArrayList без защиты от конкурентной модификации (удаление слушателя внутри вызова onEvent приведет к ConcurrentModificationException).
  • 🚩 Вызов методов подписчиков внутри блока synchronized — гарантированный риск получить Deadlock при сложной иерархии блокировок в слушателях.
  • 🚩 Отправка внешних уведомлений (SMS/Email) из синхронного слушателя без аннотации @Async или очереди задач, блокирующая транзакции и HTTP-потоки приложения.

Связанные темы