previous article

next article


Reflection-based implementation of Java extensions: the double-dispatch use-case

Rémi Forax, Institut Gaspard–Monge, Université de Marne-la-Vallée, France
Etienne Duris, Institut Gaspard–Monge, Université de Marne-la-Vallée, France
Gilles Roussel, Institut Gaspard–Monge, Université de Marne-la-Vallée, France

space ARTICLE

PDF Icon
PDF Version

Reflection-based libraries may be used to extend the expressive power of Java without modifying the language nor the virtual machine. In this paper, we present the advantages of this approach together with general guidelines allowing such implementations to be practicable. We show how these principles have been applied to develop an efficient and general double-dispatch solution for Java, and we give the details of our implementation.


1 INTRODUCTION

Java programmers sometimes realize that their programming language lacks some feature. For instance, they could wish to have a double-dispatch mechanism to ease implementations dealing with tree-like structures or binary methods [4, 17]. This feature, which is a special case of multi-methods [11, 21, 8, 3, 22, 23, 10, 12, 1, 27, 2, 14], allows a method implementation to be chosen with respect to the dynamic type (late-binding) of both the receiver and a single argument. It can be achieved programmatically through the visitor design pattern [19, 15]. However, providing it at language level improves genericity and reusability, easing the design, maintenance and evolution of applications [25].

Several techniques could be used to implement such a Java extension. The language could be modified, producing standard Java bytecode, e.g., introducing new keywords [3, 10]. The Java Virtual Machine (JVM) could also be modified, either to accept an extended bytecode or to modify its standard semantics [12]. An alternative solution is to provide such an extension as a pure Java library, using the Java Core Reflection mechanism and bytecode generation, leaving the language and the virtual machine unchanged [13, 17]. This remark could be generalized to other Java extensions where reflection-based approaches are applicable [28, 20, 26].

This paper aims to show the worth of pure Java solutions which are the most flexible ones, although the least employed for efficiency reasons. First, section 2 argues their design advantages compared to environment-intrusive ones. In section 3, general implementation-design guidelines are given to make them practicable. Based on related works, section 4 shows how we apply these principles to provide an efficient double-dispatch implementation, the Sprintabout. Its implementation details are discussed in section 5.

2 DESIGN CHOICES

The most classical approach to implement Java extensions is to modify the language syntax and to provide the corresponding translator, pre-processor or compiler. This approach has the advantage of being performed at compile time, allowing many costly computations to be performed before execution. In particular, it allows the compiler to perform some static type-checking that is known to enhance software safety. It has the corresponding drawback: it can only use compile-time information. Indeed, in the context of Java’s dynamic class loading, some essential information such as type hierarchy may only be available at runtime. Furthermore, the resulting dedicated language is difficult to maintain, particularly when the underlying language evolves.

A second approach, that may be complementary to the extension of the syntax when runtime information is required, is to modify the Java virtual machine or its semantics. It has the advantage of providing precise runtime information on the executing application with a minimum overhead. However, it requires tricky knowledge of the internal of a specific virtual machine. Furthermore, applications implemented using such a modified virtual machine require a specific execution environment which is difficult to maintain and that may not be compatible with existing applications. Thus, this approach yields dedicated solutions whose portability is seriously restrained.

An alternative approach, already chosen to provide Java with runtime type information [28], multi-dispatch [13, 17] or aspect-oriented features [26], is to use reflection to access JVM’s runtime internal structures. Like the second approach, it benefits from dynamic information and, as a pure standard Java solution, it offers optimal portability. Even if it could led to runtime type exception (just as type casts), it has the advantage of being simple and directly accessible through the Java Core Reflection API. Contrarily to previous approaches, it allows several specific features to be used or combined, on demand, inside a single application. Moreover, being provided as a simple Java extension library, a new feature deployment only consists in adding a single JAR file to the ”classpath” of a standard Java environment. Unfortunately, reflection-based implementations are usually considered to be inefficient in space and time.

All of these approaches present advantages and limitations, but this paper deliberately focuses on enhancements of reflection-based techniques.

3 USING REFLECTION

This section points out problems of reflection-based implementations and gives guidelines to make them practicable.

First, from the essence of the Java Core Reflection API, any reflection-based implementation implies significant costs in space and time. Indeed, the reification of JVM internal objects induces some performance overhead compared with direct accesses available inside the JVM. Beyond object creations, this induces additional indirections. For instance, the reflective invocation of a simple method without argument is known to be slower than the corresponding classical call [6, 7]. Tests performed on a 2.4GHz Pentium 4 with 512Gb of RAM using SUN jdk1.5.04 under Linux (two firsts raws of Figure 1) show that reflective invocation is about 50 times slower than classical one, and even worse in server mode1.

