On this page, we will revisit the topic of events (see Events, in Lesson 7). We will create our own custom event, the NotificationEvent, which can be used to notify clients when an event has occurred, for example, the operator has entered new data. To encapsulate the type of client that receives notifications, we will need a new interface, the NotificationListener. And to keep track of listeners and dispatch notification events, we will implement a new class, the NotificationManager. We’ll then write JUnit tests to validate this notification framework and prepare for using it to drive our GUI on the next page.
GitHub repository: Cartesian Plane Part 8
Previous page: Cartesian Plane Lesson 8: Streaming
Notification Events
The main topic of this part of the conversation is how to create custom events. Let’s say for now that an event is “something that occurs.” This something has to have a definite description:
- A fire alarm went off
- The doorbell rang
- In a GUI, a push button was pressed, or…
- … a toggle button changed state, or…
- … an item in a list was selected
For an event to be useful, three things are required:
- A description of the event
- A set of listeners to be informed when the event occurs
- One or more managers to keep track of listeners and to issue notifications when the event occurs.
In Java, an event is encapsulated in an event object. We’ve already seen an example of this in the PropertyChangeEvent class, which describes a change to a property. A listener is described by an interface. The interface designates one or more abstract methods to be invoked when an event occurs. This method has at least one (usually exactly one) parameter corresponding to the associated event object. In our discussion of property management, we looked at the PropertyChangeListener interface, which has the abstract method propertyChange(PropertyChangeEvent event). In our Cartesian plane application, we have (so far) one object that acts as a manager of property change events, the PropertyManager class, but there’s no reason why there can’t be many managers of an event. In Swing, one of Java’s packages for implementing GUIs, property change management is performed by various GUI components, including JSpinner, JToggleButton, and JCheckBox. Event managers provide methods for adding and removing event listeners and dispatching events to them.
Here are some more events that are implemented in the Java libraries:
- Action Event
- Event object: ActionEvent
- Listener interface: ActionListener
- Abstract method: actionPerformed( ActionEvent event )
- Examples of managers: JButton, JToggleButton, JTextField
- Keyboard Focus Event
- Event object: FocusEvent
- Listener interface: FocusListener
- Abstract methods: focusGained( FocusEvent event ), focusLost( FocusEvent event )
- Examples of managers: JTextField, JFrame, JComboBox
- Mouse Event
- Event object: MouseEvent
- Listener interface: MouseListener
- Abstract methods: mouseClicked( MouseEvent event ), mouseEntered( MouseEvent event ), mouseExited( MouseEvent event ), mousePressed( MouseEvent event ), mouseReleased( MouseEvent event )
- Examples of managers: JFrame, JPanel, JToolBar
So let’s make our own notification event for the Cartesian plane project. Our motivation, to begin with, is taken from a scenario such as:
- The user changes…
- …the color of the grid lines…
- …the background color…
- …the width of the major tic marks…
- …the grid unit…
- …(miscellaneous other things that affect the display of the Cartesian plane)…
- The user issues a notification that the graphics must be updated to reflect the above changes.
The idea is that we want to make many changes to how our graphics are displayed, but we don’t want the screen to flash with each change; we want to apply all the changes and then redisplay the graphics.
At the moment, our only interest in notifications is to tell the CartesianPlane object to redraw itself, but eventually, we will want to manage a variety of notifications, such as user has entered new data. For each type of notification, we will designate a property, such as display must be redrawn. We’ll give it a property name such as “redraw” and add a corresponding symbol to CPConstants, such as REDRAW_NP (let’s decide right now that constant symbols associated with notifications will end with the suffix _NP for “notification property”). Here’s what I have added to CPConstants, just before the static methods at the end of the file:
1 2 3 4 5 6 7 8 9 | // ... ///////////////////////////////////////////////// // Notification properties ///////////////////////////////////////////////// /** Notifies the application that the graphic must be redrawn. */ public static final String REDRAW_NP = "redraw"; // ... |
The NotificationEvent Class
Now we need an event class, NotificationEvent. Let’s give it three fields:
- Object source: the source of the event.
- String property: the name of the property associated with the notification.
- Object data: additional data associated with the event. Not all notifications will use this field; in particular, the redraw notification will not. It is present here to facilitate the introduction of new notification properties.
For convenience, let’s give our class two constructors:
- NotificationEvent( Object source, String property, Object data ) will initialize all three fields of the object;
- NotificationEvent( Object source, String property ) will initialize the source and property fields to the given values, and set the data field to null.
Finally, our class will have getters for the three fields. Here’s the code for the class (comments have been removed for brevity). Note that the two-parameter constructor chains to the three-parameter constructor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | public class NotificationEvent { private final Object source; private final String property; private final Object data; public NotificationEvent( Object source, String property, Object data ) { this.source = source; this.property = property; this.data = data; } public NotificationEvent( Object source, String property ) { this( source, property, null ); } public Object getData() { return data; } public Object getSource() { return source; } public String getProperty() { return property; } } |
The NotificationListener Interface
Next, we need a listener interface, NotificationListener. We’ll give this interface one abstract method, which will be invoked when the listener is notified: accept(NotificationEvent event).
1 2 3 4 5 6 7 8 9 10 | @FunctionalInterface public interface NotificationListener { /** * Accepts a NotificationEvent. * * @param event the NotificationEvent */ void accept( NotificationEvent event ); } |
Note 1: Since this interface has exactly one abstract method, I can designate it a functional interface. See Java Anonymous Class Primer.
Note 2: By naming the abstract method accept, this interface can also serve as a Consumer<NotificationEvent>.
Note 3: In an interface, method declarations are, by default, public and abstract. So: void accept( NotificationEvent event );
is equivalent to: public abstract void accept( NotificationEvent event )
The NotificationManager Class
As we did with properties, let’s put notification management in one place, the NotificationManager class. Also, as we did with property management, let’s make this a singleton, implemented as an enum:
public enum NotificationManager
{
/** The single instance associated with this class. */
INSTANCE;
// ...
}
Let’s keep track of listeners using the same strategy we used in PropertyManager; we’ll register listeners that want to be notified of all events, and listeners interested only in events for a single property. For listeners of all events, we’ll have a simple list of NotificationListeners. For those who are interested in notification of a single property, we will have a map of property names to a List<NotificationListener>:
/** List of NotificationListeners */
private final List<NotificationListener> notificationListeners =
new ArrayList<>();
/** map notification listeners to a specific property */
private final Map<String, List<NotificationListener>>
notificationPropertyMap = new HashMap<>();
We’ll need two public add methods, one for the NotificationListener list, and one for the property→NotificationListener-list map:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // ... public void addNotificationListener( NotificationListener listener ) { notificationListeners.add( listener ); } public void addNotificationListener( String property, NotificationListener listener ) { List<NotificationListener> listenerList = notificationPropertyMap.get( property ); if ( listenerList == null ) { listenerList = new ArrayList<>(); notificationPropertyMap.put( property, listenerList ); } listenerList.add( listener ); } // ... |
Digression: there’s an improvement we can make to addNotificationListener( String, NotificationListener). It’s not terribly important, but it’s a cleaner solution, and it gives us an opportunity for another lambda example. It entails using the Map interface’s computeIfAbsent method:
V computeIfAbsent( K key, Function<K,V> mappingFunction )
If the given key is not in the map, this method uses mappingFunction to attempt to compute it and add it to the map. Using it, we can replace lines 10-15 with:
notificationPropertyMap
.computeIfAbsent( property, k -> new ArrayList<>() )
.add( listener );
If property is not in the map, computeIfAbsent will instantiate an ArrayList<NotificationListener> and add it to the map with property as the key. The method returns the instantiated list, so we can then chain the .add(listener) invocation. The complete method looks like this:
1 2 3 4 5 6 7 | public void addNotificationListener( String property, NotificationListener listener ) { notificationPropertyMap .computeIfAbsent( property, k -> new ArrayList<>() ) .add( listener ); } |
And we’ll need two remove methods, one for the listener list, and one for the property→listener-list map:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // ... public void removeNotificationListener( NotificationListener listener ) { notificationListeners.remove( listener ); } public void removeNotificationListener( String property, NotificationListener listener ) { List<NotificationListener> list = notificationPropertyMap.get( property ); if ( list != null ) list.remove( listener ); } // ... |
Next, let’s have a method to propagate a notification to all the appropriate listeners, an “appropriate listener” being every listener in the NotificationListener list, and every per-property listener for a given property from the map:
1 2 3 4 5 6 7 8 9 10 11 12 | public void propagateNotification( NotificationEvent event ) { List<NotificationListener> perPropertyList = notificationPropertyMap.getOrDefault( event.getProperty(), new ArrayList<>() ); Stream.concat( perPropertyList.stream(), notificationListeners.stream() ).forEach( l -> l.accept( event ) ); } |
A couple of notes about the above code:
- Line 4: The two arguments to getOrDefault in the Map are:
- The key to search for in the map; if found, the associated value (type List<NotificationListener>) is returned.
- If the key is not found, this argument (an empty List<NotificationListener>, in this case) is returned. So:
- Line 3: perPropertyList will be instantiated, even if it is empty.
- Lines 8 – 10: We generate streams from the perPropertyList and notificationListeners and concatenate them into a single stream, type Stream<NotificationListener>.
- Line 11: We call the accept method for each listener in the stream.
- Line 11: Digression: There is a potential multi-thread issue here. If a listener’s accept method adds or removes a listener from the listener list, we could get a ConcurrentModificationException. Since we expect this code to run on a single thread (the EDT), it should not be a problem. In code that could be multithreaded, the solution is to add all listeners to a third list, then invoke all the listeners from the third list:
List<NotificationListener> toInvoke = new ArrayList<>();
toInvoke.addAll( perPropertyList );
toInvoke.addAll( notificationListeners );
toInvoke.forEach( l -> l.accept( event ) );
Finally, let’s make some convenience methods to help out our users:
- propagateNotification(Object source, String property, Object data) will instantiate a NotificationEvent from the given values and pass it to propagateNotification(NotificationEvent event).
- propagateNotification(Object source, String property) will do the same thing, defaulting the data value to null.
- and propagateNotification(String property) will, again, serve the same purpose, this time allowing the data value to default to null and the source value to default to the NotificationManager itself.
Here’s the code for the three convenience methods.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public void propagateNotification( String property ) { propagateNotification( INSTANCE, property, null ); } public void propagateNotification( Object source, String property ) { propagateNotification( source, property, null ); } public void propagateNotification( Object source, String property, Object data ) { NotificationEvent event = new NotificationEvent( source, property, data ); propagateNotification( event ); } |
To complete this section, we have to go to the CartesianPlane class, and tell it to register itself to receive redraw notifications. That’s one line of code in the constructor:
1 2 3 4 5 6 7 8 9 10 11 12 | public CartesianPlane( int width, int height ) { Dimension dim = new Dimension( width, height ); setPreferredSize( dim ); pmgr.addPropertyChangeListener( this ); // Register listener for redraw notifications NotificationManager.INSTANCE.addNotificationListener( CPConstants.REDRAW_NP, e -> repaint() ); } |
Testing the Notification Event Classes
Before we move on, let’s make sure that our new code works. Since NotificationListener is an interface with no default or static methods, there is nothing to test. So we’ll start with a JUnit test for NotificationEvent before moving on to NotificationManager.
Testing the NotificationEvent Class
🟦 As a reminder:
- All test classes are created on the test source code branch (src/test/java).
- Since NotificationEvent is in the com.acmemail.judah.cartesian_plane package, its test class will reside in the same package on the test branch.
- To create the test in Eclipse, right-click on the NotificationEvent class name in the package manager and select new→JUnit test.
- The new JUnit Test dialog will be displayed with the test class name already filled in: NotificationEventTest.
- Select the next button to proceed to the next dialog. In the next dialog, select all the methods directly under NotificationEvent, then click Finish.
- Verify that Eclipse put the new test class in the correct package on the test source code branch. Adjust if necessary.

🟦 JUnit Tests
The tests for the two constructors are quite straightforward, so much so that you might be tempted to take shortcuts. Don’t do this. If you’re doing this exercise for practice, treat it as if you were doing it on a real project. If you’re writing code similar to this on a real project, treat your test code with the same care you would use for your production code. To test the three-parameter constructor, create three variables reflecting the target source, property, and data values, then use those variables to instantiate a test object; don’t jump right into encapsulating the test object with constant values. Then, validate the instantiated object against the expected values contained in the variables.
1 2 3 4 5 6 7 8 9 10 11 12 13 | @Test void testNotificationEventObjectStringObject() { Object source = this; String property = "prop"; Integer data = Integer.valueOf( 42 ); NotificationEvent event = new NotificationEvent( source, property, data ); assertEquals( source, event.getSource() ); assertEquals( property, event.getProperty() ); assertEquals( data, event.getData() ); } |
The test for the two-parameter constructor is very similar to the test for the three-parameter constructor; the main difference is that, after instantiation, make sure the data field of your test object is null.
1 2 3 4 5 6 7 8 9 10 11 12 | @Test void testNotificationEventObjectString() { Object source = this; String property = "prop"; NotificationEvent event = new NotificationEvent( source, property ); assertEquals( source, event.getSource() ); assertEquals( property, event.getProperty() ); assertNull( event.getData() ); } |
The test for the one-parameter constructor is almost the same as above, except that after instantiating the NotificationEvent object, you must verify that the source is equal to NotificationManager.INSTANCE. The implementation is left as an exercise.
The tests for each of the three getters are nearly inconsequential. In fact, if I were your project manager, I would likely consider the code for this class to be fully tested at this point. You might still want to put in a test case that tests all three getters at once. And, of course, if you’re working for an organization that insists on a separate test for every public method, write all three test methods, even if they’re essentially equivalent. Here’s the code for one of them.
1 2 3 4 5 6 7 8 9 10 11 | @Test void testGetData() { Object source = this; String property = "prop"; Integer data = Integer.valueOf( 42 ); NotificationEvent event = new NotificationEvent( source, property, data ); assertEquals( data, event.getData() ); } |
Testing the NotificationManager Class
Testing the manager class is a lot more fun. First, create the JUnit test class on the test source code branch. Next, to facilitate testing, add a nested class that implements NotificationListener. This class will have four instance fields: three to store the three values passed in a NotificationEvent object, and a Boolean field that says whether or not it was notified of an event. For convenience, I have put a variable at the top of the test class to encapsulate the unwieldy NotificationManager.INSTANCE expression, and added a helper method to reset a test object between parts of a test.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | class NotificationManagerTest { private static final NotificationManager nMgr = NotificationManager.INSTANCE; // ... private static void resetTesters( TestListener... listeners ) { for ( TestListener listener : listeners ) { listener.invoked = false; listener.source = null; listener.property = null; listener.data = null; } } private static class TestListener implements NotificationListener { private boolean invoked = false; private Object source = null; private String property = null; private Object data = null; public void accept( NotificationEvent event ) { invoked = true; source = event.getSource(); property = event.getProperty(); data = event.getData(); } } } |
The test for addNotificationListener(NotificationListener listener) entails creating two test listeners, adding them to the notification manager, dispatching an event, and verifying that both listeners are notified.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @Test void testAddNotificationListenerNotificationListener() { TestListener test1 = new TestListener(); TestListener test2 = new TestListener(); String property = "prop"; nMgr.addNotificationListener( test1 ); nMgr.addNotificationListener( test2 ); nMgr.propagateNotification( property ); assertTrue( test1.invoked ); assertTrue( test2.invoked ); assertEquals( property, test1.property ); assertEquals( property, test2.property ); } |
Exercise: Write the tester for the addNotificationListener(String, NotificationListener) yourself before looking at the solution below.
For the per-property add-listener method, I have to:
- Add a listener for all properties:
nMgr.addNotificationListener( testAll ); - Add two listeners for one specific property, and a listener for a different property:
nMgr.addNotificationListener( prop1, testProp1_1 );
nMgr.addNotificationListener( prop1, testProp1_2 );
nMgr.addNotificationListener( prop2, testProp2 ); - Propagate an event for the first property and verify that:
- testAll is notified.
- testProp1_1 and testProp1_2 are notified; and
- testProp2 is not notified
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | @Test void testAddNotificationListenerStringNotificationListener() { TestListener testAll = new TestListener(); TestListener testProp1_1 = new TestListener(); TestListener testProp1_2 = new TestListener(); TestListener testProp2 = new TestListener(); String prop1 = "prop1"; String prop2 = prop1 + "___"; nMgr.addNotificationListener( testAll ); nMgr.addNotificationListener( prop1, testProp1_1 ); nMgr.addNotificationListener( prop1, testProp1_2 ); nMgr.addNotificationListener( prop2, testProp2 ); nMgr.propagateNotification( prop1 ); assertTrue( testAll.invoked ); assertEquals( prop1, testAll.property ); assertTrue( testProp1_1.invoked ); assertEquals( prop1, testProp1_1.property ); assertTrue( testProp1_2.invoked ); assertEquals( prop1, testProp1_2.property ); assertFalse( testProp2.invoked ); } |
To test removeNotificationListener(NotificationListener), use a strategy such as this one:
- Add two listeners to the notification manager.
- Propagate a notification and verify that both listeners are notified.
- Reset the TestListener objects.
- Remove the second listener.
- Propagate a notification; verify that the first listener is notified, but the second is not.
- Reset the TestListener objects.
- Remove the first listener.
- Propagate a notification and verify that neither listener is notified.
Exercise: Write the tester for the removeNotificationListener( NotificationListener ) yourself before looking at the solution in the repository.
I’m not going to include the code for the test method here, but if you want to take a look, you can find it in the GitHub repository.
Exercise: Develop a test strategy for removeNotificationListener( String, NotificationListener ) yourself. Implement your strategy before looking at the solution in the GitHub repository.
To test removeNotificationListener( String, NotificationListener ), try the strategy below. Once again, I’m not going to include the code for this test, but you can find it in the GitHub repository.
- Add a listener for all properties (testAll).
- Add two listeners for “property 1” (testProp1_1, testProp1_2).
- Add one listener for “property 2” (testProp2).
- Propagate a notification for “property 1”; verify that:
- testAll, testProp1_1, testProp1_2 are invoked;
- testProp2 is not invoked.
- Reset the test objects.
- Remove the testProp1_2 listener.
- Propagate a notification for “property 1”; verify that:
- testAll, testProp1_1 are invoked;
- testProp1_2 and testProp2 are not invoked.
- Reset the test objects.
- Remove the testProp1_1 listener.
- Propagate a notification for “property 1”; verify that:
- testAll is invoked;
- testProp1_1, testProp1_2, testProp2 are not invoked.
Exercise: before looking at the next section, develop test strategies and implementations for the four propagateNotification method overloads in the NotificationManager class.
There are now four overloads of the propagateNotification method to test. Here’s how I tested them.
- propagateNotification( NotificationEvent event ):
- Add a notification listener.
- Create and propagate a NotificationEvent; ensure that all fields are explicitly set to non-null values.
- Verify that a test listener was notified with the correct source, property, and data.
- propagateNotification( Object source, String property, Object data ):
- Add a notification listener.
- Invoke propagateNotification(Object, String, Object), being sure to specify explicit, non-null objects for all arguments.
- Verify that the test listener was notified with the correct source, property, and data.
- propagateNotification( Object source, String property ):
- Add a notification listener.
- Invoke propagateNotification(Object source, String property), being sure to specify an explicit, non-null object for the source argument.
- Verify that the test listener was notified with the correct source and property, and that the data was null.
- propagateNotification( String property ):
- Add a notification listener.
- Invoke propagateNotification(String property).
- Verify that the test listener was notified with the correct property, the NotificationManager instance for the source, and null for the data.
Here are two of the four test methods. The complete code can be found in the GitHub repository.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | // ... @Test void testPropagateNotificationString() { String property = "prop"; TestListener listener = new TestListener(); nMgr.addNotificationListener( listener ); nMgr.propagateNotification( property ); assertTrue( listener.invoked ); assertEquals( nMgr, listener.source ); assertEquals( property, listener.property ); assertNull( listener.data ); } @Test void testPropagateNotificationNotificationEvent() { Object source = new Object(); String property = "prop"; Integer data = Integer.valueOf( 42 ); NotificationEvent event = new NotificationEvent( source, property, data ); TestListener listener = new TestListener(); nMgr.addNotificationListener( listener ); nMgr.propagateNotification( event ); assertTrue( listener.invoked ); assertEquals( source, listener.source ); assertEquals( property, listener.property ); assertEquals( data, listener.data ); } // ... |
Summary
On this page, we continued our discussion of events and event listeners. We defined our own event type, the NotificationEvent, declared an interface to identify clients interested in receiving event notifications, the NotificationListener, and implemented a manager class to register listeners and dispatch notification events, the NotificationManager. We finished by writing JUnit tests to thoroughly validate our new facility. These tests give us confidence that the notification framework behaves correctly and is ready to support our GUI work.
On the next page, we’ll put this notification framework to work by building GUI utilities that control the properties of a point to be plotted on our Cartesian plane.