The package org.bzdev.lang.annotations
Please see- Introduction. This section provides an overview of these annotations.
- An example. This section provides a coding exmaple.
- Background. This section contains background material and an example using the visitor design pattern.
Introduction
Normally Java looks up an object's method at run time but bases the lookup on the method's signature (specifically the types of the method's arguments), which is determined at compile time. While this restriction is desirable for performance reasons, it is sometimes easier to develop software if dynamic methods are supported, in which the argument types are determined at run time. The org.bzdev.lang package supports dynamic methods by using annotations (staring with release 6 of the JDK, annotations are supported by the Java compiler). The mechanism is efficient, but with two restrictions: dynamic methods cannot be static methods and the use of generic types is not supported. The primary reason for not allowing generic types is that type-erasure is not compatible with the dispatch methods provided by "helper" classes.Dynamic methods are supported by 6 annotation types (only 3 or 4 are needed for the simplest cases). The necessary types are
-
DynamicMethod
. This annotation tags a method specified in a class or an interface as a dynamic method, proving the most general type for the methods (which can have return values and throw exceptions as desired). It takes the class name of a "helper" class as its argument (the helper class will be generated automatically). A dynamic method may not be a static method. -
DMethodImpl
. This annotation tags a method as being one that implements a dynamic method for specific argument types, and takes the name of the helper class used in theDynamicMethod
annotation as an argument (the class name serves as a key to match dynamic method implementations with the corresponding dynamic method). These methods may be declared to be static. -
DMethodContext
orDMethodContexts
. Each class that implements a dynamic method needs a "local helper" class for that dynamic method. ADMethodContext
annotates the class that implements a dynamic method and, using the dynamic method's helper class as a key, provides the corresponding local helper class. If a class implements multiple dynamic methods, TheDMethodContexts
method should be used, which takes an array ofDMethodContext
annotations as its argument. A class annotated withDMethodContext
orDMethodContexts
must be declared to be static if the class is an inner class.
The two remaining annotations provide additional control:
-
DMethodOptions
allows one to control the locking used by the helper class, to allow optimizations for single-threaded programs, to adjust cache sizes, and to enable tracing of method searches. The locking-mode values allow one to specify no locking, a mutex lock, a read-write lock, or a system default (normally a mutex but this can be changed at compile time). -
DMethodOrder
is used when dynamic methods are dispatched based on multiple arguments. It controls the search order for arguments and determines which arguments should use compile-time types rather than run-time types.
The implementation of a dynamic method should call its helper's dispatch
method, returning a value of the return type is not void. The first argument
to dispatch
must be this
, followed by the arguments
declared in the method. Each class that implements a dynamic method has
a DMethodContext annotation, specifying a local helper, for that method,
and must call the local helper's register
method in a static
initializer, calling register
with no arguments.
An example
An example for the use of a dynamic method when the dynamic method is specified in a class is the following (note: when referring to a helper in a different package, the fully qualified class name must be used):
public class Foo {
@DynamicMethod("FooIncrHelper")
public Number incr(Number x) throws Exception {
return FooIncrHelper.dispatch(this, x);
}
}
@DMethodContext(helper="FooIncrHelper", localHelper="BarIncrHelper")
public class Bar extends Foo {
@DMethodImpl("FooIncrHelper")
public Number doIncr(Integer x) thows Exception {
return Integer.valueOf(x + 1);
}
}
The call to dispatch will throw a runtime exception named
MethodNotPresentException
if no method that matches the arguments is found. This exception can be
caught to implement the default behavior for the dispatch method and is
located in the org.bzdev.lang package.
If incr
was declared in an interface, an implementation must
appear in the least specific class(es) that implement that interface.
For example:
public interface Incrementer {
@DynamicMethod("FooIncrementerHelper")
public Number incr(Number x) throws Exception;
}
@DMethodContext(helper="FooIncrementerHelper",
localHelper="FooIncrHelper")
public class Foo implements Incrementer {
public Number incr(Number x) {
return FooIncrementerHelper.dispatch(this, x);
}
@DMethodImpl("IncrementerHelper")
public Number doIncr(Integer x) thows Exception {
return Integer.valueOf(x + 1);
}
}
@DMethodContext(helper="IncrementerHelper", localHelper="BarIncrHelper")
public class Bar extends Foo {
@DMethodImpl("IncrementerHelper")
public Number doIncr(Bar x) thows Exception {
return Integer.valueOf(x + 1);
}
}
For compilation please follow the procedures outlined in the BZDev class library description.
Background
Several other approaches have been used to create similar capabilities.
One approach that is used when dynamic methods are not supported
in a language is to use the visitor design pattern. The idea is to define
two interfaces such as Visitor
and Visitable
defined as follows:
interface Visitor {
void visit(Foo obj);
void visit(Bar obj);
}
interface Visitable {
void accept(Visitor arg);
}
then Foo
is defined as follows:
class Foo implements Visitable {
void accept(Visitor arg) {
arg.visit(this);
}
}
class Bar extends Foo {
void accept(Visitor arg) {
arg.visit(this);
}
}
class SomeVisitor implements Visitor {
void visit(Foo obj) {
...
}
void visit(Bar obj) {
...
}
}
For mimicking dynamic methods, the accept
method is
written as given above: it merely calls the visit method with an
argument of this
, whose type matches the type of the
class in which the accept
method is defined. The
accept
method is defined in all subclasses as well
(subclasses of Foo
in this example), so that the type
at run time is known in the call to visit
. When
accept
is called on an object of type Foo, even if
referenced by a variable declared to be Visitable
, the
accept
method that is called is the one defined for
Foo
. The accept
method of
Foo
then calls its argument's visit
method, passing it an argument of type Foo
.
The downside to the use of this design pattern is that every time a
new class that implements
An alternative approach, which generates byte codes at runtime and
loads those into the Java virtual machine, has also been proposed
(Rémi Fora, Etienne Duris, and Gilles Roussel,
"Reflection-based implementation of Java extensions: the
double-dispatch use-case"). This latter approach is far more
efficient (and comparable to the approach described below) but may
fail when a sandbox security model is in use due to restrictions on
generating new classes at run time (e.g., in applets and assuming no
changes to the Java language). Recent versions of Java, however, have
removed the security-manager code.
The
A subclass of Foo that implements the dynamic method Visitable
is defined, one
must add a method to the Visitor
interface and all
Visitor
classes must then implement that new method,
even if there is really nothing for the method to do. In addition,
as mentioned above, an accept method must be defined in all classes
that implement Visitable and their subclasses, which is tedious
when there are a number of subclasses. In addition, the interface
and class definitions are typically in separate files. Attempts to
mitigate these limitations have included the use of the Java
reflection API
(Jens Palsberg and C. Barry Jay, "The Essence of the Visitor
Pattern"); however, the run-time cost is substantial: what would
take 1.17 seconds using the visitor design pattern took 4 minutes,
59.93 seconds using the reflection API, an increase in running time
of about a factor of 300 for trivial methods.
DynamicMethod
annotation and some related annotations
provides a design pattern that addresses some of these issues by
allowing method look-up to be performed more efficiently at run time,
with all the application code created at compile time. A simple
design pattern must be followed. One declares a dynamic method using
a DynamicMethod
annotation: for example,
This annotation declares that a generated class named FooIncrHelper will provide
a method called dispatch that implements a dynamic method. The arguments for
dispatch in the helper class are
public class Foo {
@DynamicMethod("FooIncrHelper")
public Number incr(Number x) throws Exception {
return FooIncrHelper.dispatch(this, x);
}
}
this
to indicate the current
instance, followed by the arguments for the method incr
.
If the DynamicMethod annotation appeared in an interface declaration, then a
class implementing the method and calling the helper's dispatch method
must appear in the first subclass of Object implementing the interface.
incr
will
then use a DMethodImpl annotation to tag a method implementing the dynamic
method for particular argument types (the dynamic method implementing incr
will have a different name). The subclass itself will be annotated with a
DMethodContext annotation, which provides the name of a subclass-specific
helper class (the localHelper attribute) and associates it with the helper
for the dynamic method incr. For example,
If class Foo also provided a method annotated with
@DMethodContext(helper="FooIncrHelper", localHelper="BarIncrHelper")
public class Bar extends Foo {
@DMethodImpl("FooIncrHelper")
public Number doIncr(Integer x) throws Exception {
return Integer.valueOf(x + 1);
}
}
@DMethodImpl("FooIncrHelper")
,
Foo itself would have to have a DMethodContext annotation with its
helper attribute set to FooIncrHelper
and its
localHelper attribute set to some other class name. When multiple
@DMethodContext annotations are needed for a class, the
annotation @DMethodContexts should be used -
@DMethodContexts has a single 'value' defined - an array of
@DMethodContext. The design decision to annotate the
class with the local helper data rather than the individual method
was due in part for supporting the class loader
DMClassLoader
, which removes the
need for the static initializers that call register() methods of
the local helper classes.
The look-up can be based on more than one
argument, and the default behavior is to do nothing, which
eliminates the coding required when using the visitor design
pattern when a change to the Visitor
interface is
made. The order in which argument types are searched can be controlled
with a DMethodOrder annotation, applied to method definition or
declaration annotated by the DynamicMethod annotation.
In addition, One may use a throws
clause in the method
definitions and declarations.
For the single argument case, the overhead over a trivial implementation
(a couple of method calls, and an if
statement using the
instanceof
operator) is roughly a factor of 15, which can
be reduced to a factor of 10 by setting an option to turn off locking
for cases in which objects using a specific class are accessed by only
one thread at a time. This assumes the method called performs a trivial
operation - the overhead will not be noticeable if the method called
does anything substantial. The implementation uses "helper"
classes to dispatch method calls at run-time. These classes are
automatically generated by the java compiler via an annotation processor.
For the two-argument case, the overhead is approximately factor of 34
with locking and 29 without locking (in both cases, the running time is
higher than in the single-argument case).
The equivalent of the visitor design pattern example above can be implemented using dynamic methods. For example:
interface Visitor {
@DynamicMethod("VisitorHelper")
void visit(Object arg);
}
@DMethodContext(helper = "VistorHelper",
localHelper = "SomeVisitorHelper")
class SomeVisitor implements Visitor {
void visit(Object arg) {
VisitorHelper.getHelper().dispatch(this, arg);
}
@DMethodImpl("VisitorHelper")
void doVisit1(Foo arg) {
...
}
@DMethodImpl("VisitorHelper")
void doVisit2(Bar arg) {
...
}
}
The Visitable
interface is not needed, and explicit
methods can be added only for those argument classes (Foo
and Bar
in this example) for which some action is needed.
The Visitor
interface does not have to be modified, and
the visit
method is not defined in subclasses of
SomeVisitor
. The method doVisit(Foo)
does not have to be defined in subclasses of SomeVisitor
unless it has to be coded differently in the subclass.
If a default action is needed, one can add a declaration such as
@DMethodImpl("VisitorHelper")
void doVisit(Object arg) {
return; // do nothing.
}
that will match any object passed as an argument. Alternatively, one
may change the implementation of the visit
method to
catch an exception that indicates that the dispatch mechanism could
not find a method to invoke:
void visit(Object arg) {
try {
VisitorHelper.getHelper().dispatch(this, arg);
} catch (MethodNotPresentException e) {
}
}
It is better in this case to define the doVisit
method
with an argument of type Object
, as one of the
doVisit
methods may call a different dynamic method
that throws MethodNotPresentException
due to a
programming error, in which case the partial execution of some
methods may put the application into an illegal state.
The example above
assumes the use of a dynamic-method-aware class loader such as
org.bzdev.lang.DMClassLoader
. If
such a class loader is not used, then a static initializer has to be
added to the SomeVisitor class:
@DMethodContext(helper = "VistorHelper",
localHelper = "SomeVisitorHelper")
class SomeVisitor implements Visitor {
static {
SomeVisitorHelper.register();
}
...
}
The call to the register() simply forces the SomeVisitorHelper class
to be initialized: during initialization, SomeVisitorHelper will
register itself with VisitorHelper to create tables that allow the
correct method to be looked up at run time. The general rule is that
whenever a class is annotated with a @DMethodContext or
@DMethodContexts annotation, for each localHelper attribute
provided, there must be call to each helper's static method register()
in the class's initializer.
In the example above, if all classes for which visit
can be called are subclasses of SomeVisitor
, then
the interface definition is not needed and the code can be written
as follows:
@DMethodContext(helper = "VistorHelper",
localHelper = "SomeVisitorHelper")
class SomeVisitor {
@DynamicMethod("VisitorHelper")
void visit(Object arg) {
VisitorHelper.getHelper().dispatch(this, arg);
}
@DMethodImpl("VisitorHelper")
void doVisit(Foo arg) {
...
}
@DMethodImpl("VisitorHelper")
void doVisit(Bar arg) {
...
}
}
Subclasses can implement doVisit
methods, annotated
with @DMethodImpl("VisitorHelper")
, and
subclasses that implement this method must of course have a
@DMethodContext
annotation for the subclass with
a unique localHelper
class name. For a similar example
where a register() is called explicitly, one might define the
interface (the complete file is shown)
package dmtest;
import org.bzdev.lang.annotations.*;
public interface Visitor {
@DynamicMethod("VisitorHelper")
void visit(Object arg);
}
package dmtest.a;
import org.bzdev.lang.annotations.*;
@DMethodContext(helper="VisitorHelper", localHelper="SomeVisitorHelper")
public class SomeVisitor implements Visitor {
static {
SomeVisitorHelper.register();
}
public void visit(Object arg) {
VisitorHelper.getHelper().dispatch(this, arg);
}
@DMethodImpl("VisitorHelper")
void doVisit1(Double arg) {
System.out.println("double = " + arg.toString());
}
@DMethodImpl("VisitorHelper")
void doVisit2(Integer arg) {
System.out.println("integer = " + arg.toString());
}
public static void main(String argv[]) {
Object obj1 = Double.valueOf(10.0);
Object obj2 = Integer.valueOf(20);
SomeVisitor sv = new SomeVisitor();
sv.visit(obj1);
sv.visit(obj2);
}
}
The output when the program is run is
double = 10.0
integer = 20
By contrast, the following program will fail at compile time:
package dmtest.a;
import org.bzdev.lang.annotations.*;
public class SomeVisitor2 {
public void visit(Double arg) {
System.out.println("double = " + arg.toString());
}
public void visit(Integer arg) {
System.out.println("integer = " + arg.toString());
}
public static void main(String argv[]) {
Object obj1 = Double.valueOf(10.0);
Object obj2 = Integer.valueOf(20);
SomeVisitor sv = new SomeVisitor();
sv.visit(obj1);
sv.visit(obj2);
}
}
Without dynamic methods, one would have to add a method such as
public void visit(Object arg) {
if (arg instanceof Double) visit((Double) arg);
else if (arg instanceof Integer) visit((Integer) arg);
}
which is error prone due to the need to make a change in multiple
places.