Next, the genericity of reflective methods implies the construction of several intermediate objects that impacts on performance. In particular, a single call to the invoke() method usually requires the creation of an Object array to be filled with arguments and, when dealing with primitive types, boxing/unboxing is also necessary. Indeed, for any primitive-type argument or return value (int, double...), its reflective manipulation must be done through the corresponding wrapper type (Integer, Double...). Thus, to abstract a primitive value onto the reflection level, its wrapper object has to be constructed (boxing). Moreover, return values have to be cast to conform to the return-type of the method and sometimes, unwrapping primitive types (unboxing) is also necessary. These remarks still hold with jdk 1.5 even if boxing/unboxing and array creation are now hidden to the programmer. Figure 1 shows that performance of method calls with no argument and with one int argument are comparable, even for reflective invocations. However, this is only true if object and array creations are not taken into account2.

Finally, since Java does not allow modifications (extensions) to its internal structures through reflection, some data structures have to be duplicated outside the JVM, in the application. For instance, in order to associate information with classes, it is impossible to directly add a field to the class Class and it requires to rely on external structures such as hashtables. Thus, using reflection may lead to important performance penalties in terms of time and space if implementation is carried out without caution.

We argue that general strategies, already used by other frameworks [28, 13, 17], allow reflection-based implementations of language extensions to be improved. Our goal is not to improve Java reflective calls [6, 7] in general but rather to give some guidelines that allow to enhance the design and implementation of reflection-based implementation in special contexts such as language extensions.

Figure 1: Performance of one hundred million method calls

The first guideline to follow is to clearly identify two stages in the implementation: the creation time, when objects with reflective purpose are created, and the invocation time, when such objects are actually used to perform multiple tasks. To reduce time overhead, as many computations as possible have to be transfered from invocation time (just in time) to creation time. Indeed, in Java, durations of many computations remain negligible compared to class loading (disk access, class verification) and thus may not be perceived by users. Nevertheless, these pre-computations imply some states to be stored in order to be available just in time. This strategy transfers most computations from invocation time to creation time but may produce large data structures. Thus, programmers have to find the right balance between invocation time, creation time and space. The choice of algorithms and data structures used during these two phases is essential to make reflective implementation practicable.

The second guideline is to reduce space overhead, sharing as much data as possible. This can be achieved through specific algorithm implementations. However, this sharing should also conform to classical object-oriented design principles. Indeed, it is usually possible to share data between objects of the same class (e.g. through static fields), between classes if they are related by inheritance, or between threads. These data structures should support incremental modifications to ensure that creation-time information are reusable when some new information is discovered at invocation time. Developers must also take into account multi-threading since this worry is usually contradictory with time and space performance. Indeed, to maintain data coherence, synchronization is usually required and induces extra duration for method calls. More precisely, a method call is slower if synchronized (see Figure 1), even without delay induced by mutual exclusion. One way to avoid synchronization is to relax coherence and duplicate data but this leads to space overhead.

The third guideline is to try to minimize the use of expensive reflective mechanisms for the invocation-time part3 of the implementation. The first objective is to eliminate as many reified JVM’s internal objects as possible, in order to save space and to minimize indirections. Another goal is to minimize generic reflective calls, replacing them with dedicated calls in order to avoid intermediate object creations, casts and boxing/unboxing. This can be achieved by generating, at creation time, dedicated bytecode [17] according to a precise method signature specified by the developer instead of relying on the generic signature of the reflective API. More precisely, many JVM’s internal reified objects like Method or Class do not necessarily require to refer to the internal state of the JVM. Thus they may be partially simulated by user-defined objects which can be more efficient than reflective API
ones. In particular, a Method object that refer to a public4 method may be efficiently simulated by generating and instantiating a class implementing the corresponding method call. Moreover, in order to reduce intermediate object and array creations required by the generic invoke() method call, we propose to let the developer specify precisely the method signature as an abstract method in an abstract class. Code generation is then used at creation time to implement this abstract method by a call to the method with a corresponding signature on a specific receiver.

In order to illustrate this approach and its worth, we developed a class RFX. This class provides reflective-like method invocations that are more efficient than those of the generic reflective API. Suppose that you have to call a method with name m and two int parameters on a receiver whose class is unknown at compile time. With RFX, instead of using the generic class Method, you declare an abstract class5 containing at least an abstract method m() with a first parameter of type Object, corresponding to the receiver, and two other int parameters, as follows:

According to the receiver’s class c (dynamically provided in the following example by retreiveClass()), a call to RFX.bind() generates6 the bytecode of a concrete subclass of MethodM and returns an instance of it, mm. Then, given an object o of class c, invocation of o.m(1,2) is performed through the call mm.m(o,1,2).


