Observer Pattern with PropertyChangeSupport in Java

The Observer Pattern implemented with PropertyChangeSupport provides a robust way to implement the publish-subscribe mechanism in Java. This approach leverages Java's built-in property change support for type-safe, event-driven communication.

Basic PropertyChangeSupport Implementation

1. Observable Subject with PropertyChangeSupport

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.Objects;
// Observable subject that notifies observers of property changes
public class UserModel {
private final PropertyChangeSupport support;
private String username;
private String email;
private boolean active;
private int loginCount;
public UserModel() {
this.support = new PropertyChangeSupport(this);
}
public UserModel(String username, String email) {
this();
this.username = username;
this.email = email;
this.active = true;
this.loginCount = 0;
}
// Property change methods
public void setUsername(String newUsername) {
String oldUsername = this.username;
this.username = newUsername;
support.firePropertyChange("username", oldUsername, newUsername);
}
public void setEmail(String newEmail) {
String oldEmail = this.email;
this.email = newEmail;
support.firePropertyChange("email", oldEmail, newEmail);
}
public void setActive(boolean active) {
boolean oldActive = this.active;
this.active = active;
support.firePropertyChange("active", oldActive, active);
}
public void setLoginCount(int loginCount) {
int oldLoginCount = this.loginCount;
this.loginCount = loginCount;
support.firePropertyChange("loginCount", oldLoginCount, loginCount);
}
public void incrementLoginCount() {
setLoginCount(this.loginCount + 1);
}
// Bulk update with single notification
public void updateUser(String username, String email, boolean active) {
// Store old values
String oldUsername = this.username;
String oldEmail = this.email;
boolean oldActive = this.active;
// Update properties
this.username = username;
this.email = email;
this.active = active;
// Fire individual property changes
if (!Objects.equals(oldUsername, username)) {
support.firePropertyChange("username", oldUsername, username);
}
if (!Objects.equals(oldEmail, email)) {
support.firePropertyChange("email", oldEmail, email);
}
if (oldActive != active) {
support.firePropertyChange("active", oldActive, active);
}
// Fire a generic "userUpdated" event
support.firePropertyChange("userUpdated", null, this);
}
// Listener management
public void addPropertyChangeListener(PropertyChangeListener listener) {
support.addPropertyChangeListener(listener);
}
public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
support.addPropertyChangeListener(propertyName, listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
support.removePropertyChangeListener(listener);
}
public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
support.removePropertyChangeListener(propertyName, listener);
}
// Getters
public String getUsername() { return username; }
public String getEmail() { return email; }
public boolean isActive() { return active; }
public int getLoginCount() { return loginCount; }
@Override
public String toString() {
return String.format("User{username='%s', email='%s', active=%s, loginCount=%d}",
username, email, active, loginCount);
}
}

