Parametric Polymorphism in Java: an Efficient ... - CiteSeerX

0 downloads 0 Views 124KB Size Report
posed a translation for parameterized types into Java, called .... public static int countID; static{ ... } //Initialization code for h public static $TD ..... String[] args) {.
Parametric Polymorphism in Java: an Efficient Implementation for Parametric Methods Mirko Viroli DEIS - Universita’ degli Studi di Bologna via Rasi e Spinelli 176 47023 Cesena (FC), Italy

[email protected] ABSTRACT The typical implementations for parametric polymorphism in object oriented programming languages are space consuming, in that new binary code is created as new instantiations of parametric types are used. Recently, it was proposed a translation for parameterized types into Java, called LM translation, which avoids this problem by collecting type descriptors for parametric types at load-time. Another useful feature related to parametric polymorphism is that of parametric methods, which is often a necessary tool to completely benefit from th expressiveness and the code reuse of parametric polymorphism. In this paper the LM translator is extended in order to deal with parametric methods too. While previous implementations lead to a relevant size overhead, the one here proposed defers most of the overhead at the classes’ load-time, with only little overhead at run-time. It turns out that the general impact of this implementation on the code execution is definitely lower than that of previous approaches.

1.

INTRODUCTION

Parametric polymorphism is a programming mechanism widely adopted, and whose usefulness has been at this point well understood. It allows to abstract a piece of code from one or more types, making it reusable in many different contexts. The most frequent example of use is the implementation of collection data types, whose behaviour and features are independent from the type of the elements of the collection. Typical implementations of parametric polymorphism are Ada generics [1], C++ templates [6], and ML polymorphic functions or data types [8, 5]. In its former versions, Java has not provided parametric polymorphism, mainly in order to keep the language as simple as possible. Genericity is partially supported through the super-type Object from which each class extends. In fact, dealing with a common bound for all the types makes it possible to strengthen the code reuse of inclusive poly-

Permission to make digital or hard copies of all or part of this work for personal or classroom use is granted without fee provided that copies are not made or distributed for profit or commercial advantage and that copies bear this notice and the full citation on the first page. To copy otherwise, to republish, to post on servers or to redistribute to lists, requires prior specific permission and/or a fee. SAC 2001 Las Vegas, Nevada USA Copyright 2001 ACM 1-58113-324-3/01/02 ..$5.00

morphism (inheritance), as the practice of programming in Java has revealed. However, the lack of parametric polymorphism has been recognized to be a serious limitation of expressiveness [4], thus many proposals for the extension of Java have been studied. They can be mainly divided in two groups: the proposals for extension of the Java Virtual Machine (JVM) or of parts of it [2, 7, 12], and the translation approaches [11, 3, 10, 13]. In the former case compatibility with existing Virtual Machines is lost, although they generally lead to more efficient implementations. The translation approaches on the contrary, may introduce a significant overhead in both space and time, but do not affect in any way the Virtual Machine, then producing code executable on all the existing Java platforms. In this paper we focus on the latter kind of implementation, which has in fact received more interest. Two important issues that greatly influence the design of a translator are (i) the full integration of parametric types with the core typing and (ii) performance. At this time the proposal that seems to better address these issues is LM translator [13]. It uses the basic idea of carrying type information at run-time through objects representing types, in a framework which is a mixture of previous approaches. It relies on the erasure technique as GJ translator [11] does, it uses type information created by the translator in a similar way to NextGen translator [3], and finally it supports parametric type instantiations at load-time as suggested in [2]. Full type-integration is reached in that both parametric types and type variables can be used in each context where a type identifier is expected. Overhead is bound to acceptable values due to a novel translation technique in which most of the run-time management needed to support the typeintegration is deferred at the classes’ load-time. This highly decreases the impact of the translation with respect to more naive implementations (see for instance the depiction of RM translator in [13]). An important feature that LM translator does not deal with yet, is that of parametric methods. A lot of useful operations involve two values belonging to different parametric types, for instance appending two different parametric lists in order to create a more general one. In this case parametric methods allow for a simple, concise and direct implementation, while parametric classes alone do not provide for satisfactory solutions. In this paper we extend the LM translator so as to deal

with parametric methods too, by applying its basic idea of type-integration through the creation of type descriptors at load-time. The main problem with the implementation of parametric methods is to deal with dynamic dispatching, which is needed when a parametric method is overridden by a more specific class. This is addressed by attaching to the type descriptors generated by LM data structures called Virtual Parametric Method Tables, containing the instantiations of the parametric methods that the current application may use. This is done as usual only once at load-time, highly reducing the impact on the execution speed. The result is a significant improvement with respect to older implementations for Java and C++, which typically create a different method for each different instantiation of its generic version, leading to dynamically increasing classes. As in [13, 3, 11], we generally describe the translation (i) by isolating a source code with minimal size but comprehending all the features of interest, (ii) by giving the corresponding translation and finally (iii) by providing a detailed description of both the translation behaviour and the execution of the translated code. In particular we tried to choose the examples that better allow to provide a comprehensive yet simple treatment of the problem. The remainder of the paper is organized as follows. Section 2 introduces LM translation, trying to keep the description both short and self-containing (see [13] for more details on it). Section 3 describes parametric methods, their usefulness, and the drawbacks of their typical implementations. Section 4 shows the implementation of parametric methods in LM supposing not to deal with dynamic dispatching, while Section 5 describes how this issue can be addressed by exploiting Virtual Parametric Methods Tables. Section 6 shows the details of the final translation, together with some evaluation of performance. Section 7 concludes.

2.

LM TRANSLATION

The typical implementation for parametric polymorphism in object-oriented programming languages relies on the socalled macro-expansion technique, or at least on some optimized variant of it [6, 10, 3]. A different version of the binary code is created for each different instantiation of the corresponding generic source code. This is done for parametric classes, parametric methods and parametric functions as well. In order to avoid the high consumption of space involved, LM translator for Java [13] applies the idea of mapping parametric types to objects representing type descriptors. In this way each object carries all the information needed to support type-integration. The overhead impact is highly decreased thanks to a novel technique that defers the creation of all the type descriptors at the loading of some client class. A table of type descriptors allowing for fast access, typically a Hashtable, is maintained so as to prevent double creations of descriptors, thus keeping memory overhead small. Each object that is an instance of a parametric type will refer to the corresponding type descriptor, namely to an entry in the table of descriptors. A type descriptor keeps the following information: • a unique, incremental identifier assigned at creation time; • the core class representation java.lang.Class of the current class;

• an array of type descriptors storing the instantiation for the type parameters; • the type descriptor of the direct super-type of the current one, called the father descriptor; • an array of type descriptors representing those parametric types, used within the class in type-dependent operations, whose instantiation depends on the actual binding of the current type parameters, and which we call friend types. The class of type descriptors is called $TD, and has the following structure: public class $TD { public int ID; public java.lang.Class c; public $TD[] params; public $TD[] friends; public $TD father; ... public $TD(Class c, $TD[] p) { ... } public boolean checkNew() { ... } public boolean isInstance(Object o) { ... } }

The constructor is used to create a partially-defined type descriptor, so that one can check if the type descriptor has already been registered in the type descriptor table, stored in the $TDManager class (type descriptors manager). This checking process is realized through the method checkNew. In case its result is true, the descriptor has then to be inserted in the descriptor table, namely it has to be registered, and then it has to be completed by filling the fields friends and father. The class $TDManager has the following signature: public class $TDManager { private static Hashtable h; public static int countID; static{ ... } //Initialization code for h public static $TD register(Class c) { ... } public static $TD register(Class c,$TD[] p) { ... } }

Two methods for registering type descriptors are provided; they respectively register non-parametric and parametric types1 . Both may return an already-registered type descriptor or create and return a new one. See Figure 1 for the actual implementation of both $TD and $TDManager, along with that of interface $Parametric, which each parametric class will implement, and with the depiction of class $Array, which is used to integrate arrays and parametric types2 . Now consider the example code of Figure 2. Class Cell is a simple wrapper for an element of type T, and provides a getter method (get), a setter method (set), a setter method that assigns the field only if the object conforms to the type T (setIfPossible) and finally a method that returns a Cell representation of the receiver object (getCString). Notice that method setIfPossible realizes a type-dependent operation, which requires run-time type 1 Notice that since a non-parametric type may be the instantiation for a type parameter, and these instantiations are represented through type descriptors, we also need type descriptors for non-parametric types 2 We won’t discuss about arrays in this paper, since their influence on parametric methods does not issue new problems than those discussed in [13].

public class $TD { public int ID; //Unique identifier public Class c; public $TD[] params,friends; public $TD father; private int hashValue; private boolean bNew=true; public static $TD[] voidTDs=new $TD[0]; public static $TD objTD;

public class $TDManager { private static Hashtable h=new Hashtable(101,0.75f); public static int countID=1; static{ h.put($TD.objTD,$TD.objTD);} public static $TD register(Class c) { $TD T=new $TD(c,$TD.voidTDs); Object o=h.get(T); if (o==null){ h.put(T,T); T.ID=countID++; T.father=register(c.getSuperclass()); return T; } return(($TD)o); } } public static $TD register(Class c,$TD[] p) { $TD T=new $TD(c,p); Object o=h.get(T) ; if (o==null){ h.put(T,T); T.ID=countID++; return T; } return(($TD)o); }

static { objTD=new $TD(Object.class,voidTDs); objTD.ID=0; objTD.father=objTD; } public $TD(Class c, $TD[] p) { this.c=c; params=p; ID=-1; processHash(); } public boolean checkNew() { if (!bNew) return bNew; bNew=false; return true; } private void processHash() { ... // Implementation for a hash function } public int hashCode() { return hashValue; } public boolean equals(Object obj) { ... // Simple equality test } // Implementing subtyping public boolean isInstance(Object o) { if (o instanceof $Parametric){ int Q;$TD T=(($Parametric)o).getTD(); for (;(Q=T.ID)!=ID && Q!=0;T=T.father); return Q==ID; } else return c.isInstance(o); }

} public class $Array implements $Parametric { private $TD $td; public Object[] ao; public $Array($TD $td,int n){ this.$td=$td; ao=new Object[n]; } public $Array($TD $td,Object[] a){ this.$td=$td; ao=a; } public $TD getTD() { return $td;} public static $TD createTD($TD[] params) { // Simple facility to create descriptors for Arrays ... } } public interface $Parametric { public $TD getTD(); }

}

Figure 1: Implementation for library classes of standard LM translation

class Cell{ private T t; Cell(T t){ this.t=t; } void set(T t){ this.t=t;} T get(){ return t;} void setIfPossible(Object o){ if (o instanceof T) this.t=(T)o; } Cell getCString(){ return new Cell(""+t); } }

class Pair extends Cell{ private S s; Pair(R r,S s){ super(r); this.s=s; } void setSecond(S s){ this.s=s; } S getSecond(){ return s;} Pair getReverse(){ return new Pair(s,get()); } }

class Client{ public static void main(String[] s){ Cell c1=new Cell(null); c1.set(new Integer(1)); Integer I=c1.get(); c1.setIfPossible("2"); // "2" is not stored into c1 Cell c2=c1.getCString(); // new Cell("1") Pair p=new Pair(I,c2.get()); // pair[1,"1"] Object o=p.getReverse(); c1=(o instanceof Cell)?(Cell)o:null; // c1=null } }

Figure 2: An example of source code

information about the actual instantiation of the type variable T. Class Pair extends Cell and defines a getter and a setter method for the second element of the pair, together with a getReverse method returning a new pair where the two elements are inverted. Within Pair, the type Pair is one of those types whose type parameters depend on the current binding of the type variables R and S, that is a friend type of Pair. Hence in general, the type descriptor for Pair will contain in the field friends the type descriptor for Pair. Class Client uses some instantiation of the parametric classes Cell and Pair for type-dependent operations, such as the creation of class instances, instanceof tests and casts (i.e. type-conversion operations). Notice that our management has the only goal of allowing each parametric object to refer its corresponding type descriptor built at load-time, so as to support type-dependent operations in a fast way. The translation for the source code is given in Figure 3. The translation of generic classes Cell and Pair relies on the erasure technique [11]. This mainly means that all the occurrences of the type parameters in which they are not used for type-dependent operations are simply translated to Object3 . For instance, a class Vector is translated into a Vector class storing Object elements. Static controls and insertions of casts are used to support this kind of mapping (see the body of Client.Main in Figure 3). The translation technique is however unable to properly translate the occurrences of type variables used in typedependent operations [11]. LM translator differs from the standard erasure technique in that it translates this kind of occurrences into operations exploiting type descriptors. Within Cell and Pair the argument list of each constructor is augmented with a $TD argument containing the type descriptor for the current instantiation. This descriptor will be put into a newly-created instance field called $td, which can be accessed through the method getTD inherited from the interface $Parametric. Each parametric class provides then a static method called createTD, which can be used to register a type descriptor of the receiving class. This method checks for an alreadyregistered descriptor to exist, in this case this descriptor is simply returned. In the opposite case instead, a new descriptor is created and registered, and the fields father and friends are filled, possibly by recursively calling the method createTD of other generic classes. The type descriptor of a parametric type is used to implement type-dependent operations. For instance, its field param is used for the instance test exploited in the method Cell.setIfPossible, or to create the reversed pair in the method Pair.getReverse. When translating a client class, i.e. a class which uses instantiations of some parametric type, the translator should gather all these instantiations, and insert the code to register the corresponding type descriptors in the static initiliazer of this client. So, each client actually creates the type descriptors it will need immediately after its loading. This is clearly the case of class Client, which directly uses Cell and Pair, but also 3 More generally, dealing with bounded and f-bounded polymorphism, erasure technique translates a type-variable to its bound, which is Object by default. See [10] for more details.

of class Cell, which uses Cell. The registration of a generic type descriptor is realized through the corresponding createTD method, while that of a non-generic types by directly using the method $TDManager.register. These type descriptors are passed to the parametric objects when they are created, and will be used for implementing type-dependent operations over parametric types, namely instanceof tests and casts. This kind of operation is realized exploiting the method $TD.isInstance, shown in Figure 1. If the object o is not an instance of a parametric type the core reflective functionality Class.isInstance is used. In the opposite case the interface $Parametric is used to access the type descriptor of o. Then, through a navigation via the field $TD.father the subtyping relation is checked.

3.

PARAMETRIC METHODS

Parametric classes allow a programmer to define generic abstract data types independently from the value of other types, which are mapped to type parameters of the generic class. Any instance method of a parametric class is used to define an operation on the value represented by the receiver object, or at most an operation between two data values belonging to the same parametric type. Most times however, one may need to define an operation that does not involve two data values belonging to the same instantiation of the same generic class. With parametric classes only, the problem can be solved by defining a wrapper parametric class having both the type variables of the two generic classes involved in the operation. Considering the class Pair, suppose one wants to define an operation that takes two elements Pair and Pair and returns the pair Pair. A possible implementation is the following: class TwoPairs{ Pair p1; Pair p2; TwoPairs(Pair p1,Pair p2){ this.p1=p1;this.p2=p2; } Pair compose(){ return new Pair(p1.get(),p2.get()); } } ... Pair p1=... Pair p2=... Object o=new TwoPairs(p1,p2).compose();

This implementation is not satisfactory. First of all, it requires a new class to be defined for each operation over two different parametric classes. Moreover, it needs a new object to be created, storing the two elements involved in the operation. In principle, the latter drawback can be avoided by defining the operation as a static method and passing to it the two elements. Consider however that the semantics of static methods of parametric classes is not already standardized [12], so the right semantics for this case, which is that of heterogeneous-based implementations [3], may not be available. Parametric methods may help in coding the functionality in a more elegant way, as follows: class Pair{ ... Pair compose(Pair p2){ return new Pair(get(),p2.get());} }

public class Cell implements $Parametric { static $TD $cl; static{ $cl=createTD(new $TD[]{ $TDManager.register(String.class)}); } public static $TD createTD($TD[] params) { $TD t=$TDManager.register(Cell.class,params); if (t.checkNew()){ t.father=$TD.objTD; t.friends=$TD.voidTDs; } return t; } public $TD getTD() { return $td;} private $TD $td; private Object t; Cell($TD $td,Object t){ this.$td=$td; this.t=t; } void set(Object t){ this.t=t;} Object get(){ return t;} void setIfPossible(Object o){ if ($td.params[0].isInstance(o)) t=o; } Cell getCString(){ return new Cell($cl,""+t); } }

public class Pair extends Cell { public static $TD createTD($TD[] params) { $TD t=$TDManager.register(Pair.class,params); if (t.checkNew()){ t.father=Cell.createTD(new $TD[]{params[0]}); t.friends=new $TD[1]; t.friends[0]=Pair.createTD( new $TD[]{params[1],params[0]}); } return t; } public $TD getTD() {return $td;} private $TD $td; Object s; public Pair($TD $td,Object r,Object s) { super($td.father,r); this.$td=$td; this.s=s; } public void setSecond(Object s) { this.s=s; } public Object getSecond() { return s;} public Pair getReverse() { return new Pair($td.friends[0],s,get()); } }

public class Client { static $TD[] $t=new $TD[4]; static{ $t[0]=$TDManager.register(Integer.class); //Integer $t[1]=Cell.createTD(new $TD[]{$t[0]}); //Cell $t[2]=$TDManager.register(String.class); //String $t[3]=Pair.createTD(new $TD[]{$t[0],$t[2]});//Pair } public static void main(java.lang.String[] args) { Cell c1=new Cell($t[1],null); // new Cell(null); c1.set(new Integer(1)); Integer I=(Integer)c1.get(); c1.setIfPossible("2"); Cell c2=c1.getCString(); Pair p=new Pair($t[3],I,c2.get()); //new Pair(I,c2,get()); Object o=p.getReverse(); c1=$t[1].isInstance(o)?(Cell)o:null; // o instanceof Cell?... } }

Figure 3: Translation of the source code

Pair p1=... Pair p2=... Object o=p1.compose(p2);

Some examples of operation that may benefit from this kind of management are: appending two parametric lists, mapping a function to all the elements of a list, performing the cartesian product of two sets, and so on. The most straightforward implementation for parametric methods follows the idea of the implementations for parametric types based on macro-expansion. In particular, a new method is created for each different instantiation. Considering the code above, a compiler would create the following code: class Pair$String_String${ \\ each occurrence of type variables R and S \\ are substituted to String and String, then.. Pair$String_Double$ compose$Double_Double$ (Pair$Double_Double$ p2){ return new Pair$String_Double$(get(),p2.get()); } }

... Pair$String_String$ p1=... Pair$Double_Double$ p2=... Object o=p1.compose$Double_Double$(p2);

This implementation is space-consuming: the binary code of the program increases as different instantiations of the method are used. Even though some optimized solution (see for instance [3]) reduces the binary code created, the drawbacks remain: there is not limit in principle on the size of a class due to this kind of management. The problem is even more dramatic for the Java architecture in particular, which may rely on remote class loading. Here, the size of the class files is preferred to be fixed and as small as possible. In [9] it is sketched a translation for parametric methods into Java in which the Class object of each instantiation of a type variable is passed as argument. This proposal however has a number of problems, as the authors themselves pointed out: it does not address any performance issue, it does not allow for nesting parameter-

izations (as in Cell), and it does not integrate well with parametric classes and the core typing. Obviously, as mentioned in the introduction, the problem holds if one needs the full type-integration of parametric types in the core typing. In the opposite case in fact there is no need of carrying type information about the actual instantiation, and the erasure technique still remains a sufficient tool (see [11]). The importance of this integration has however been recognized in several papers [3, 13, 12].

4.

PARAMETRIC METHODS WITHOUT DYNAMIC DISPATCHING

Suppose not to deal with dynamic dispatching, i.e. having only parametric methods declared private, static or final, or anyway when they are not overridden by a more specific version into a sub-class. Then, the related implementation in the framework of LM translation can be built as follows. The basic idea is to manage method descriptors as well as type descriptors. They will be passed as the first argument of the parametric method, and they will contain all the information needed to deal with type-operations. While a type descriptor is identified by a Class object and an array of type descriptors representing type variables, a method descriptor will be identified by (i) the type descriptor of the parametric class within it is defined, (ii) an identifier of the method within the class and finally (iii) an array of type parameters representing the instantiations of the method type variables. So, for instance, we identify the method m with type parameter T, invoked to an instance of the parametric class C, with the notation C.m. Unlikely type descriptors, method descriptors have not to provide for subtyping, hence they do not have a super-descriptor field and they do not provide any unique identifier. In Section 2 we showed that a type descriptor, say t, may refer a set of type descriptors, that we call its friend types, whose binding of type parameters depend on the binding of the type variables of t. They were stored into the field t.friends. When dealing with parametric methods, this management is a little more complicate, as now the idea of binding dependency has to be extended. In particular, both parametric methods and parametric types will have friend types as well as friend methods. See the source code of Figure 4. Given two instantiations for R and S respectively, namely X and Y, the type descriptor for Pair has the friend type Pair. Moreover, due to the body of the method moveFirst, the type Pair has also the friend method Pair.chgSecond whose type variable instantiation depends on the current binding of the type variables of Pair. Analogously, given an instantiation Z for the type variable T, the method descriptor Pair.chgSecond has the friend type Pair, while Pair.chgFirst has the friend method Pair.chgSecond, since Z influences their actual instantiation. So, the signature of the class of method descriptors, called $MD, is the following: public class $MD { public int mID; // method identifier public $TD td; // type descriptor public $TD[] params; // method’s type variables public $TD[] friendTD; // friend types

public $MD[] friendMD; // friend methods public $MD($TD t, int mID, $TD[] p) { ... } public boolean checkNew() { ... } ... }

The class $TD has just to be changed by adding a field for friend methods, called friendMD, and renaming the field friends to friendTD. The class $MDManager, analogously to $TDManager, keeps track of all the method descriptors and provides a registration functionality that prevents double registrations. Its signature is: public class $MDManager { private static Hashtable h; static{ ... } //Initialization code public static $MD register ($TD t,int mID,$TD[] params){ ... } }

For brevity, we omit the actual implementation for both $MD and $MDManager. It is straightforward and moreover in Section 6 we will provide the final one, which is quite similar but deals with dynamic dispatching too. In Figure 4 the translation for this source code is also reported. When a class provides some parametric method, whether it is parametric or not, the translator inserts a createMD static method which handles the creation of method descriptors analogously to what method createTD does for type descriptors.4 A client class now in its static initializer has to create and register the method descriptors as well as type descriptors, and has to pass method descriptors when invoking parametric methods. When not dealing with dynamic dispatching it is possible to statically infer the instantiations of the parametric classes on which each parametric method is invoked. Thus, it is possible to prepare the static initializer of a client class in an appropriate way.

5.

DYNAMIC DISPATCHING

Consider the following code: class ExPair extends Pair{ ExPair(R r,S s){super(r,s);} Pair chgSecond(T t){ return new ExPair(r,t); } } ... class Client{ boolean dynamicTest(Pair p){ Object o=p.chgSecond(new Double(2)); return o instanceof ExPair; } }

The two method descriptors corresponding to the signatures ExPair.chgSecond and Pair.chgSecond contain different information. For instance, the former has the friend type ExPair while the second has the friend type Pair. These friend types are used to create instances of Pair and ExPair respectively. In particular, passing to the method dynamicTest an argument of type ExPair would cause the method to return a true value, while if the argument type is 4 In case of a non-generic class defining method descriptors, the translation accommodates it so that its type descriptor is automatically registered at load-time, and can be accessed in order to properly register the method descriptors.

class Pair extends Cell{ //Source S s; Pair(R r,S s){super(r); this.s=s;} Pair swap(){return new Pair(s,get());} Pair chgFirst(T t){ return swap().chgSecond(t).swap();} Pair chgSecond{T t}{ return new Pair(get(),t);} Pair moveFirst(){return chgSecond(get());} }

class Client{ //Source public boolean simpleTest(){ Pair p= new Pair(new Integer(1),"2"); Pair p2= p.changeFirst(new Double(1)); return (p2.reverse() instanceof Pair); } }

class Pair{ //Translation Object s; Pair(Object r,Object s){super(r); this.s=s;} Pair swap(){return new Pair($td.friendTD[0],s,get());} Pair chgSecond{$MD $m,Object t}{ return new Pair($m.friendTD[0],get(),t);} Pair chgFirst($MD $m,Object t){ return swap().chgSecond($m.friendMD[0],t).swap();} Pair moveFirst(){ return chgSecond($td.friendTD[0],get());} $TD $td; public $TD getTD(){ return $td; } static $TD createTD($TD[] params){...} static $MD createMD($TD t,int ID,$TD[] params){...} }

class Client{ //Translation public boolean simpleTest(){ Pair p=new Pair(t[3],new Integer(1),"2"); Pair p2=p.changeFirst(m[0],new Double(1)); return t[4].instanceOf(p2.getReverse()); } $TD[] t=new $TD[5]; $MD[] m=new $MD[1]; static{ t[0]=$TDManager.register(Integer.class); t[1]=$TDManager.register(String.class); t[2]=$TDManager.register(Double.class); t[3]=Pair.createTD(new $TD[]{t[0],t[1]}); t[4]=Pair.createTD(new $TD[]{t[1],t[2]}); m[0]=Pair.createMD(t[3],0,new $TD[]{t[2]}); } }

Figure 4: An example of code and translation with parametric methods when not dealing with dynamic dispatching Pair the result is false. This highlights that in the two cases different method bodies for chgSecond are actually executed, thus different method descriptors are needed. So, what kind of method descriptor should we pass at the invocation of chgSecond? This example shows that when dealing with dynamic dispatching it is not possible in general to statically know which method descriptor has to be passed and used by the method body. What we need is a technique allowing to dynamically select the appropriate method descriptor depending on the actual type descriptor of the receiver object. Doing so, we also require run-time overhead to be as small as possible5 . To avoid this problem, we use a technique roughly similar to a standard Virtual Method Table, used to deal with dynamic dispatching of non-parametric methods. If a class defines a parametric method that is not declared static, private or final, then the corresponding type descriptor will keep a table of parametric method descriptors for that method, a VPMT for short (Virtual Parametric Method Table). Instead of passing to a parametric method a method descriptor, the client passes the position of the descriptor within the corresponding VPMT, so that any access can be resolved dynamically in a fast way. Considering the example shown above, with this kind of management the descriptors for Pair and ExPair will have both two vector structures: one representing the VPMT for the method chgFirst and one for the method chgSecond. When a client registers the method chgSecond a new entry is inserted in both the VPMTs for chgSecond, at the same position. In particular, the method descriptor Pair.chgSecond is inserted in 5

In a trivial solution we would create the method descriptor by-need, but the corresponding overhead would be unacceptable.

the VPMT of Pair, while the method descriptor ExPair.chgSecond into that of ExPair. The result of the registration is the index in the VPMTs, which is the same in both the types Pair and ExPair. This index is passed at the method invocation-time, and is used to access the VPMT in the type descriptor of the receiver object.

6.

IMPLEMENTATION DETAILS

The final translation of class Pair and ExPair is reported in Figure 5, together with the translation for the following simple client class: class Client{ void main(){ Pair p= new Pair(new Integer(1),"2"); Pair p2= p.chgFirst(new Double(1)); ExPair e2= new ExPair(new Integer(1),"2"); Object o=e2.chgFirst(""); Object o2=e2.chgSecond(new Double(1)); }}

The implementation of library classes $MDManager and $MD is reported in Figure 6, along with the new signature for class $TD, which includes an array of Vector objects (representing VPMTs for each parametric method), and which uses for the field friendMD an array of int values (positions in the VPMTs) instead of one of $MD values. The implementation for $TDManager remains unchanged. From the Client point view, the translation is changed only in the information returned when registering a method descriptor. This is no more the descriptor itself, but just an int value representing its position within the method’s VPMT. Then, the first argument of the method register

class Pair extends Cell{ Object s; Pair($TD $td,Object r,Object s){ super($td.father,r); this.$td=$td;this.s=s;} Pair swap(){return new Pair($td.friendTD[0],s,get());} Pair chgSecond(int $p,Object t){ return new Pair((($MD)$td.$MDs[1].elementAt($p)) .friendTD[0],get(),t);} Pair chgFirst(int $p,Object t){ return swap().chgSecond((($MD)$td.$MDs[0]. elementAt($p)).friendMD[0],t).swap();} Pair moveFirst(){return chgSecond($td.friendMD[0],get());}

class ExPair extends Pair{ ExPair($TD $td,Object r,Object s){ super($td.father,r,s);this.$td=$td;} Pair chgSecond(int $p,Object t){ return new ExPair((($MD)$td.$MDs[1].elementAt($p)) .friendTD[0],get(),t);}

private $TD $td; public $TD getTD(){ return $td; } static public $TD createTD($TD[] params){ $TD t=$TDManager.register(Pair.class,params); if (t.checkNew()){ t.$MDs=new Vector[]{new Vector(),new Vector()}; t.father=Cell.createTD(new $TD[]{params[0]}); t.friendTD=new $TD[]{Pair.createTD( new $TD[]{params[1],params[0]})}; t.friendMD=new int[]{Pair.createMD( t,new Integer(1),new $TD[]{params[0]})}; } return t; } static public int createMD($TD t,Integer mID,$TD[] params){ int ID=mID.intValue(); $MD m=$MDManager.register(t,ID,params); if (m.checkNew()){ t.$MDs[ID].addElement(m); m.lPos=t.$MDs[ID].size()-1; if (ID==0){ //chgFirst m.friendMD=new int[]{createMD(t.friendTD[0], new Integer(1),new $TD[]{params[0]})}; } else if (ID==1){ //chgSecond m.friendTD=new $TD[]{createTD( new $TD[]{t.params[0],params[0]})}; } } $MDManager.propagate(m); } return m.lPos; }

$TD $td; public $TD getTD(){ return $td; } static public $TD createTD($TD[] params){ $TD t=$TDManager.register(ExPair.class,params); if (t.checkNew()){ t.father=Pair.createTD(params); t.$MDs=new Vector[]{new Vector(),new Vector()}; for (int i=0;i

Suggest Documents