The generated subclass implements the abstract method m() of MethodM by a direct call to the method m() on the receiver (see appendix A). Thanks to the object mm, the invocation of the method m is efficient, avoiding object or array creation. Figure 1 shows that the RFX method call is only 7 times slower than a classical method and 8 times faster than a standard reflective method call. Performance is even better in server mode where a RFX call is as fast as a classical one.

4 VISITORS: WALK, RUN AND SPRINT

We now deal with several solutions proposed to provide Java with double-dispatch. We first recall the interest of this mechanism and we argue the worth of offering it through a simple specific feature, freeing the programmer from its implementation details. Next, we present several existing frameworks providing improvements in this direction. Then, we detail the Sprintabout, a new framework allowing doubledispatch we have designed and implemented with respect to previous guidelines for reflection-based extensions. Finally, we present experimental results that show its efficiency compared with other approaches, including ad-hoc hard-to-maintain ones.

Motivation

The classical late-binding mechanism provided by Java allows a method implementation to be chosen dynamically, according to the type of the receiving object. This simplifies code reusability since, when new classes implementing the method are introduced, programmers do not have to implement complex selection mechanism nor modify the code.

Nevertheless, adding a new functionality for each class of an existing hierarchy is less straightforward. For instance, as proposed in [17], consider the following type hierarchy.

Imagine that, given an array of type A[], we want to compute where value(a) = i if a is of type Ai . In other words, we want a method int sum(A... array) such that, for instance, sum(new A0(),new A1(),new A2()) returns 3 (0+1+2).

A first solution is to test, with the Java instanceof statement, the actual dynamic type of each array element that is statically typed A. The corresponding code, based on successive and nested tests could become very intricate if the type hierarchy concerns interfaces with multiple inheritance. Anyway, it is tedious to implement, error prone and hard to maintain if the hierarchy or the semantics evolves.

A more object-oriented, but naive and dedicated solution, would be to add in each class a method int value() that returns the right number. To be simply used with an array of type A[], this not only requires modification of all classes, but also of the interface. Nevertheless, nowadays, most applications are built using classes or objects provided by third-party libraries (Java standard API, or off-the-shelf libraries). Even if their sources are available, they usually should not be modified. Thus, programmers cannot simply add methods to such classes7.

Two classical object-oriented techniques allow this difficulty to be worked around: class inheritance and delegation. However, on the one hand, inheritance is not always applicable: the case of final classes apart, some libraries use factory methods to create objects and prevent constructor access. On the other hand, if delegation could always be used by defining new wrapping classes for each data type, this leads to data structure duplications and to burdensome hard-to-maintain code.

A better design is to specify a set of methods int value (T t) in a separate class, one for each ”interesting” type (e.g. A0, A1 and A2 in our example), and to provide a general mechanism allowing the right one to be chosen with respect to the dynamic type of t. This late-binding mechanism on the argument of a method call, known as double-dispatch, would actually allow behaviors (methods) to be specified outside classes, separating algorithms from data. Unfortunately, this mechanism is not available in Java.

To face this lack programmatically, the programmer could design his code following the visitor design pattern [19, 15]. It simulates late-binding on the argument of a visitor method visit(a) using the classical late-binding on an a.accept (Visitor v) method provided by data classes. If this technically solves the problem of adding new functionality to a given class without modifying it, this design pattern has several limitations. First, it requires data classes to be engineered to accept visitors, i.e., to implement an accept() method. Second, the behavior is strongly tied to the existing type hierarchy: it must implement a special Visitor interface which, for full generality, must include one visit() method for each accessible type. This has two drawbacks. The programmer must implement every visit() method even if some of them could have been captured by a widening reference conversion of the argument type. Moreover, any extension of the type hierarchy requires the Visitor interface to be extended; then, all existing visitor implementations have to be modified since they must implement the new visitor interface to be accepted by existing classes.

Double-dispatch implementations

Several research works proposed to implement the double-dispatch as a reflectionbased extension. Based on the Walkabout of Palsberg and Barry Jay [25], Grothoff proposed the Runabout [17] that allows a new functionality to be specified over data without modifying their class nor requiring them to implement accept(Visitor v) methods. The programmer specifies behaviors through visit() methods (one for each interesting parameter type) in a class that extends the provided general class Runabout. This class contains a method visitAppropriate(Object o) that is able to dispatch the invocation to the right method visit() with respect to its (dynamic) argument type.

Compared with theWalkabout, the Runabout minimizes the use of the Java Core Reflection API to improve its performance. When an instance of the Runabout is created, reflection is used to find all available visit() methods. For each parameter type, a class is generated and loaded on-the-fly. It contains a method dedicated to invoke the visit() method corresponding to this parameter type. An instance of each of these classes is created and stored in a dynamic code map. Thus, at invocation time, the visitAppropriate() method simply uses its argument class and the dynamic code map to dispatch the invocation to the appropriate code.This technique avoids the use of the generic invoke() method, known to be time consuming.

