google-site-verification: google61fe8ba583a51912.html
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