2. Different Observer Implementations

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
// Basic property change listener
public class UserActivityLogger implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
String propertyName = evt.getPropertyName();
Object oldValue = evt.getOldValue();
Object newValue = evt.getNewValue();
System.out.printf("[Logger] Property '%s' changed from '%s' to '%s'%n",
propertyName, oldValue, newValue);
}
}
// Specific property listener
public class EmailChangeListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if ("email".equals(evt.getPropertyName())) {
String oldEmail = (String) evt.getOldValue();
String newEmail = (String) evt.getNewValue();
System.out.printf("[Email Monitor] Email changed from '%s' to '%s'%n",
oldEmail, newEmail);
// Additional business logic
if (isValidEmail(newEmail)) {
System.out.println("✅ New email is valid");
} else {
System.out.println("❌ New email is invalid");
}
}
}
private boolean isValidEmail(String email) {
return email != null && email.contains("@") && email.contains(".");
}
}
// UI Update listener
public class UIUpdateListener implements PropertyChangeListener {
private String componentName;
public UIUpdateListener(String componentName) {
this.componentName = componentName;
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
System.out.printf("[%s UI] Updating display for property '%s'%n",
componentName, evt.getPropertyName());
// Simulate UI update
switch (evt.getPropertyName()) {
case "username":
updateUsernameDisplay(evt.getNewValue());
break;
case "email":
updateEmailDisplay(evt.getNewValue());
break;
case "active":
updateActiveStatusDisplay((Boolean) evt.getNewValue());
break;
case "userUpdated":
refreshEntireDisplay();
break;
}
}
private void updateUsernameDisplay(Object newValue) {
System.out.println("   ↳ Username display updated to: " + newValue);
}
private void updateEmailDisplay(Object newValue) {
System.out.println("   ↳ Email display updated to: " + newValue);
}
private void updateActiveStatusDisplay(boolean active) {
System.out.println("   ↳ Active status display updated to: " + 
(active ? "🟢 Active" : "🔴 Inactive"));
}
private void refreshEntireDisplay() {
System.out.println("   ↳ Refreshing entire user interface");
}
}
// Statistics tracker
public class UserStatisticsTracker implements PropertyChangeListener {
private int totalChanges = 0;
private int loginCountChanges = 0;
@Override
public void propertyChange(PropertyChangeEvent evt) {
totalChanges++;
if ("loginCount".equals(evt.getPropertyName())) {
loginCountChanges++;
int newCount = (Integer) evt.getNewValue();
System.out.printf("[Statistics] Login count changed to %d (Total changes: %d)%n",
newCount, totalChanges);
}
// Log every 10th change
if (totalChanges % 10 == 0) {
System.out.printf("[Statistics] Total changes tracked: %d%n", totalChanges);
}
}
public int getTotalChanges() {
return totalChanges;
}
public int getLoginCountChanges() {
return loginCountChanges;
}
}

Advanced PropertyChangeSupport Patterns

3. Generic Observable Base Class

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
// Generic base class for observable objects
public abstract class ObservableModel {
protected final PropertyChangeSupport support;
protected ObservableModel() {
this.support = new PropertyChangeSupport(this);
}
// Protected method for subclasses to fire property changes
protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
support.firePropertyChange(propertyName, oldValue, newValue);
}
protected void firePropertyChange(String propertyName, int oldValue, int newValue) {
support.firePropertyChange(propertyName, oldValue, newValue);
}
protected void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) {
support.firePropertyChange(propertyName, oldValue, newValue);
}
protected void fireIndexedPropertyChange(String propertyName, int index, 
Object oldValue, Object newValue) {
support.fireIndexedPropertyChange(propertyName, index, oldValue, newValue);
}
// Public listener management
public void addPropertyChangeListener(PropertyChangeListener listener) {
support.addPropertyChangeListener(listener);
}
public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
support.addPropertyChangeListener(propertyName, listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
support.removePropertyChangeListener(listener);
}
public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
support.removePropertyChangeListener(propertyName, listener);
}
public boolean hasListeners(String propertyName) {
return support.hasListeners(propertyName);
}
public PropertyChangeListener[] getPropertyChangeListeners() {
return support.getPropertyChangeListeners();
}
public PropertyChangeListener[] getPropertyChangeListeners(String propertyName) {
return support.getPropertyChangeListeners(propertyName);
}
}

4. Concrete Observable Models