At the same time this work was performed, Forax et al. developed a reflective framework to provide full multi-polymorphism in Java [13], i.e. late-binding on all method arguments. With the Java Multi-Method Framework (JMMF), the programmer constructs a MultiMethod object that represents all methods with a given name and a given number of parameters that are accessible in a given class. To perform a multi-polymorphic invocation with an array of arguments, it suffices to call the invoke() method of the multi-method object. JMMF dispatches the call to the most specific method, according to dynamic types of arguments.

The aim of JMMF is more general than the simple double-dispatch and more generic than the Runabout. It does not require inheritance from any class, allowing another inheritance. Furthermore, it does not constraint the name of the method to be visit and allows several distinct multi-methods to be defined in a same class. Nevertheless, even if it computes data-structure at creation time in order to minimize invocation-time overhead (and even allows them to be shared between multi-methods), its implementation efficiency suffers from relying on the Java Core Reflection API.

The Sprintabout

Inspired by previous related works, we have designed and implemented the Sprintabout, following the guidelines we drew in section 3. The code below illustrates how it can be used on our running example.

In this example, the abstract method whose name ends with Appropriate is used to identify the methods to be considered for multi-dispatch, i.e. value(). The createVisitor() method collects these methods through reflection on its argument class. It returns a visitor object, standing for all methods value() belonging to the class and in charge of dispatching invocations according to the dynamic type of the argument provided to valueAppropriate() calls.

The Sprintabout implementation respects guidelines outlined in section 3. At creation time, a hashtable is created to be used at invocation time. Initially filled by the method createVisitor(), it associates types with integers that index the method to call for this type of argument. It could be completed dynamically, when new argument types are discovered, in order to cache the method identifier resolved for this type. As a static field, this hashtable is shared between all instances of the same class.

The implementation bytecode for the abstract “appropriate” method is generated at creation time. At invocation time, when this concrete generated method is called, it performs a lookup in the hashtable to retrieve the method index associated with the type of its argument; if the type is not found, the method’s index for the closest type must be determined and their association is cached into the hashtable. Then, the dispatch is performed using this index in a simple switch/case statement to call the right method. The source code obtained by decompiling the bytecode generated for the method valueAppropriate() of our example is given in Appendix B. The classloader caching mechanism ensures that the bytecode generation is only performed once. This implementation technique allows Sprintabout to become completely independent of reflective method call at invocation time. Compared with the Runabout that reifies one dedicated code object (generated class) for each visit() method, the Sprintabout only builds a single visitor class for all methods. This minimizes memory footprint and delegation overhead.

Moreover, compared to other approaches, the specification of the abstract “appropriate” method allows the user to give a signature as precise as possible. Indeed, this method does not have to be generic since it is specifically generated for each particular visitor class. This feature allows the compiler to perform some static type-checking on argument types, e.g. it is possible to constraint argument type. For instance, in previous example, the declaration of valueAppropriate() imposes the argument to implement the interface A. Finally, the “appropriate” method signature may return values of any type, including primitive types, without requiring cast or boxing/unboxing. As JMMF and contrarily to the Runabout, the Sprintabout does not require the implementation class to inherit from some special class (i.e. Runabout) and thus allows any other class extension.

Sprintabout implementation relies on jdk 1.5 generics, on JSR133 for concurrency and uses ASM [5] for code generation. It is freely available on the Web8 and its implementation is detailed in section 5.

 

number of visit methods (client mode deep hierarchy)
number of visit methods (client mode flat hierarchy)
number of visit methods (server mode deep hierarchy)
number of visit methods (server mode flat hierarchy)

Figure 2: Dispatch time in client and server mode, with deep and flat type hierarchy

Experimental results

In this section, we present an evaluation of invocation performance of Sprintabout compared with other double-dispatch solutions. We compare it with six techniques: Instanceof, implementing filtering using instanceof tests, Dedicated, where a dedicated method is added inside each data class, Visitors, that implements the visitor design pattern, Runabout, implementing double-dispatch using inheritance of the class Runabout, JMMF, implementing double-dispatch as a special case of multidispatch provided by the library JMMF and finally Sprintabout.

Our tests present the evolution of the time required by one million method invocations when the number of methods increases. In these tests, we use two distinct type hierarchies to provide the argument values of method calls. In the deep hierarchy, all types are related by inheritance whereas in the flat one, they are unrelated. Figure 2 presents results of these tests in both standard “client” execution environment and with a virtual machine started in server mode. All the tests of this section have been performed on a 2.4GHz Pentium 4 with 512Gb of RAM using SUN jdk1.5.04 under Linux, but results on the same architecture using Windows XP are comparable.

