// WeakProxyReference.java
import java.lang.ref.*;
import java.lang.reflect.*;
//These imports are just for the test code used by the main method
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
/**
* DESCRIPTION<p>
* This class contains a static <code>create</code> method which creates a
* proxy for a given object and adds it to a given object to a given container
* in such a way that all the methods invoked on that proxy by the container
* will be forwarded to the original object.
* It should work exactly as if the object had been added directly to the
* container but with one important difference:
* No strong reference to the object will be created in the process. This
* allows the object <i>and any resources that object references</i> to be
* garbage collected when no other parts of your application reference it.
* This helps eliminate one of the most common sources of memory leaks usually
* involving listener objects that keep their referenced windows, etc. from
* being garbage collected <i>without requiring you to figure out when to
* remove those listeners yourself</i>. In other words, it makes it possible
* to "set and forget" your listeners.<p>
*
* The only caveat is that you will need to make sure that there is at least
* one strong reference to your listener objects in your application,
* otherwise they will now be immediately garbage collectable.
* That's because you can no longer count on the container holding a strong
* reference to your listener. The cleanest solution is to make the thing being
* referenced by the listener (rather than the container which would have held
* the listener) keep any sort of strong reference to that listener.
* This sort of circular reference will keep both resource and listener alive
* until no other parts of your application hold references to the resource.<p>
*
* EXAMPLE
* <pre><code>
* final JPanel my_panel = new JPanel(); // the expensive resource
* ActionListener my_listener = new ActionListener() {
* public void ActionPerformed(ActionEvent ae) {
* ...code that directly affects "my_panel"...};
* }
* });
* JButton my_button = new JButton("Panel Controller");
* // use the following instead of my_button.addActionListener(my_listener);
* WeakProxyReference.create(my_button, my_listener, ActionListener.class);
* my_panel.putClientProperty(new Object(), my_listener); // circular reference
* </code></pre>
*
* @author Melinda Green - Superliminal Software
*/
public class WeakProxyReference {
private WeakReference reference_to_held; // weak reference to held object
private Object proxyForHeld;
private final static boolean DEBUG = true;
/**
* Creates a weak proxy reference between two objects.
*
* @param holder is the object to which a weak proxy reference
* to the second parameter is to be added.
*
* @param held is the object which holder is supposed to reference.
*
* @param heldBaseClass is the Class object for the <i>base class</i>
* of the held object.
* IOW, the class of object the holder is expecting to recieve.
* For example, if you want to add your own derived action listener
* to a button, you must pass ActionListener.class,
* <i>not</i> MyActionListener.class.
*
* @param addMethodName is the name of holder's method used to
* add objects of type <code>heldBaseClass</code>.
*
* @param removeMethodName is the name of holder's method used to
* remove objects of type <code>heldBaseClass</code>.
*
* <br><br><b>NOTE:</b> Because the object which you pass as the "held"
* parameter will not be strongly held by the holder, you will need to make
* sure you keep at least one reference to it.
* The most natural place to do that is to store that reference is in a
* private member variable of whatever object it operates on. That sort
* of circular reference is good because both objects will be garbage
* collected together when there are no external references to either one.
* Or more prosaically, this makes them bound by a mutual suicide pact.
*/
public static void create(
Object holder,
Object held,
Class heldBaseClass,
String addMethodName,
String removeMethodName)
{
new WeakProxyReference(holder, held, heldBaseClass,
addMethodName, removeMethodName);
}
/**
* Shorthand version of <code>create</code> method which assumes
* the add and remove method names are simply the heldBaseClass class name
* with the strings "add" and "remove" prepended. For example, since the
* JButton class has "addActionListener" and "removeActionListener"
* methods, you can create a WeakProxyReference from a JButton to an
* instance of your custom action listener like this: <br><code>
* WeakProxyReference.create(myButton, myListener, ActionListener.class);
* </code><br>
* Be sure to see "NOTE" in the 5 argument version of this method.
*/
public static void create(Object holder, Object held, Class heldBaseClass) {
String shortBaseName = heldBaseClass.getName().substring(
heldBaseClass.getName().lastIndexOf('.')+1);
create(holder,
held,
heldBaseClass,
"add" + shortBaseName,
"remove" + shortBaseName);
if(DEBUG)
System.out.println("Created a WeakProxyReference to a " +
shortBaseName);
}
/**
* Constructor is private since there's nothing useful the user can do with
* an object of this type.
*/
private WeakProxyReference(
final Object holder,
Object held,
final Class heldBaseClass,
String addMethodName,
final String removeMethodName)
{
reference_to_held = new WeakReference(held);
//Define the call-through behavior used by the proxy object we're about
//to create which dispatches all method calls to the original target
//object if it still exists, otherwise remove the unneeded proxy.
InvocationHandler handler = new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) {
String meth_name = method.getName();
if(DEBUG)
System.out.println("invocation handler invoked for "+
method.getName());
Object original_held_object = reference_to_held.get();
if(original_held_object == null) {
//The equals() method is a special case
//because container remove methods called below use equals
//to find the element to remove, and because the object in
//the collection is really a proxy object and not the
//original held object. Normally we'd dispatch even the
//equals method but since the held object is already gone,
//we need to make sure the container remove methods still
//work.
if(meth_name.equals("equals") && proxy == proxyForHeld)
return new Boolean(args[0] instanceof Proxy);
//Because the weak reference object returned null, we know
//the original held object is now only weakly reachable
//and is therefore garbage-collectable or even already
//garbage collected. Just to be clean we take this
//opportunity to remove the little proxy object from the
//holder since its no longer useful and can also be garbage
//collected.
Method remover = getMethodFor(holder,
removeMethodName, new Class[] { heldBaseClass });
if(DEBUG) System.out.println("\tremoving stale proxy");
doInvoke(remover, holder, new Object[] { proxyForHeld });
if(DEBUG) System.out.println("\tdone removing stale proxy");
return null; // nobody to dispatch to
}
Method held_object_method = getMethodFor( original_held_object,
meth_name, method.getParameterTypes());
return doInvoke(held_object_method, original_held_object, args);
}
};
//Create the proxy
proxyForHeld = Proxy.newProxyInstance(
held.getClass().getClassLoader(),
new Class[] { heldBaseClass },
handler);
//Add the proxy to the holder by looking up its appropriate "add"
//method and invoking it.
Method adder = getMethodFor(holder, addMethodName,
new Class[] { heldBaseClass });
doInvoke(adder, holder, new Object[] { proxyForHeld });
}
/**
* Helper function for invoking a given method on a given object
* with exceptions caught and dealt with.
*/
private static Object doInvoke(Method method, Object obj, Object[] args) {
try { return method.invoke(obj, args); }
catch (IllegalAccessException e) { printException(e); }
catch (InvocationTargetException e) { printException(e); }
catch (NullPointerException e) { printException(e); }
return null;
}
/**
* Helper function for getting a given method of a given object
* with exceptions caught and dealt with.
*/
private Method getMethodFor(Object obj, String method_name,
Class parameterTypes[])
{
try { return obj.getClass().getMethod(method_name, parameterTypes); }
catch (NoSuchMethodException e) { printException(e); }
return null;
}
private static void printException(Exception e) {
System.err.println("WeakListener exception " + e);
}
/**
* A test method for WeakProxyReference.
* Call with no arguments to run a simple example,
* and with any extra argument to run a more complex example.
*/
private static void main(String args[]) {
if(args.length == 0)
twoWindowTest();
else
buttonSelectionTest();
}
private static void twoWindowTest() {
// Make a window with a button that will control another window.
JButton controller_button = new JButton(
"Toggle other window's background");
JFrame control_frame = new JFrame("Control Window");
control_frame.getContentPane().add(controller_button);
control_frame.setLocation(new Point(100, 100));
control_frame.pack();
control_frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// Create the window to be controlled by the first one.
final JFrame controlled_window = new JFrame(
"Resource which needs to be garbage collectable");
final JTextArea instructions = new JTextArea(
"This window represents an expensive resource you want to be "+
"garbage collectible after it's closed. Note that the JFrame is "+
"set up to 'dospose on close', so clicking the X in the upper "+
"right hand corner should do it. But first, click the toggle "+
"button in the other window a few times to see that it has a "+
"button listener which references this window. Then close this "+
"window. To see the console message that the resource has indeed "+
"been garbage collected depends upon when, if ever, your VM does "+
"garbage collection. You will at least need to compile this code "+
"with the DEBUG flag set to true to generate the print statements. "+
"With the JDK 1.3 Windows VM I found that the garbage collector "+
"will kick in if after I've closed this window, I maximize the "+
"control window and then restore it to normal. When I then click "+
"the toggle one more time I get the message showing that the "+
"garbage collecter has done it's thing.");
instructions.setLineWrap(true);
instructions.setWrapStyleWord(true);
controlled_window.getContentPane().add(instructions);
controlled_window.setSize(new Dimension(450, 250));
controlled_window.setLocation(new Point(400, 100));
controlled_window.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
// Create the controler which will affect the controled window.
ActionListener toggler = new ActionListener() {
boolean yellow = false; // the current background color
public void actionPerformed(ActionEvent ae) {
System.out.println("colored window being toggled");
yellow = !yellow;
instructions.setBackground(yellow ? Color.yellow : Color.white);
instructions.revalidate();
}
};
//Rather than calling
//controller_button.addActionListener(controlled_window.getController())
//like we normally would, here's the magic call to set up
//the weak proxy reference instead:
WeakProxyReference.create(controller_button, toggler,
ActionListener.class);
// Unfortunately replacing one call with another is not quite enough.
// Here's the important line that circumvents the one drawback of
// replacing a strong reference to the listener with a weak one.
// We create a strong reference from the CONTROLLED object to the
// listener that controls it. IOW a circular reference chain that's
// garbage collectible when there are no external references left to
// either of them which is the case after the controlled window is
// closed. After that, both the controlled window and the control
// listener are subject to garbage collection.
// Note that you don't need to use putClientProperty to do this.
// ANY call that would result in a strong reference to the listener
// from any object that's only referenced within the controlled
// resource will do. For the common case of AWT component listeners,
// the putClientProperty method is particularly useful as it provides
// a way to associate a client object with *any* component. In this case
// the toggler listener is associated with the instructions object
// it affects. The object passed as the first argument is completely
// arbitrary since any unreferenced object will do.
instructions.putClientProperty(new Object(), toggler);
// It's showtime folks!
control_frame.setVisible(true);
controlled_window.setVisible(true);
}
/**
* A somewhat more complex example. In this case the resource and the
* controller live in the same panel. The "remove" button allows you to
* remove the resource--in this case a colored label--which then becomes
* garbage collectible.
*
* The main functional difference between this example and the previous
* one is in the way the listener is kept alive by the thing it's affecting.
* In the previous example the hard reference was created by explicitly
* adding the listener as a client property of an object within the window
* it affects. In this case that reference is implicit in the fact that
* the listener lives as a member variable within the class it affects.
* Either way works fine. Just choose from these or other ways of making
* the association you need depending upon what's most natural in each case
* where you want to use a WeakProxyReference.
*/
private static void buttonSelectionTest() {
// here's an inner class that represents a JLabel with toggleable color.
// the toggle() method switches between two different background colors.
class ColoredLabel extends JLabel {
private boolean green = true; // current background color
// NOTE: The line below creates and stores the action listener
// inside the ColoredLabel class. The fact that it is stored here
// is what keeps the listener from being garbage collected too soon.
// It's essentially a hard reference to that listener that won't
// be garbage collected until its containing ColoredLabel instance
// is also collectible. That happens as soon as the label is removed
// from it's containing panel by clicking the "remover" button.
private ActionListener toggler = new ActionListener() {
public void actionPerformed(ActionEvent ae) {
toggle();
}
};
public ActionListener getController() {
return toggler;
}
private ColoredLabel(String title) {
super(title);
setFont(new Font("Courier", Font.BOLD, 30));
toggle(); // to set original background color
}
private void toggle() {
System.out.println("colored window being toggled");
green = !green;
setForeground(green ? Color.green : Color.blue);
repaint();
System.gc();
}
}
final ColoredLabel text = new ColoredLabel("Resource");
JButton control_button = new JButton("Toggle text field");
JFrame control_frame = new JFrame("WeakProxyReference Test");
final JButton remover = new JButton("Remove Resources");
final JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
remover.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent ae) {
panel.remove(text);
panel.remove(remover);
//text label and remover button are now garbage collectable
panel.repaint();
}
});
control_frame.getContentPane().add(panel);
control_button.setAlignmentX(0.5f);
panel.add(control_button);
text.setAlignmentX(0.5f);
panel.add(text);
remover.setAlignmentX(0.5f);
panel.add(remover);
JTextArea instructions = new JTextArea(
"The toggle button controls the color of the 'Resource' label. "+
"That label is acting as some expensive resource that you don't "+
"want kept alive just because by the control button has a "+
"listener which affects it. The 'remove' button removes both "+
"the resource label and the remove button itself, both of which "+
"then become garbage collectable. "+
"To see the console message that the resource has indeed been "+
"garbage collected, you need to compile this code with the DEBUG "+
"flag set to 'true' and you may need to maximize the window, "+
"restore it to normal, and hit the toggle button once more "+
"before you see that the garbage collecter has done it's thing.");
instructions.setLineWrap(true);
instructions.setWrapStyleWord(true);
panel.add(instructions);
control_frame.setLocation(new Point(100, 100));
control_frame.setSize(new Dimension(300, 300));
control_frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//Rather than calling
//control_button.addActionListener(text.getController())
//like we normally would, here's the magic call to set up
//the weak proxy reference to the button listener
WeakProxyReference.create(control_button, text.getController(),
ActionListener.class);
control_frame.setVisible(true);
}
}