import java.util.ArrayList;
import java.util.List;
// Product model using the observable base class
public class Product extends ObservableModel {
private String name;
private double price;
private int stock;
private List<String> categories;
public Product(String name, double price, int stock) {
this.name = name;
this.price = price;
this.stock = stock;
this.categories = new ArrayList<>();
}
public void setName(String name) {
String oldName = this.name;
this.name = name;
firePropertyChange("name", oldName, name);
}
public void setPrice(double price) {
double oldPrice = this.price;
this.price = price;
firePropertyChange("price", oldPrice, price);
// Also fire a generic price change event
firePropertyChange("priceChanged", null, this);
}
public void setStock(int stock) {
int oldStock = this.stock;
this.stock = stock;
firePropertyChange("stock", oldStock, stock);
// Fire low stock warning if applicable
if (oldStock > 5 && stock <= 5) {
firePropertyChange("lowStock", null, this);
}
// Fire out of stock event
if (oldStock > 0 && stock == 0) {
firePropertyChange("outOfStock", null, this);
}
}
public void addCategory(String category) {
categories.add(category);
fireIndexedPropertyChange("categories", categories.size() - 1, null, category);
}
public void removeCategory(String category) {
int index = categories.indexOf(category);
if (index >= 0) {
categories.remove(index);
fireIndexedPropertyChange("categories", index, category, null);
}
}
// Bulk update
public void updateProduct(String name, double price, int stock) {
// Store old values
String oldName = this.name;
double oldPrice = this.price;
int oldStock = this.stock;
// Update properties
this.name = name;
this.price = price;
this.stock = stock;
// Fire individual changes
if (!java.util.Objects.equals(oldName, name)) {
firePropertyChange("name", oldName, name);
}
if (oldPrice != price) {
firePropertyChange("price", oldPrice, price);
}
if (oldStock != stock) {
firePropertyChange("stock", oldStock, stock);
}
// Fire comprehensive update
firePropertyChange("productUpdated", null, this);
}
// Getters
public String getName() { return name; }
public double getPrice() { return price; }
public int getStock() { return stock; }
public List<String> getCategories() { return new ArrayList<>(categories); }
@Override
public String toString() {
return String.format("Product{name='%s', price=%.2f, stock=%d}", name, price, stock);
}
}
// Shopping cart with observable items
public class ShoppingCart extends ObservableModel {
private List<Product> items;
private double total;
private String customerId;
public ShoppingCart(String customerId) {
this.customerId = customerId;
this.items = new ArrayList<>();
this.total = 0.0;
}
public void addItem(Product product) {
items.add(product);
recalculateTotal();
firePropertyChange("items", null, items);
fireIndexedPropertyChange("items", items.size() - 1, null, product);
firePropertyChange("cartUpdated", null, this);
// Also listen to price changes in the product
product.addPropertyChangeListener("price", evt -> {
recalculateTotal();
firePropertyChange("totalRecalculated", null, this);
});
}
public void removeItem(Product product) {
int index = items.indexOf(product);
if (index >= 0) {
items.remove(index);
recalculateTotal();
firePropertyChange("items", null, items);
fireIndexedPropertyChange("items", index, product, null);
firePropertyChange("cartUpdated", null, this);
}
}
public void clear() {
List<Product> oldItems = new ArrayList<>(items);
items.clear();
total = 0.0;
firePropertyChange("items", oldItems, items);
firePropertyChange("total", total, 0.0);
firePropertyChange("cartCleared", null, this);
}
private void recalculateTotal() {
double newTotal = items.stream()
.mapToDouble(Product::getPrice)
.sum();
double oldTotal = this.total;
this.total = newTotal;
if (oldTotal != newTotal) {
firePropertyChange("total", oldTotal, newTotal);
}
}
// Getters
public List<Product> getItems() { return new ArrayList<>(items); }
public double getTotal() { return total; }
public String getCustomerId() { return customerId; }
public int getItemCount() { return items.size(); }
@Override
public String toString() {
return String.format("ShoppingCart{customerId='%s', items=%d, total=%.2f}",
customerId, items.size(), total);
}
}

Specialized Listeners and Utilities