These tests show that Visitors and Dedicated techniques (whose graphs overlap) have the best performance on average. Instanceof technique has the best performance for the flat hierarchy. However, for the deep one, its performance strongly decreases when the number of visit increases; it seems that Instanceof implementation relies on some caching techniques that fails when hierarchy becomes to large.

Considering the reflection-based implementations, we see that the Spintabout is about three times faster than the generic JMMF and one fourth faster than the Runabout, which also uses a code generation technique. This shows that avoiding the use of Java Core Reflection mechanism allows Runabout and Sprintabout invocation time perfomance to be improved. This also proves that the Sprintabout, which eliminates JVM’s internal object reification and genericity of method calls, has better performance than Runabout.

Tests performed in server mode give comparable results. Nevertheless, Sprintabout has now better performance than Visitor and Dedicated techniques. This behavior comes from the execution performance of optimized code [9] that is better for switch statements produced by Sprintabout than for virtual calls used by other techniques.

5 SPRINTABOUT IMPLEMENTATION

This section describes more precisely some Sprintabout implementation details. Our aim is not to give burdensome technical details, but rather to point out some problems and the way they are addressed. First, we briefly describe the process overview, and its two main steps: creation time and invocation time. Next, we detail particular contexts, determined at creation time, where specific algorithms may be used at invocation time. These algorithms, given an argument type at invocation time, select the right method to invoke. Finally, we describe our specific implementation of hashtable that, given our particular context, deals efficiently with concurrency.

Overview and general principle

The Sprintabout implementation respects guidelines outlined in section 3. At creation time, all declared parameter types of concerned methods are considered in order to prepare the invocation time process.

First of all, at creation time, the method createVisitor() of the Visitor-Generator is called with a class as argument. In this class, given a method method-Appropriate(), a reflective retrieval of all methods named method () with one parameter is performed. Parameter types of these methods are collected and associated with indexes9 (non-negative integers). If n is the number of parameter types, a precision matrix PM of size n × n is constructed. In this binary matrix, PM[i][j] = 1 if and only if i is the index of a type Ti that is more precise than the type Tj indexed by j. The precision relation defined by this matrix, noted Ti Tj, intuitively means that a method declaring a parameter of type Tj accepts arguments of type Ti. This precision relation is usually provided by the method isAssignableFrom() of java.lang.Class or, for special cases such as primitive types or arrays, by specific treatments according to the Java Language Specification, chapter 5 [16]. This matrix is widely used at creation time, in order to determine the definition context of the set of methods considered. The definition context depends on the particularities of the parameter types hierarchy and induces the choice of a dedicated algorithm that selects the right method to call at invocation-time. In addition to the choice of the algorithm, the creation-time step builds the main data-structure used at invocation time: a hashtable whose keys are types and values are indexes. This hashtable is initially filled at creation time with the declared parameter types and their indexes.

At invocation time, given the type Arg of an actual argument arg of a call to method Appropriate(), the invocation-time algorithm uses the hashtable to find the index identifying the method to invoke. If the type Arg is found in the hashtable, this directly yields the index. Otherwise, the algorithm has to inspect all less precise types (usually supertypes) of Arg in order to find the closest type registered in the hashtable; this process may require the use of the precision matrix. Sometimes, several less precise types are not comparable and it is not possible to determine a closest type. In this case, a runtime exception is thrown. The result of this process is cached, by inserting in the hashtable the association between the argument type Arg and the index found.

The case of null argument

Since this process uses the type of the argument, the case where the argument is null must be treated separately. In such a situation, the method to be called is the most precise one, if it exists, i.e., the method whose parameter type is more precise than every other declared parameter types. The existence of such a most precise method is determined at creation time using the precision matrix. Indeed, if a type of index i is the most precise then . If a most precise method exists, its index is associated in the hashtable with an unused type (e.g. void.class). At invocation time, when the value null is encountered, the algorithm artificially looks for this type in the hashtable. If a most precise method does not exist, then a predefined index (e.g., -1) is associated in the hashtable with void.class and, at invocation time, when this index is found, a runtime exception is thrown.

Definition contexts and algorithms

The particular treatment of a null argument does not depend on the definition context. In the same way, whatever is the definition context, if the argument is not null, its type Arg is looked for in the hashtable. If an index is found, the corresponding method is invoked, otherwise, invocation-time process depends on the definition context determined at creation time. This section describes these different contexts, how they are determined at creation time, and their corresponding invocation-time algorithms.

Only classes in parameter types

