Practical apps

In the Online Only Bank data model, the JavaBeans mechanism was employed to allow users of the data model to register for property change events. This gives you a great deal of functionality in these classes, but it also introduces the potential memory leak discussed earlier. Your data model objects will be referred to by various parts of the program that will then register themselves as listeners for property change events, creating a cycle of strong-reference dependencies.

A Weak Collection

Ideally, you should be able to alter your code so that the objects in the data model register their property change listeners in a nonbinding way. To do this, you need to create a different kind of collection in which to store your listeners—a collection based on weak references. To store your listeners, create a class called WeakHashSet. If you look in the JDK source code for HashSet, you will see that it is implemented by using the keys in the HashMap class. You implement WeakHashSet in a similar way, using the java.lang.util.WeakHashMap class as the backing store for the WeakHashSet contents. The keys in WeakHashMap are stored in weak references instead of in strong references, so this class will fit your needs nicely.

Screenshot

The fact that the class WeakHashSet isn't in the JDK already is something of a mystery. Sun implemented the WeakHashMap class but failed to finish the job with the WeakHashSet.


I won't bore you with all of the implementation details, since most of them are fairly trivial. If you want to read the whole source, you can find it in the oracle.hcj.references package. However, there are some noteworthy aspects of your new WeakHashSet class. Consider this source snippet:

public class WeakHashSet extends AbstractSet implements Set {
 private static final Object DUMMY = new String("DUMMY"); // $NON-NLS-1$
 WeakHashMap backingStore = new WeakHashMap( );
 public boolean add(final Object o) {
 if (o == null) {
 throw new NullPointerException( );
 }
 return backingStore.put(o, DUMMY) == null;
 }


Items were placed in the backing store WeakHashMap by using a dummy object for the value. When you place this object into the backing store, the WeakHashMap class creates a new WeakReference object to o, and this reference is stored as the key. Also note that unlike the HashSet class, you do not allow null in WeakHashSet. Since your set is meant to store only references to objects, there is no point in making a reference to null. Therefore, you disallow it. The next point of interest in the WeakHashSet class is the operation of the set iterator. Fortunately, the WeakHashMap does most of the work:

 public Iterator iterator( ) {
 return backingStore.keySet( ).iterator( );
 }


Although this iterator seems normal, it's not. Normally, you expect to be able to iterate over the contents of a set, and to have that set be fixed for the duration of the iteration. For example, if you iterate over a HashSet, and during the iteration, call add( ), you will get a ConcurrentModificationException. However, when using WeakHashSet, you must remember that the contents of the set can change from second to second. Since the garbage collector is working in the background to remove objects, there is always a second thread that silently modifies the contents. As you iterate through the set, the object you encountered three iterations ago could have been garbage collected and removed from the set before the fourth iteration. Essentially, concurrent modifications are the rule rather than the exception.

Screenshot

The WeakHashSet iterator will give you back strong references to the objects in the collection. However, you shouldn't store the actual reference object with the intention of using the referent later. If you store only the reference, the referent could be garbage collected between the time you store it and the time you actually try to use it.


Testing WeakHashSet

Since you will use the WeakHashSet class over and over again, it would be a good idea to test the class to make sure it performs its job properly. Conveniently, testing WeakHashSet will also give you the chance to explore ReferenceQueues. However, you first have to do a bit of setup work on your unit test. To test the new WeakHashSet class, take advantage of the JUnit testing framework. Place all of your test cases in a subpackage of oracle.hcj.references called _test. This is a good strategy for sorting test cases because it allows you to keep your tests with the code that is being tested. Also, the underscore prefix for testing packages separates them from nontesting packages, making it easy to filter the testing packages out of production builds. The majority of the test, found in oracle.hcj.references._test, is fairly standard JUnit test code. The testWeakRefCleanup method contains the fancy work. However, before you get to the testing method, you need to create some preliminary test objects. This is done in the static initialization code:

package oracle.hcj.references._test;
public class TestWeakHashSet extends TestCase {
 private static final String A = new String("Object A");
 private static final String B = new String("Object B");
 private static final String C = new String("Object C");
 private static final String D = new String("Object D");
 private static final String E = new String("Object E");
 private static final String F = new String("Object F");
 private static final String G = new String("Object G");
 private static final Set TEST_SET;
 static {
 TEST_SET = new HashSet( );
 TEST_SET.add(A);
 TEST_SET.add(B);
 TEST_SET.add(C);
 TEST_SET.add(D);
 TEST_SET.add(E);
 TEST_SET.add(F);
 TEST_SET.add(G);
 }


To prepare the test, create a master set of test objects that contain String objects. This will serve as your control group; the objects should never be removed from the set because their strong references will never be dropped. Later, you will use this set to compare the contents of the WeakHashSet and to make sure that these objects are still in the set no matter what you do to other parts of the set. Once this setup is complete, you can focus on the testWeakRefCleanup method. The testWeakRefCleanup method is designed to test the weak-reference part of the set. Its purpose is to prove that objects will be removed if they go out of scope. To examine how this test works, we will examine each phase of the method. The setup phase is shown here:

 public void testWeakRefCleanup( ) {
 try {
 String x = new String("Object x");
 String y = new String("Object y");
 String z = new String("Object z");
 ReferenceQueue queue = new ReferenceQueue( );
 WeakReference refx = new WeakReference(x, queue);
 WeakReference refy = new WeakReference(y, queue);
 WeakReference refz = new WeakReference(z, queue);
 // *** add everything to the WeakHashSet
 WeakHashSet whs = new WeakHashSet( );
 whs.addAll(TEST_SET);
 whs.add(x);
 whs.add(y);
 whs.add(z);


In this part of the test method, you instantiate several new objects with strong references (x, y, and z) to those objects. You then create a new WeakHashSet and weak references (refx, refy, and refz) to x, y, and z. While creating the weak references, you register them with a new ReferenceQueue. This queue will tell you when the objects are garbage collected by placing the reference object in the queue.

Screenshot

The referent of a Reference object in a Reference queue will always be null because the object has already been garbage collected by the virtual machine before it is placed in the queue.


Finally, you add your static test set and all of the strong objects to the WeakHashSet. This causes the WeakHashSet to create internal weak references to these objects. Since there can be multiple weak references to the same object, this process will work properly. You are finally ready to begin testing the class. Start by making sure that everything is actually in the set:

 // *** validate everything in the WeakHashSet assertEquals(CONTAINS_ALL, true, whs.containsAll(TEST_SET));
 assertEquals(CONTAINS, true, whs.contains(x));
 assertEquals(CONTAINS, true, whs.contains(y));
 assertEquals(CONTAINS, true, whs.contains(z));


Now it's time to delete strong references. Throughout the test, you will use the reference queue (queue), which will tell you when an object is garbage collected. Once you destroy a strong reference to an object, it should vanish from the set and appear in the reference queue. The following code shows how this is done:

 // *** nuke x from the JVM. x = null;
 System.gc( );
 while (queue.remove(1000) != null) {
 }


x is set equal to null. This will break x's path to the root set and leave it connected by weak references only. It will then be garbage collected and will appear in the queue. Use a timeout of 1 second (1,000 milliseconds) to make sure that the garbage collector has a chance to complete its run before you poll the queue. Check the set to make sure that x is actually gone:

 // *** make sure x isn't in the set anymore but all others are.
 assertEquals(CONTAINS_ALL, true, whs.containsAll(TEST_SET));
 assertEquals(CONTAINS, true, whs.contains(y));
 assertEquals(CONTAINS, true, whs.contains(z));
 assertEquals(SIZE, (TEST_SET.size( ) + 2), whs.size( ));


You can be sure that x is gone because y, z, and the TEST_SET are still in the WeakHashSet, and the set is only the size of TEST_SET + 2. You can make this determination because there can be no duplicates in a set, and you know that y and z are still in the set. The rest of the method continues to create objects, dropping them into the set and then removing their strong references and waiting for them to be collected. Here is the creation of another strong reference to y and the destruction of z:

 // *** build a double link, p, to y and nuke z.
 String p = y;
 z = null;
 System.gc( );
 while (queue.remove(1000) != null) {
 }
 // *** make sure x, and z aren't in the set anymore but all others are.
 assertEquals(CONTAINS_ALL, true, whs.containsAll(TEST_SET));
 assertEquals(CONTAINS, true, whs.contains(y));
 assertEquals(SIZE, (TEST_SET.size( ) + 1), whs.size( ));


This test succeeds because there are no more strong references to z. Now remove one of the strong references to y but not the other one. You can assume that y is still in the set.

 // *** nuke y but leave p intact.
 y = null;
 System.gc( );
 while (queue.remove(1000) != null) {
 }
 // *** y should still be in the set as p.
 assertEquals(CONTAINS_ALL, true, whs.containsAll(TEST_SET));
 assertEquals(CONTAINS, true, whs.contains(p));
 assertEquals(SIZE, (TEST_SET.size( ) + 1), whs.size( ));


Finally, null the final strong reference to y and make sure that it is removed from the method, then check the results:

 // *** finally nuke p.
 p = null;
 System.gc( );
 while (queue.remove(1000) != null) {
 }
 // *** y should still be in the set as p.
 assertEquals(CONTAINS_ALL, true, whs.containsAll(TEST_SET));
 assertEquals(SIZE, (TEST_SET.size( )), whs.size( ));
 } catch (Exception ex) {
 LOGGER.error(ex.getMessage( ), ex);
 fail(ex.getMessage( ));
 }
 }


Throughout the test, the TEST_SET elements are never removed because the test class always holds onto them with strong references. However, it has been shown that the WeakHashSet class does manage the references properly.

      
Comments