A Java Programmer Looks at C# Delegates
Implementing Delegates in Java
The code presented in this article implements a significant portion of the
functionality of C# delegates in Java. Two ways of accomplishing this will
be presented. In the first case, the developer describes the method to be
delegated by providing a list of the parameter and return classes. In the
second case, the parameters are deduced by examining a suitable interface that
declares a single method. The code presents a factory class,
Delegator, capable of handling either case. The factory method,
build, returns an object implementing the Delegate interface.
Where the Delegator has been constructed with an interface, the
return is a Proxy implementing that interface.
The Delegator Class
Two methods may be considered comparable if the argument lists of each
method are assignable the same common list of classes and if the return types
are assignable to a common type. As an example, if Foo is a
superclass of Bar, the two methods
public String m1(Foo p1);
public Object m2(Bar p1);
both match a signature taking Bar and returning
Object. We may express this signature in code by providing a
Class object that represents the return type and an array of
Class to represent the parameter types. We may also express this
signature by providing an interface with a single method to be used as an
exemplar.
A method may match the signature described by a Delegator in
the weak sense that all arguments are assignable to the declared types rather
than the stronger test required by a Java interface that the arguments be
identical. Also note that methods are considered comparable even it they throw
different exceptions. Delegation will convert all exceptions into a runtime
DelegateInvokeException emulating C# behavior (all C#
exceptions act like RuntimeExceptions).
Delegator provides a factory method, build, which
associates the method template with a specific implementation. The
implementation is a combination of either an instance method and a target
instance or a static method. In either case, the method must be compatible with
the requested signature. The object returned by the build method
will satisfy the Delegate interface, which contains the
method:
public Object invoke(Object[] args);
The returned object is a thin wrapper that invokes the method on the
supplied object, converting any checked exceptions returned by the wrapped
object into a runtime DelegateInvokeException. The code in
Scenario 1 shows use of this basic type of Delegate object.
Scenario 1. Using a generic Delegate object
Java Code
class Class1 {
public void show(String s) { System.out.println(s); }
}
class Class2 {
public void display(String s) { System.out.println(s); }
}
// allows static method as well
class Class3 {
public static void staticDisplay(String s) { System.out.println(s); }
}
public class TestDelegate {
public static final Class[] OUTPUT_ARGS = { String.class };
public final Delegator DO_SHOW = new Delegator(OUTPUT_ARGS,Void.TYPE);
public void main(String[] args) {
Delegate[] items = new Delegate[3];
items[0] = DO_SHOW .build(new Class1(),"show,);
items[1] = DO_SHOW.build (new Class2(),"display");
items[2] = DO_SHOW.build(Class3.class, "staticDisplay");
for(int i = 0; i < items.length; i++) {
items[i].invoke("Hello World");
}
}
}
The code described above offers many of the advantages of C# delegates.
Methods, either static or dynamic, can be treated in a uniform manner. The
complexity in calling methods through reflection is reduced and the code is
reusable, in the sense of requiring no additional classes in the user code.
Note we are calling an alternate convenience version of invoke,
where a method with one parameter can be called without creating an object
array.
Scenario 2. Delegating via an interface
One advantage of C# delegates that is still missing is static type
checking enforced by the compiler. In the example above, it is possible to
call invoke on a returned Delegate with a
Date object, and the error will not be discovered until run time.
In order to get the full benefits of compiler support, it is necessary to
construct the Delegator with an interface. The interface may be
well-known or special-purpose, but should declare a single method. The signature
of that method becomes the template used by the Delegator. In
addition, the returned object will be a proxy that implements the requested
interface.
Java Code
// interface to implement
public static interface IStringDisplay {
public void doDisplay(String s);
}
public final Delegator ST_DEL = new Delegator(IStringDisplay.class);
public void testDelegate()
{
IStringDisplay[] items = new IStringDisplay[3];
// build the delegates
items[0] = (IStringDisplay) ST_DEL.build(new Class1(),"show");
items[1] = (IStringDisplay) ST_DEL.build(new Class1()2,"display");
items[2] = (IStringDisplay) ST_DEL.build(Class3.class,"staticDisplay");
// call the delegates
for(int i = 0; i < items.length; i++) {
items[i].doDisplay("test");
}
}
Note that while a cast is required to convert the value returned from the
build method into an instance of the desired interface,
invocations of the delegated method are now made through the interface with
full static type checking.
Thread Delegates
One major use of delegates in C# is in threading. Rather than constructing a
thread with an instance of Runnable, threads in C# are constructed
by using a delegate, Thread.ThreadStart, with the appropriate
signature. In Java, a similar problem exists where a developer wants to call a
no-argument method as the active portion of a Runnable. While this
may be accomplished with an anonymous inner class, the construct is clumsy and
reduces the readability of the code.
This important usage pattern is supported by the Delegator
class, which implements convenience methods to create delegates implementing
Runnable. This is done by providing a static, final instance
variable holding a Delegator constructed using the well-known
interface Runnable, implementing two static methods that build
delegates using this Delegator and cast the returned object to the
underlying interface. Similar code could be used any time it is necessary to
build many delegates that all implement a particular interface.
Java Code
static final Delegator RUNNABLE_DELEGATE = new Delegator(Runnable.class);
public static Runnable buildRunnable(Object o,String methodName) {
return((Runnable)RUNNABLE_DELEGATE.build(o,methodName));
}
public static Runnable buildRunnable(Class c,String methodName) {
return((Runnable)RUNNABLE_DELEGATE.build(c,methodName));
}
The above code can be called with a line such as:
Runnable r = Delegator. buildRunnable(this,methodName);
Note that because a special-purpose method has been used, there is no need to
cast the return value from buildRunnable. The cast is performed
in the method implementation.
How It Works
The critical code is in the method build. This method
constructs a DelegateProxy, exposed through an
Delegate interface that is a wrapper around the method named in
the call to build. If the Delegator was constructed
by specifying an interface, the returned object is wrapped in a dynamic
Proxy so that it will appear to the Java runtime as an instance of
the requested interface.
Java Code
/**
* @param target non-null target with a bindable method
* @param MethodName name of the method
* @return non-null IDelegate. If getInterface() is non-null the returned
* Delegate will be a dynamic proxy implementing that interface
*/
public Delegate build(Object target,String methodName)
{
Class myInterface = getInterface();
DelegateProxy theDelegate = new DelegateProxy(target,methodName,this);
// build a dynamic proxy
if(myInterface != null) {
Class[] interfaces = { myInterface,Delegate.class };
Delegate ret = (Delegate)java.lang.reflect.Proxy.newProxyInstance(
target.getClass().getClassLoader(),
interfaces, theDelegate);
return(ret);
}
return theDelegate;
}
The constructor DelegateProxy(target,methodName,this) uses
reflection to find the best method in the target class matching the signature
contained in the Delegator. When an interface has been specified,
the DelegateProxy can be used as an InvocationHandler
to construct a Proxy implementing the specified interface. The
returned object may then be called using the Delegate's
invoke method or, if an interface is specified in the
Delegator, cast to that interface and used as an instance of that
interface.
Usage
Sufficient information to build delegates is present once classes are
loaded. Binding a delegate is a non-trivial operation requiring identification
of an appropriate method in the target class. When a delegate is constructed
with an instance method, the build call can occur any time after
the target instance has been created. Delegates invoking static methods can be
constructed at load time. It is usually a good idea to build delegatees as
early as possible, caching them for later use. Actually performing method
calls through delegates is relatively cheap.
Timing and Performance Costs
The most logical use of delegates involves messaging and event handling
where the code is infrequently executed; i.e., not in a tight loop. In these
situations, executing the code contained in the target method usually takes
significantly longer than the process of finding and invoking the method.
In addition, Hotspot and JDK 1.4 have significantly reduced the cost of method
calls. I found that executing the loop in TestDelegate (three calls)
for 10,000,000 iterations took 43 seconds on a 1.5GHz Athlon processor running
JDK 1.4 under Windows 2000. This averages slightly more than one microsecond per
call. This cost can be ignored in all but the tightest loops.
Discussion
This approach represents an implementation of the Adapter pattern (Gamma et
al, Design Patterns). It differs from a Proxy in that an Adapter maps
a number of different methods into identical calls, allowing multiple objects
implementing methods with different names but compatible signatures to be used
interchangeably. It will also work with or without a target interface to
implement. The Delegator class simplifies and generalizes the
steps in creating an Adapter. Delegates are simple to use, and a single
delegate instance may be reused multiple times.
Where an interface is known or can be constructed, use of delegates provides
a simple, readable way to coerce a method into implementing the actions in that
interface. Common interfaces, such as Runnable and many event
listeners, may easily be mapped to any matching public method. In these cases,
the Java code is almost as simple as the code that could be written using C#'s
built-in delegate construct.
In my own development, I find that delegates implementing interfaces are more
useful than those using invoke. The buildRunnable
method is especially useful. In Swing programming, where large numbers of
Runnables are needed to pass control to the swing thread, the
ability to turn methods into Runnables is particularly useful.
Delegates allow me to largely eliminate the need for anonymous inner classes,
improving the readability of my code.
References
Design Patterns: Elements of Reusable Object-Oriented Software by
Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. Addison Wesley
Professional Computing Series, 1994.
Steven M. Lewis
, PhD, is a Director of Development for UnifiedSignal, a provider of telecom
solutions.
Wilhelm Fitzpatrick
is an independent
consultant specializing in Java development for enterprise platforms.
Return to ONJava.com.