The simplest definition context is when all declared parameter types are classes like in Figure 3 (a). Since Java does not allow multiple inheritance of classes, looking step by step in the hashtable for direct superclass of any argument type yields a single index, if one exists. At creation time, in order to determine this definition context, we simply use the method isInterface() of java.lang.Class over each declared parameter type. This particular context allows us to discard the precision matrix. The corresponding invocation-time algorithm simply retrieves the superclass of Arg, using the method getSuperclass() of java.lang.Class, and looks for it in the hashtable. This is done step by step until either it finds an index (the method to invoke) or reaches the root class Object (in this case a runtime exception is thrown). The association between Arg and its index is cached in the hashtable for further use.

Totally ordered parameter types

Another definition context allows the precision matrix to be discarded. It occurs when all declared parameter types are totally ordered by the precision relation. In other words, the hierarchy is linear: each declared parameter type is either more precise or less precise than any other declared parameter types like in Figure 3 (b). This context is determined from the precision matrix verifying that, for all i 1..n:

In this case, several declared parameter types less precise than an argument type may exist but, since they are totally ordered, one of them is the most precise. To ease to selection of the most precise method and to avoid the use of the precision matrix at invocation time, the indexes of declared parameter types are reordered in the precision order10. Then, to retrieve the most precise type it suffices to retain the type with the greatest index. This renumbering is realized at creation time, and the algorithm used at invocation time only uses the hashtable. More precisely, it traverses recursively all types less precise than Arg (supertypes) in order to find, if it exists, the type with greatest index. During this traversal through the type hierarchy, for each type found in the cache the branch leading to less precise types is pruned since they will never yield a greater index. If a greatest index is found, the result is cached in the hashtable and the corresponding method is invoked; otherwise, a runtime exception is thrown.

Precision relation and bad fork

The difference between the last two definition contexts relies on the notion of fork between parameter types. We say that there is a fork in the hierarchy of parameter types if a given type is more precise than at least two other parameter types that are not comparable, as in examples (d) and (e) of Figure 3.

Figure 3: Examples of parameter type hierarchies for different definition contexts

Generally, looking for less precise types of a given argument type leads to several parameter types and the most precise one must be determined, if it exists. Suppose that two incomparable parameter types less precise than the argument type are found. If there is no fork, then it is possible to conclude that no most precise method exists, like in Figure 3 (c). Otherwise, if a fork exists, a most precise method may exist. Indeed, a third parameter type may exist that is more precise than the two incomparable ones, and thus, further comparisons are required.

In order to minimize comparisons, we propose to retrieve types the most precise first. This ensures that when two types are found in this order then either one is more precise than the other, or a most precise does not exist. This property can sometimes be ensured by the traversal of the type hierarchy. More precisely, if one of the three types involved in a fork is a class, then the most precise type is necessarily a class, as in Figure 3 (d); a traversal that performs getSuperclass() before getInterfaces() will encounter this most precise class first. Otherwise, the fork, called “bad fork”, only involves interfaces, as in Figure 3 (e), and the hierarchy traversal cannot guarantee retrieval of types in order. In this case, parameter type indexes need to be renumbered at creation time, like in the totally ordered context, to allow the invocation-time algorithm to test them in order.

To be able to characterize the definition context at creation time, after collecting all declared parameter types, we look for an interface definition that explicitly extends two other parameter interfaces. If such a bad fork is found, the definition context is said to be “with bad fork”, else “without bad fork”.

In a definition context without bad fork, at invocation time, given an argument type Arg not found in the cache, the algorithm begins like for totally ordered types. It traverses the hierarchy of less precise types (supertypes) of Arg, looking for classes prior to interfaces, and if none or only one index is found, it respectively throws an exception or invoke the corresponding method. Otherwise, as soon as two distinct indexes are found, their types must be compared. If they are comparable, the most precise is retained and the process continues. If they are not comparable, the context allows us to stop the traversal and to conclude that there is no most precise method and an exception is thrown.

In a context definition with a bad fork, at invocation time, the algorithm starts similarly, but must collect all indexes of types less precise than Arg before performing any comparison. Then, while at least two indexes remain, the two largest i and j are considered. If the corresponding types are not comparable, the type renumbering performed at creation time ensures that there is no more precise type, and thus no most precise method: a runtime exception is thrown. Otherwise, if one of the corresponding types is more precise than the other, its index is retained and the comparison process continues with remaining indexes. When the process succeeds with a single index, the corresponding method is invoked.

In both contexts, successful index retrievals are cached.

Then, in better cases, complexity of method retrieval at invocation time has the same complexity as a dynamically-sized hashtable access (roughly O(1)). The worst case requires to traverse the hierarchy of the argument type in order to retrieve all its supertypes and then to identify the most precise one. In the worst case, this process is linear (O(n)) in the number, n, of types in the hierarchy. Linearity is ensured by the creation-time pre-processing which requires d2 calls to the method isAssignableFrom(), where d is the number of declared parameter types. Complexity of isAssignableFrom() is, in the worst case, linear in the size of the type hierarchy. Since complexity of other pre-processing steps are less than O(d2), the general worst-case complexity of creation-time process is O(d2× n).