5. Advanced Listeners

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
// Debounced listener - prevents too frequent updates
public class DebouncedPropertyChangeListener implements PropertyChangeListener {
private final PropertyChangeListener delegate;
private final long delayMillis;
private final ScheduledExecutorService scheduler;
private PropertyChangeEvent lastEvent;
public DebouncedPropertyChangeListener(PropertyChangeListener delegate, long delayMillis) {
this.delegate = delegate;
this.delayMillis = delayMillis;
this.scheduler = Executors.newSingleThreadScheduledExecutor();
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
lastEvent = evt;
scheduler.schedule(() -> {
if (lastEvent == evt) {
delegate.propertyChange(evt);
}
}, delayMillis, TimeUnit.MILLISECONDS);
}
public void shutdown() {
scheduler.shutdown();
}
}
// Filtered listener - only reacts to specific conditions
public class FilteredPropertyChangeListener implements PropertyChangeListener {
private final PropertyChangeListener delegate;
private final String propertyName;
private final java.util.function.Predicate<PropertyChangeEvent> filter;
public FilteredPropertyChangeListener(PropertyChangeListener delegate, 
String propertyName) {
this(delegate, propertyName, evt -> true);
}
public FilteredPropertyChangeListener(PropertyChangeListener delegate,
String propertyName,
java.util.function.Predicate<PropertyChangeEvent> filter) {
this.delegate = delegate;
this.propertyName = propertyName;
this.filter = filter;
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (propertyName.equals(evt.getPropertyName()) && filter.test(evt)) {
delegate.propertyChange(evt);
}
}
}
// Composite listener - combines multiple listeners
public class CompositePropertyChangeListener implements PropertyChangeListener {
private final List<PropertyChangeListener> listeners;
public CompositePropertyChangeListener() {
this.listeners = new ArrayList<>();
}
public void addListener(PropertyChangeListener listener) {
listeners.add(listener);
}
public void removeListener(PropertyChangeListener listener) {
listeners.remove(listener);
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
for (PropertyChangeListener listener : listeners) {
try {
listener.propertyChange(evt);
} catch (Exception e) {
System.err.println("Error in listener: " + e.getMessage());
}
}
}
}
// Async listener - processes events in background thread
public class AsyncPropertyChangeListener implements PropertyChangeListener {
private final PropertyChangeListener delegate;
private final java.util.concurrent.ExecutorService executor;
public AsyncPropertyChangeListener(PropertyChangeListener delegate) {
this.delegate = delegate;
this.executor = Executors.newCachedThreadPool();
}
public AsyncPropertyChangeListener(PropertyChangeListener delegate,
java.util.concurrent.ExecutorService executor) {
this.delegate = delegate;
this.executor = executor;
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
executor.submit(() -> {
try {
delegate.propertyChange(evt);
} catch (Exception e) {
System.err.println("Error in async listener: " + e.getMessage());
}
});
}
public void shutdown() {
executor.shutdown();
}
}

6. Observer Management Utility

import java.beans.PropertyChangeListener;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
// Utility class for managing observers with weak references
public class ObserverManager {
private final Map<String, CopyOnWriteArrayList<PropertyChangeListener>> listeners;
private final Object source;
public ObserverManager(Object source) {
this.source = source;
this.listeners = new HashMap<>();
}
public void addListener(String propertyName, PropertyChangeListener listener) {
listeners.computeIfAbsent(propertyName, k -> new CopyOnWriteArrayList<>())
.add(listener);
}
public void removeListener(String propertyName, PropertyChangeListener listener) {
CopyOnWriteArrayList<PropertyChangeListener> list = listeners.get(propertyName);
if (list != null) {
list.remove(listener);
if (list.isEmpty()) {
listeners.remove(propertyName);
}
}
}
public void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
// Fire for specific property
CopyOnWriteArrayList<PropertyChangeListener> specificListeners = listeners.get(propertyName);
if (specificListeners != null) {
PropertyChangeEvent event = new PropertyChangeEvent(source, propertyName, oldValue, newValue);
for (PropertyChangeListener listener : specificListeners) {
listener.propertyChange(event);
}
}
// Fire for wildcard listeners (listening to all properties)
CopyOnWriteArrayList<PropertyChangeListener> wildcardListeners = listeners.get(null);
if (wildcardListeners != null) {
PropertyChangeEvent event = new PropertyChangeEvent(source, propertyName, oldValue, newValue);
for (PropertyChangeListener listener : wildcardListeners) {
listener.propertyChange(event);
}
}
}
public boolean hasListeners(String propertyName) {
CopyOnWriteArrayList<PropertyChangeListener> list = listeners.get(propertyName);
return list != null && !list.isEmpty();
}
public void clear() {
listeners.clear();
}
}

