The JavaTM Language
Team
JavaSoft, Sun Microsystems, Inc.
CONTENTS
Introduction
Inner Classes vs. Method References
Adapter Objects and Delegation
- Example: A clickable button
- Example: Sorting an array
- What about multicast?
Performance Issues
- Sidebar:Implementing delegates using
reflection
- Application footprint
Conclusion
Introduction
The newest version of the Microsoft Visual J++ development
environment supports a language construct called delegates or
bound method references. This construct, and the new keywords
delegate and multicast introduced to support it,
are not a part of the JavaTM programming
language, which is specified by the
Java Language
Specification and amended by the
Inner
Classes Specification included in the
documentation for
the
JDKTM 1.1 software.
It is unlikely that the Java programming language will ever
include this construct. Sun already carefully considered
adopting it in 1996, to the extent of building and discarding working
prototypes. Our conclusion was that bound method references are
unnecessary and detrimental to the language.
This decision was made in consultation with Borland International,
who had previous experience with bound method references in Delphi Object
Pascal.
We believe bound method references are unnecessary because another
design alternative, inner classes, provides equal or superior
functionality. In particular, inner classes fully
support the requirements of user-interface event handling, and have
been used to implement a user-interface API at least as comprehensive
as the Windows Foundation Classes.
We believe bound method references are harmful because they detract
from the simplicity of the Java programming language and the
pervasively object-oriented character of the APIs. Bound method
references also introduce irregularity into the language syntax and
scoping rules. Finally, they dilute the investment in VM technologies
because VMs are required to handle additional and disparate types of
references and method linkage efficiently.
Inner Classes vs. Method References
It has been clear from the outset that the original language needed
some equivalent of method pointers in order to support delegation and
"pluggable" APIs. James Gosling noted this in his keynote at the
first JavaOneSM conference. The key
design question at the time was whether we needed a specialized
function pointer construct, or whether there was some new way to use
classes to meet the requirements.
In the summer of 1996, Sun was designing the precursor to
what is now the event model of the AWT and the JavaBeansTM component architecture. Borland contributed
greatly to this process. We looked very carefully at Delphi Object
Pascal and built a working prototype of bound method references in
order to understand their interaction with the Java programming
language and its APIs.
Working through the implementation of bound method references
showed us how foreign they are to a pure object-oriented model. We
discovered the following limitations:
- Bound method references add complexity. The type system
must be extended with an entirely new kind of type, the bound method
reference. New language rules are then required for matching
expressions of the form `
x.f' to method reference
constructors, most notably for overloaded or static methods.
- They result in loss of object orientation. Because of
their special role and supporting syntax, bound method references are
"second-class citizens" among other reference types. For example, while
delegate types in Visual J++ are technically classes, they cannot extend
classes chosen by the programmer, nor implement arbitrary interfaces.
- They are limited in expressiveness. Although bound method
references appear to the Java VM as reference types, they are no more
expressive than function pointers.
For example, they cannot implement groups of operations or contain
state, and so cannot be used with Java class library iteration interfaces.
We wanted the Java language to be
class-based, in which classes do
all the work, rather than supplying
function pointers as an alternate and redundant way to perform the same
delegation tasks.
- They are no more convenient than adapter objects. Although
our analysis indicated that some code examples could be expressed more
concisely with a specialized method reference syntax, we also found that
the additional notational overhead for inner classes
was never more than a handful of tokens.
Although a native code compiler like Delphi Object Pascal can
implement bound method reference calls with a couple of instructions,
there is no comparably efficient way to implement them on an
unmodified Java VM. Our prototype therefore used compiler-generated
adapter classes. This solution was fast, but pointed to an even
better solution -- improving support for adapter objects of all kinds.
With these results in hand, the designers of the Java programming
language decided to introduce inner classes. Inner classes,
especially anonymous inner classes, make it possible
to create and use adapter objects as easily as bound method references,
while retaining all of the benefits of a uniformly class-based solution.
Adapter Objects and Delegation
We have argued above that adapter objects, using inner classes, are a
better solution than bound method references. Let's now take a look
at adapter objects in action. We will examine two cases from the Java
platform APIs, contrasting the solution we adopted with an alternative
based on method references.
Example: A clickable button
Programs that use delegation patterns are easily expressed with inner
classes. In the AWT event model, user-interface components send event
notifications by invoking methods of one or more delegate objects that
have registered themselves with the component. A button is a very
simple GUI component that signals a single event when pressed. Here
is an example using the JButton class from the Swing toolkit:
public class SimpleExample extends JPanel {
JButton button = new JButton("Hello, world");
private class ButtonClick
implements ActionListener {
public void actionPerformed(ActionEvent e) {
System.out.println("Hello, world!");
}
}
...
public SimpleExample() {
button.addActionListener(new ButtonClick());
add(button);
...
}
...
}
When the user clicks on the button, it invokes the
actionPerformed method of its delegate, which can be of
any class that implements the ActionListener interface.
In a language that uses bound method references, the corresponding
code would be quite similar. Again, a button delegates a click to an
action method. In this case, the delegate is an enclosing object.
Keeping the identifiers the same to make the remaining differences
stand out clearly:
public class SimpleExample extends JPanel {
JButton button = new JButton("Hello, world");
private void button_clicked(ActionEvent e) {
System.out.println("Hello, world!");
}
...
public SimpleExample() {
button.addActionListener(
new ActionDelegate(this.button_clicked));
add(button);
...
}
...
}
The key conclusion is quite simple: Any use of a bound method
reference to a private method can be transformed, in a simple and
purely local way, into an equivalent program using a one-member
private class. Unlike the original, the transformed program will
compile under any JDK 1.1-compliant Java compiler.
Method references require the programmer to flatten the code of the
application into a single class and choose a new identifier for each
action method, whereas the use of adapter objects requires the
programmer to nest the action method in a subsidiary class and choose
a new identifier for the class name. In terms of program complexity, the
difference is four extra tokens and some indentation for the
adapter-object pattern -- a negligible difference.
It is sometimes more convenient for the programmer to use an
anonymous inner class. Anonymous classes make the code using an
adapter object as short as the code using a method reference. Though
it requires the same number of tokens as a method reference,
an anonymous adapter object avoids
the need for an extra identifier by nesting the action method directly
within the expression that attaches it to the button:
public class SimpleExample extends JPanel {
JButton button = new JButton("Hello, world");
...
public SimpleExample() {
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("Hello, world!");
}
});
add(button);
...
}
...
}
Note that this style of event configuration puts the button event method
as close as possible to the initialization of the button. By contrast,
when named classes or methods are used, the event method can end up far
away from the initialization, because it must be placed in the enclosing
class, and must be mixed with other event methods for the same panel.
With either kind of delegate, whether an adapter object or a method
reference, the programmer is free to ignore the separate identity of
the delegate object and regard it as an integral part of the panel
class, i.e., SimpleExample. The action method is free in
either case to access private members of the panel class, because it
is in all cases nested inside the implementation of that class.
Example: Sorting an array
Adapter objects also find natural uses in non-GUI APIs. For example,
the JDK 1.2 class libraries provide a routine for sorting an array of objects.
The
routine takes an argument that lets the programmer specify how to
compare the array elements. This argument, called a comparator,
is an object with a method that is invoked by the sort routine to compare
pairs of array elements. It is of the following type:
public interface Comparator {
public int compare(Object o1, Object o2);
}
As an interface, Comparator can have any implementation
whatever. It is often most conveniently a local class
or even an anonymous one, as in this example
where an array of strings is sorted in a case-insensitive manner:
void sortIgnoreCase(String words[]) {
Arrays.sort(words, new Comparator() {
public int compare(Object o1, Object o2) {
String s1 = ((String)o1).toLowerCase();
String s2 = ((String)o2).toLowerCase();
return s1.compareTo(s2);
}
});
...
}
Since we used an anonymous class, the comparison method is placed within
the call to the sort routine, where it is used just once.
If the delegate were created with a bound
method reference, the sort routine would have to be given a unique name
and placed in the body of the enclosing class, possibly far from
its actual point of use:
private int compareIgnoreCase(Object o1, Object o2) {
String s1 = ((String)o1).toLowerCase();
String s2 = ((String)o2).toLowerCase();
return s1.compareTo(s2);
}
...
void sortIgnoreCase(String words[]) {
Arrays.sort(words,
new Comparator(this.compareIgnoreCase));
...
}
Note also that the bound method reference in this case includes a
spurious reference to the enclosing class, which is not actually used
by the algorithm. If the delegate outlives the
enclosing object this feature can cause unexpected storage retention.
More generally, it is poor program organization to place this method
in the enclosing class because it does not operate on the class. All
these problems stem from the relative inflexibility of bound method
references as compared to inner classes.
Storage retention is not a problem with the local adapter object.
Since the object does not actually use all the values visible to it
(like this or words), the JDK 1.1 compiler does
not incorporate them into the object as unnecessary references.
What about multicast?
The Visual J++ delegate construct provides a convenient multicast
facility whereby a single delegate object can serve as a container
for several other delegates, and "fan out" calls to it. Since
Microsoft WFC components internally multicast their events (as does
Swing), there is little occasion for clients to directly combine
delegates. This feature appears to be intended to make it easier to
create new event types and new components which multicast them.
When implementing new event types in the Swing toolkit, a component
author may use the EventListenerList class to manage
lists of event handlers. Code using EventListenerList is
slightly more verbose than the corresponding code relying on the
Visual J++ compiler support for multicasting. On the other hand,
coding multicast in the standard Java language is more straightforward
and less "magical." This approach is also more easily modified to create
variations of the basic event notification procedure, such as running
boolean-valued event handlers until one returns true, or adding up the
results produced by integer-valued event handlers.
Moreover, the cost model for multicasting in a program written in the
unadulterated Java language is easier to understand and (if necessary) modify.
For example, each additional listener in a Swing component occupies
exactly two additional words, and a component with no listeners uses
no storage for the absent listeners. Instead of a one-size-fits-all
multicast mechanism, Swing designers and extenders are free to use
whichever data structure best fits the application.
Performance Issues
Looking "under the hood," it is easy to understand our conclusion that any
implementation of bound method references will either be inefficient
or non-portable.
A portable implementation of delegates that generates code capable of
running on any compliant implementation of the Java platform would
have to rely on the standard Java Core Reflection API. Every
bound method reference would then include an embedded object of type
java.lang.reflect.Method to refer to the target method. This
object, counting its three unshared subobjects, occupies over 30 words of
storage (assuming 2 words per object header, and subject to variability
with the length of the method name). Thus, the Visual J++ bound method
references, if they use reflection, must be an order of magnitude heavier
than Delphi Object Pascal method references. Moreover, each call to a
bound method reference must go through the reflective
Method.invoke protocol, which requires that arguments be
packaged up inside an Object array, and additional wrappers
and casts for primitive arguments and return values. (The
sidebar contains a detailed example.)
Thus, every call to a delegate of type
int(int,int) requires creation of three
Integer objects and a two-element Object
array, for a total of 14 words of heap allocation. This cost will
make programmers hesitate to use bound method references for frequent
operations, since calls to adapter objects always run at full speed.
Of course, a VM and its JIT could recognize these uses of reflective
operations as special cases and inline away the allocations. However,
the engineering effort required to implement such clever optimizations
is much better spent making all kinds of objects faster and lighter.
It is at this point that an aesthetic objection to adding redundant
features to the language design solidifies into a real-world product
engineering issue.
In the most extreme case, a compiler and VM might avoid reflection
altogether in favor of special native methods.
While a highly efficient implementation is achievable in this way,
such a strategy immediately precludes portability to compliant
implementations of the Java platform. This is a serious objection,
as it abandons the core value proposition of the Java platform:
"Write once, run anywhere."
Application footprint
The proponents of bound method references have made much of the
proliferation of classes that are created when adapter classes are used
extensively. They claim that these large numbers of classes
threaten an excessive enlargement of the application footprint.
In evaluating these claims, it is important to understand that adapter
classes are not inherently costly. The code for the methods within
these classes would be needed even if method references were used.
The cost increment is almost entirely due to overhead imposed by a
class file format that was designed prior to the introduction of inner
classes. As a result, much information is replicated for each inner
class that could in principle be shared.
For example, the classes under the package java.awt.swing
occupy about 3 megabytes of storage in about 1200 class files.
Constant pools account for 59 percent (1.8 megabytes) of this size.
Among the anonymous inner classes in this same group, fully 75 percent
of the on-disk size, or 611 bytes on average, is devoted to constant pools.
(Note: Unlike delegates, 30% of these anonymous classes have two
or more methods, so an exact comparison with delegates is difficult.)
Sun chose to implement inner classes in a manner that was
compatible with the existing VM. This preserved the investment
that our licensees and their customers have made in Java technology.
However, the Java platform is still evolving. From the very introduction
of inner classes, Sun has considered eventual extensions to
the JAR or class file formats that would make small
inner classes very inexpensive, because of sharing of constant
pools and other data.
It is also important to note that the in-memory footprint of an
application is not constrained by compatibility considerations. A VM
is free to introduce alternative in-memory representations, as
exemplified by the on-the-fly translation to machine code performed by
current high-performance VMs.
Newer versions of Sun's VM share information between constant pools,
so that the total size of the class files, "flattened" as they are,
is a misleading indicator of memory footprint.
It is clear, then, that efforts to
reduce application footprint are best directed toward creating even
more efficient Java VMs, not changing the language. Such
improvements will benefit all programs, not only those making use of
delegation, and will do so without compromising the simplicity and
elegance of the Java programming language.
Conclusion
Many of the advantages claimed for Visual J++ delegates -- type
safety, object orientation, and ease of component interconnection --
are simply consequences of the security or flexibility of the Java
object model. These advantages are routinely enjoyed by all
programmers using the Java language, without resorting to non-standard
language syntax or platform-specific VM extensions.
Bound method references are simply unnecessary. They are not part
of the Java programming language, and are thus not accepted by
compliant compilers. Moreover, they detract from the simplicity and
unity of the Java language. Bound method references are not the
right path for future language evolution.
|