Як реалізований Observer у Java
Підписка на YouTube-канал або новинну розсилку. Автор відео (Subject) не знає кожного глядача особисто. Глядачі підписуються (subscribe), і коли виходить новий ролик, сповіщення...
🟢 Junior Level
Observer (Спостерігач / Видавець-Підписник) — це поведінковий патерн проєктування, який створює механізм підписки, що дозволяє одним об’єктам (спостерігачам / підписникам) стежити за подіями, які відбуваються в іншому об’єкті (суб’єкті / видавці), та автоматично реагувати на них.
Побутова аналогія
Підписка на YouTube-канал або новинну розсилку. Автор відео (Subject) не знає кожного глядача особисто. Глядачі підписуються (subscribe), і коли виходить новий ролик, сповіщення автоматично розсилається всім активним підписникам (Observers).
Класична реалізація на чистій Java
import java.util.ArrayList;
import java.util.List;
// 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+)
public class Main {
public static void main(String[] args) {
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 (пакет java.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 removeListener(PropertyChangeListener listener) {
support.removePropertyChangeListener(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:
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import java.math.BigDecimal;
// Подія (незмінний 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 Root через колекцію видавця.
- Відповідь: Видавець зберігає сильні посилання (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