Data-structure implementation and concurrency

The cache used in our implementation allows to retrieve, given a class object, the index (integer) corresponding to a visit method. This cache is implemented by a dedicated hashtable. Indeed, compared to the generic implementation available in the Java API, our implementation can avoid boxing/unboxing of the index value into Integer. It is also possible to use reference equality == instead of equals() on Class objects since uniqueness is ensured by the virtual machine. Another optimization concerns concurrency. Compared with java.util.concurrent.ConcurrentHashMap, our implementation may be simplified since we never need to remove an entry from the cache. The cache only supports insert() and get() operations. To improve performance of the cache, we have chosen to completely relax the synchronization on the get() operation. This is possible since all hashtable modifications are atomic. Freshness of values is ensured by volatile variable accesses according to the causality of the Java Memory Model (JSR 133). We use mutual exclusion on the insert() operation for data coherence, however, we allow multiple instances of a same entry to be present in the hashtable to improve time performance.

6 CONCLUSION

This paper promotes the use of reflection based implementations to extends the Java environment. It proposes several guidelines to make such implementations practicable. First, as many computations as possible have to be performed at creation time to reduce invocation-time overhead. Next, as many data as possible have to be shared to reduce space overhead, taking into account concurrency issues. Finally, code generation may be used to minimize the invocation-time use of reified objects and of the generic methods of the Java Core Reflection API. This latter point is illustrated by a simple class RFX that improves reflective method invocations. To illustrate this approach, we describe a reflection-based implementation of double-dispatch for Java that conforms to these guidelines. Experimental results compare this implementation with other techniques and validate the usability of our approach.

A RFX GENERATED CODE

The following Java class corresponds to the bytecode generated by the RFX.bind() method if, in the example of section 3, the class c provided by the method retreive-Class() has name Test.

B SPRINTABOUT GENERATED CODE

The following Java source code has been obtained by decompiling the bytecode generated by Sprintabout for the example of section 4.


Footnotes

1 Server mode corresponds to a special version of the Sun HotspotTM virtual machine that performs aggressive optimizations [24].

2 Figure 1 details subcases of reflective call with one argument, that measure distinct invocation times taking or not taking into account array and Integer object creations.

3 They may also be minimized for creation-time part but this is less crucial.

4 This is not the case for other method kinds, like private ones, that require access to the JVM’s internal in order to pass security checks.

5 An interface may also be used but with some performance penalty.

6 The class is generated only once thanks to the classloader’s cache.

7 This may be done through Aspect Oriented Programming features without class modification [18].

8 http://igm.univ-mlv.fr/~forax/works/sprintabout/

9 An index identifies both a type and the method that declares a parameter of this type.

10 In this particular case, a well-ordered numbering should respect,


REFERENCES

[1] G. Baumgartner, M. Jansche, and K. Lufer. Half & half: Multiple dispatch and retroactive abstraction for Java. Technical Report OSU-CISRC-5/01-TR08, Dept. of Computer and Information Science, Ohio State University, Mar. 2002.

[2] L. Bettini, S. Capecchi, and B. Venneri. Translating double-dispatch into singledispatch. In Proc. of WOOD’04, ENTCS. Elsevier, 2004.

[3] J. Boyland and G. Castagna. Parasitic methods: An implementation of multimethods for Java. In OOPSLA’97, number 32–10 in SIGPLAN Notices, pages 66–76, Atlanta, Georgia, Oct. 1997. ACM Press.

[4] K. Bruce, L. Cardelli, G. Castagna, The Hopkins Object Group, G. T. Leavens, and B. Pierce. On binary methods. Theory and Practice of Object Systems, 1(3):221–242, 1996.

[5] E. Bruneton, R. Lenglet, and T. Coupaye. ASM: a code manipulation tool to implement adaptable systems. In Adaptable and extensible component systems, Grenoble, France, Nov. 2002.

[6] W. Cazzola. SmartMethod: an Efficient Replacement for Method. In SAC’04, pages 1305–1309, Nicosia, Cyprus, Mar. 2004. ACM Press.

[7] W. Cazzola. Smartreflection: Efficient introspection in java. Journal of Object Technology, 3(11):117–132, Dec. 2004. http://www.jot.fm/issues/issue_2004_12/article6

[8] C. Chambers. Object-oriented multi-methods in Cecil. In ECOOP’92 proceedings, LNCS, Utrecht, The Netherlands, July 1992. Springer.

[9] C. Click. Java technology performance myths exposed, 2005. http://developers.sun.com/learning/javaoneonline/2005/coreplatform/TS-3268.pdf.

[10] C. Clifton, G. T. Leavens, C. Chambers, and T. Millstein. MultiJava: Modular open classes and symmetric multiple dispatch. In OOPSLA’00 proceedings, ACM SIGPLAN Notices, Minneapolis, USA, Oct. 2000.

[11] L. G. DeMichiel and R. P. Gabriel. The Common Lisp Object System: An overview. In ECOOP’87 Proceedings, LNCS, pages 151–170, Paris, France, June 1987. Springer.

[12] C. Dutchyn, P. Lu, D. Szafron, S. Bromling, and W. Holst. Multi-dispatch in the Java Virtual Machine design and implementation. In COOTS’01 proceedings, San Antonio, USA, Jan. 2001.

[13] R. Forax, E. Duris, and G. Roussel. Java multi-method framework. In TOOLS Pacific’00 Proceedings, Sidney, Australia, Nov. 2000. IEEE Computer.

[14] R. Forax, E. Duris, and G. Roussel. A reflective implementation of java multimethods. IEEE Transactions on Software Engineering (TSE), 30(12):1055– 1071, 2004.

[15] E. Gamma, R. Helm, R. Johnson, and J. Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1995.

[16] J. Gosling, B. Joy, G. Steele, and G. Bracha. The JavaTM Language Specification–Second Edition. Addison-Wesley, 2000.

[17] C. Grothoff. Walkabout revisited: The runabout. In ECOOP’03 proceedings, LNCS, pages 103–125. Springer, 2003.

[18] J. Hannemann and G. Kiczales. Design pattern implementation in java and aspectj. In Proc. of OOPSLA’02, pages 161–173, Seattle, USA, Nov. 2002. ACM SIGPLAN.

[19] D. H. H. Ingalls. A simple technique for handling multiple polymorphism. In Proceedings of OOPSLA’86, pages 347–349, Portland, Oregon, Nov. 1986.

[20] G. Kiczales, E. Hilsdale, J. Hugunin, M. Kersten, J. Palm, and W. Griswold. Getting started with AspectJ. Communications of the ACM, 44(10):59–65, 2001.

[21] G. Kiczales, J. D. Rivieres, and D. Bobrow. The Art of the Metaobject Protocol. MIT Press, Cambridge, MA, 1991.

[22] M. Kizub. Kiev language specification. An extension of Java language that inherits Pizza features and provides multi-methods (http://forestro.com/kiev/), July 1998.

[23] T. Millstein and C. Chambers. Modular statically typed multimethods. In ECOOP’99 proceedings, number 1628 in LNCS, pages 279–303, Lisbon, Portugal, June 1999.

[24] M. Paleczny, C. Vick, and C. Click. The Java HotSpotTM server compiler. In Proc. of JVM-01, pages 1–12, Monterey, California, USA, Apr. 2001. USENIX Association.

[25] J. Palsberg and C. B. Jay. The essence of the visitor pattern. In COMPSAC’98 proceedings, pages 9–15. IEEE Computer Society, 1998.

[26] R. Pawlak, L. Seinturier, L. Duchien, and G. Florin. JAC: A flexible solution for aspect-oriented programming in java. In Proceedings of Reflection’01, number 2192 in LNCS, Kyoto, Japan, Sept. 2001. Springer-Verlag.

[27] J. Smith. Draft proposal for adding multimethods to c++, Sept. 2003. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2003/n1529.html.

[28] M. Viroli and A. Natali. Parametric polymorphism in java: an approach to translation based on reflective features. In Proceedings of OOPSLA’00, pages 146 – 165, Minneapolis, Minnesota, United States, 2000.

 

About the authors



  Rémi Forax is Mâitre de Conférences at University of Marne-la-Vallée since 2003. His main research areas concern the use of reflection and of bytecode generation to enhance the Java programming and executing environment. He can be reached at remi.forax@univmlv.fr. See also http://igm.univ-mlv.fr/˜forax.


  Etienne Duris is Mâitre de Conférences at University of Marnela-Vallée since 2000. His research focuses on program transformations and on the use of reflection to extend the expressive power of programming languages. He can be reached at etienne.duris@univmlv.fr. See also http://igm.univ-mlv.fr/˜duris.
space Gilles Roussel is Professor at University of Marne-la-Vallée since 2004. His research works cover program transformations, language processing enhancements, genericity and routing. He can be reached at gilles.roussel@univ-mlv.fr. See also http://igm.univmlv.fr/˜roussel.

Cite this article as follows: Rémi Forax, Etienne Duris and Gilles Roussel, "Reflection-based implementation of Java extensions: the double-dispatch use-case", in Journal of Object Technology, vol. 4, no. 10, Special Issue: OOPS Track at SAC 2005 SantaFe, December 2005, pages 49-69, http://www.jot.fm/issues/issues 2005 12/article3


Previous column

Next article