Comprehensive Demo

7. Complete Demonstration

public class PropertyChangeSupportDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("=== PropertyChangeSupport Demo ===\n");
demoBasicUsage();
demoAdvancedPatterns();
demoShoppingCart();
demoSpecializedListeners();
}
private static void demoBasicUsage() {
System.out.println("1. BASIC USAGE DEMO");
System.out.println("===================");
// Create observable model
UserModel user = new UserModel("john_doe", "[email protected]");
// Create and register listeners
UserActivityLogger logger = new UserActivityLogger();
EmailChangeListener emailMonitor = new EmailChangeListener();
UIUpdateListener uiUpdater = new UIUpdateListener("Profile");
UserStatisticsTracker stats = new UserStatisticsTracker();
user.addPropertyChangeListener(logger);
user.addPropertyChangeListener("email", emailMonitor);
user.addPropertyChangeListener(uiUpdater);
user.addPropertyChangeListener(stats);
// Make changes that trigger notifications
System.out.println(">>> Changing username:");
user.setUsername("john_smith");
System.out.println("\n>>> Changing email:");
user.setEmail("[email protected]");
System.out.println("\n>>> Changing active status:");
user.setActive(false);
System.out.println("\n>>> Incrementing login count:");
user.incrementLoginCount();
user.incrementLoginCount();
System.out.println("\n>>> Bulk update:");
user.updateUser("jane_doe", "[email protected]", true);
// Remove a listener
user.removePropertyChangeListener(logger);
System.out.println("\n>>> Change after removing logger:");
user.setUsername("jane_smith");
System.out.println("\n📊 Final Statistics:");
System.out.println("Total changes tracked: " + stats.getTotalChanges());
System.out.println("Login count changes: " + stats.getLoginCountChanges());
}
private static void demoAdvancedPatterns() {
System.out.println("\n\n2. ADVANCED PATTERNS DEMO");
System.out.println("========================");
Product laptop = new Product("Laptop", 999.99, 10);
// Create specialized listeners
PropertyChangeListener priceWatcher = evt -> {
if ("price".equals(evt.getPropertyName())) {
double oldPrice = (Double) evt.getOldValue();
double newPrice = (Double) evt.getNewValue();
double change = newPrice - oldPrice;
System.out.printf("[Price Watcher] Price changed by: $%.2f (%+.2f%%)%n",
change, (change / oldPrice) * 100);
}
};
PropertyChangeListener stockMonitor = evt -> {
if ("stock".equals(evt.getPropertyName())) {
int newStock = (Integer) evt.getNewValue();
if (newStock <= 2) {
System.out.printf("[Stock Monitor] ⚠️  Low stock alert: %d items left%n", newStock);
}
}
};
PropertyChangeListener outOfStockNotifier = evt -> {
if ("outOfStock".equals(evt.getPropertyName())) {
System.out.println("[Out of Stock] 🚨 Product is out of stock!");
}
};
// Register listeners
laptop.addPropertyChangeListener("price", priceWatcher);
laptop.addPropertyChangeListener("stock", stockMonitor);
laptop.addPropertyChangeListener("outOfStock", outOfStockNotifier);
// Simulate changes
System.out.println(">>> Price changes:");
laptop.setPrice(899.99);
laptop.setPrice(849.99);
System.out.println("\n>>> Stock changes:");
laptop.setStock(5);
laptop.setStock(2);
laptop.setStock(0);
System.out.println("\n>>> Adding categories:");
laptop.addCategory("Electronics");
laptop.addCategory("Computers");
}
private static void demoShoppingCart() {
System.out.println("\n\n3. SHOPPING CART DEMO");
System.out.println("====================");
ShoppingCart cart = new ShoppingCart("customer123");
// Create cart listeners
PropertyChangeListener cartLogger = evt -> {
System.out.printf("[Cart] %s: %s -> %s%n",
evt.getPropertyName(), evt.getOldValue(), evt.getNewValue());
};
PropertyChangeListener totalWatcher = evt -> {
if ("total".equals(evt.getPropertyName())) {
double newTotal = (Double) evt.getNewValue();
System.out.printf("[Total Watcher] Cart total: $%.2f%n", newTotal);
}
};
cart.addPropertyChangeListener(cartLogger);
cart.addPropertyChangeListener("total", totalWatcher);
// Create products
Product laptop = new Product("Laptop", 999.99, 5);
Product mouse = new Product("Mouse", 29.99, 10);
Product keyboard = new Product("Keyboard", 79.99, 8);
System.out.println(">>> Adding items to cart:");
cart.addItem(laptop);
cart.addItem(mouse);
cart.addItem(keyboard);
System.out.println("\n>>> Changing product price (should update cart total):");
laptop.setPrice(899.99);
System.out.println("\n>>> Removing item:");
cart.removeItem(mouse);
System.out.println("\n>>> Clearing cart:");
cart.clear();
System.out.println("\n📊 Final Cart State: " + cart);
}
private static void demoSpecializedListeners() throws InterruptedException {
System.out.println("\n\n4. SPECIALIZED LISTENERS DEMO");
System.out.println("============================");
UserModel user = new UserModel("test_user", "[email protected]");
// Debounced listener example
System.out.println(">>> Debounced Listener (300ms delay):");
DebouncedPropertyChangeListener debouncedLogger = 
new DebouncedPropertyChangeListener(
evt -> System.out.printf("[Debounced] %s changed%n", evt.getPropertyName()),
300
);
user.addPropertyChangeListener(debouncedLogger);
// Rapid changes - only the last one should trigger
user.setUsername("user1");
user.setUsername("user2");
user.setUsername("user3");
user.setUsername("final_user");
Thread.sleep(500); // Wait for debounced execution
// Filtered listener example
System.out.println("\n>>> Filtered Listener (only significant price changes):");
Product product = new Product("Test Product", 100.0, 10);
FilteredPropertyChangeListener priceFilter = new FilteredPropertyChangeListener(
evt -> System.out.printf("[Price Filter] Significant change: $%.2f -> $%.2f%n",
evt.getOldValue(), evt.getNewValue()),
"price",
evt -> {
double oldPrice = (Double) evt.getOldValue();
double newPrice = (Double) evt.getNewValue();
return Math.abs((newPrice - oldPrice) / oldPrice) > 0.1; // More than 10% change
}
);
product.addPropertyChangeListener(priceFilter);
product.setPrice(105.0); // 5% change - filtered out
product.setPrice(115.0); // 9.5% change - filtered out
product.setPrice(130.0); // 13% change - should trigger
// Cleanup
debouncedLogger.shutdown();
}
}

Key Benefits of PropertyChangeSupport

  1. Type Safety: Compile-time type checking for property changes
  2. Built-in Java Support: Part of standard Java library (java.beans)
  3. Flexible Listening: Can listen to all properties or specific ones
  4. Rich Event Information: PropertyChangeEvent contains old/new values and property name
  5. Thread Safety: PropertyChangeSupport is thread-safe
  6. Memory Management: Proper listener management prevents memory leaks
  7. Integration: Works well with JavaBeans and various frameworks

Common Use Cases

  • UI Updates: Notifying UI components of model changes
  • Data Binding: Synchronizing data between different parts of application
  • Caching Systems: Invalidating caches when underlying data changes
  • Audit Logging: Tracking changes to important business objects
  • Real-time Updates: Pushing changes to connected clients
  • Configuration Management: Reacting to configuration changes

The PropertyChangeSupport approach provides a robust, standardized way to implement the Observer pattern in Java applications, particularly suitable for desktop applications, data binding scenarios, and any situation where you need type-safe property change notifications.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper