University of Virginia, Department of Computer Science
CS655: Programming Languages
Spring 2000

Project Preliminary Report
CS655 Group 4
20 January 2000

Java ParTy: Parameterized Types in Java

 

Description of the Problem

 

In Java, a programmer who wants to define a group of related classes that have similar behavior but manipulate different types cannot easily do so. An ideal solution would allow the programmer to define a generic abstraction of the class, and then allow the class to adapt according to the type with which it is instantiated. This solution would require only one generic class to be written for a potentially large number of different instantiations, greatly reducing the amount of code that needs to be written. In addition, the ideal solution would still be able to guarantee type safety before run time, so it would not introduce any new type errors.

Unfortunately, Java does not provide any explicit method to achieve this kind of genericity. However, the language was intended to be extendable as the need for new programming power arises. We believe that Java users would benefit from the additional power of generic abstractions in code, and in our project, we design and implement an extension to Java that adds support for parametric polymorphism via parameterized types.

There have already been several attempts at offering support for parameterized types in Java (PolyJ, Pizza, GJ, and others). Each of these previous attempts is cleverly designed to offer complete support for all flavors of parametric polymorphism. Accommodating all possible scenarios of generic abstractions is a difficult programming task, and the current extensions handle these problems quite well.

Instead of trying to provide a better version of the same complete extension, we take a different approach. Firstly, we have examined what properties of parameterized types are most valuable to Java users. We wish to answer the question: What makes parameterized types valuable to programmers? We believe that determining the usefulness of the extension is of primary importance, and we will then use this knowledge to guide our design, rather than attempting to provide full support first, and then reflect on its usefulness after the implementation is complete.

After choosing what makes parameterized types valuable to programmers, we will finally design and implement a limited version of these features. We aim to add only the most useful extensions to Java, and will not add complexity to the language unless its usefulness outweighs its complexity.

 

Related Work

In our consideration of related work, we examined both the existing support for type parameterization in other languages, and the current work done on Java to support parameterized type extensions. We chose to examine the existing support in languages other than Java in order to deduce exactly what features of type parameterization were desirable ones. The languages that already support generic types generally have a large amount of code written that takes advantage of it. From examining this code, we were better able to decide which supported features could be considered useful, and which supported features were not. The languages under consideration included C++, Ada, Sather, Eiffel, and ML.

We chose to examine the currently proposed extensions so that we could better understand the unique challenges in adding type parameterization to the Java programming language. Although we reviewed a large amount of work in this area, we concentrated our efforts on examining PolyJ and GJ, two separately proposed extensions that are among the top proposals currently being considered by Sun Microsystems [1].

 

Parameterized Types in other Programming Languages

C++ 

C++ supports type parameterization through the use of templates. Templates were added to C++ in 1989 with the release of C++ 2.0, and were not a part of the original language design [A History of C++]. A template definition begins with the keyword template, and is followed by a parameter list. Templates can be used in function declarations as well as class declarations. The parameter list of a template definition can include both type and value information. The type information allows a particular type to be passed into the corresponding function or class so that it can be properly instantiated for that type. The value information is the same as a function value parameter, and contains a value of a known type. Although value template parameters are supported by C++ templates, they are rarely used [2].

Templated classes are instantiated heterogeneously, so that a copy of each class exists for each appropriate type that could be passed as a parameter to the template. This allows C++ to appear to have dynamic type binding when it actually provides static binding, and allows C++ to provide type parameterization without sacrificing code execution speed. However, it causes an increase in code size.

Templates do not impose any restrictions on what a type must provide in order for it to be an appropriate candidate as a type parameter. Thus, a templated class that attempts to use a method of a type for which the method does not exist can cause a run-time error. Other languages avoid this problem by including type constraints in the declaration of the templated class that specify necessary features of the candidate type.

Templates support primitive types as well as user-defined types as type parameters. In fact, a type parameter can be another templated class itself. A class nested inside another class can share the same templated type information. Also, declarations of a templated class can implicitly include type information, based on other parameters, and can avoid requiring the programmer to explicitly declare the parameterized type.

In general, C++ templates were added to provide a maximum amount of flexibility on top of an already existing language. Although flexible, some of the features of templates were excluded from our design decisions because we did not consider them necessary to genericity.

 

ML

ML has a polymorphic type system. As an example, consider the following function (the identity function):

 fun f x = x;

This function really doesn't care what the type of x is. In other words,

 - f 1;

val it = 1 : int

- f 1.0;

val it = 1.0 : real

- f [1,2,3];

val it = [1,2,3] : int list

- f "Hi!";

val it = "Hi!" : string

-

ML requires that every expression be properly typed. ML infers the most general type for an expression, and uses type variables whenever the type is not uniquely determined by the expression. Type variables are denoted by variable names which prefixed by the quote character: '.

For the function f defined above, ML recognizes that it is not specific to any one type, and assigns the function f a polymorphic type:

 val f = fn : 'a -> 'a

If a single function accepts arguments of different types, then it is called polymorphic. The identity function above is a perfect example. Another example is a function which counts the number of elements in a list:

 fun g ([]) = 0

| g (hd::tl) = 1+g(tl);

This function works with lists of any type, so ML infers that its type must be:

 val g = fn : 'a list -> int

Here are some examples of its use:

 - g [1,2,3,4,5];

val it = 5 : int

- g [[],[],[],[]];

val it = 4 : int

[3]

 

ADA

The generic mechanism in ADA allows a subprogram or package to be parameterized by types and subprograms as well as normal in parameters. For instance:

 

GENERIC

TYPE ValueType IS PRIVATE;

WITH FUNCTION Compare( L, R : ValueType) RETURN Boolean;

FUNCTION Maximum_Generic(L, R : ValueType) RETURN ValueType;

-- Specification for generic maximum function

 

FUNCTION Maximum_Generic(L, R : ValueType) RETURN ValueType IS

-- Body of generic maximum function

Begin

IF Compare(L, R) THEN

RETURN L;

ELSE

RETURN R;

END IF;

END Maximum_Generic;

 

FUNCTION FloatMax IS

NEW Maximum_Generic (ValueType => Float, Compare => ">");

-- An instantiation for Float values

[4][5] 

 

Haskell

Haskell also incorporates polymorphic types---types that are universally quantified in some way over all types. Polymorphic type expressions essentially describe families of types. For example, (for all a)[a] is the family of types consisting of, for every type a, the type of lists of a. Lists of integers (e.g. [1,2,3]), lists of characters (['a','b','c']), even lists of lists of integers, etc., are all members of this family. (Note, however, that [2,'b'] is not a valid example, since there is no single type that contains both 2 and 'b'.)

[Identifiers such as a above are called type variables, and are uncapitalized to distinguish them from specific types such as Int. Furthermore, since Haskell has only universally quantified types, there is no need to explicitly write out the symbol for universal quantification, and thus we simply write [a] in the example above. In other words, all type variables are implicitly universally quantified.]

Lists are a commonly used data structure in functional languages, and are a good vehicle for explaining the principles of polymorphism. The list [1,2,3] in Haskell is actually shorthand for the list 1:(2:(3:[])), where [] is the empty list and : is the infix operator that adds its first argument to the front of its second argument (a list). (: and [] are like Lisp's cons and nil, respectively.) Since : is right associative, we can also write this list as 1:2:3:[].

As an example of a user-defined function that operates on lists, consider the problem of counting the number of elements in a list:

length :: [a] -> Int

length [] = 0

length (x:xs) = 1 + length xs

This definition is almost self-explanatory. We can read the equations as saying: "The length of the empty list is 0, and the length of a list whose first element is x and remainder is xs is 1 plus the length of xs." (Note the naming convention used here; xs is the plural of x, and should be read that way.)

Although intuitive, this example highlights an important aspect of Haskell that is yet to be explained: pattern matching. The left-hand sides of the equations contain patterns such as [] and x:xs. In a function application these patterns are matched against actual parameters in a fairly intuitive way ([] only matches the empty list, and x:xs will successfully match any list with at least one element, binding x to the first element and xs to the rest of the list). If the match succeeds, the right-hand side is evaluated and returned as the result of the application. If it fails, the next equation is tried, and if all equations fail, an error results.

Defining functions by pattern matching is quite common in Haskell, and the user should become familiar with the various kinds of patterns that are allowed. 

length is also an example of a polymorphic function. It can be applied to a list containing elements of any type. For example: 

length [1,2,3] =>3

length ['a','b','c'] => 3

length [[],[],[]] => 3

Here are two other useful polymorphic functions on lists that will be used later:

head :: [a] -> a

head (x:xs) = x

tail :: [a] -> [a]

tail (x:xs) = xs

With polymorphic types, we find that some types are in a sense strictly more general than others. For example, the type [a] is more general than [Char]. In other words, the latter type can be derived from the former by a suitable substitution for a. With regard to this generalization ordering, Haskell's type system possesses two important properties: First, every well-typed expression is guaranteed to have a unique principal type (explained below), and second, the principal type can be inferred automatically (see type-semantics). In comparison to a monomorphically typed language such as Pascal, the reader will find that polymorphism improves expressiveness, and type inference lessens the burden of types on the programmer.

An expression's or function's principal type is the least general type that, intuitively, "contains all instances of the expression." For example, the principal type of head is [a]->a; the types [b]->a, a->a, or even a are too general, whereas something like [Int]->Int is too specific. The existence of unique principal types is the hallmark feature of the Hindley-Milner type system, which forms the basis of the type systems of Haskell, ML, Miranda, and several other (mostly functional) languages [6].

 

Proposals for Parameterized Types in Java

GJ, or Generic Java, was designed in 1998 by Gilad Bracha of JavaSoft, Martin Odersky of the University of South Australia, David Stoutamire of JavaSoft, and Philip Wadler of Bell Labs, Lucent Technologies. It was originally based on the handling of parametric types in Pizza [7], but uses a simpler type system, and provides greater support for backwards compatibility. All GJ code translates into normal Java virtual machine (JVM) code. The designers chose this method over modifying Java bytecodes because it preserves the safety and security of the Java platform, and would allow for forwards and backwards compatibility with previously existing Java code.

GJ programs look much like the equivalent Java programs, except they have more type information and fewer casts. The GJ compiler erases type parameters, replaces the type variables by their bounding type (typically Object), and adds the appropriate type casting. Thus, generic code written in GJ is translated into code for type Object, and casts are used to appropriately modify the code for each desired type. This method allows for homogeneous genericity, and avoids the code size "blowup" that occurs in heterogeneous implementations like the one found in C++ [8].

The method used by GJ to support parameterized types could just as easily be done by programmers themselves. By allowing the GJ compiler to handle type casting instead of requiring the programmer to do it, GJ pushes the possibility of run-time type errors into compile time, adding to type safety. However, GJ is really a hack that does not change the underlying structure of Java to accommodate for parameterized types in any way. Because of this, GJ suffers from run-time type casting, usually between the desired type and type Object. This causes a significant decrease in code execution speed.

 

PolyJ

PolyJ was designed in 1997 by Andrew C. Myers, Joseph A. Bank and Barbara Liskov of Massachusetts Institute of Technology. PolyJ is an extension to Java that supports constrained parametric polymorphism. It allows abstractions to be parameterized so a single piece of code can implement many related abstractions.

PolyJ chose to use where clauses, which is quite the same concept in CLU as we studied before, as the mechanism for constraining parameterized types. 'where' clauses allow the programmer to state that a class conforms to a certain constraint without explicitly declaring the relationship when the class is defined. For example, in a Hashtable class example, in which it is necessary to obtain a hashCode for each object inserted, where clauses would allow the programmer to use any class that has a hashCode method. While other projects such as Pizza and the following Stanford project would require the programmer to use only those classes that implement the Hashable interface (assuming Hashable interface has the method hashCode).

The main implementation described in PolyJ for where clauses changes the bytecodes and virtual machine significantly in order to allow shared code, but separate constant pool information, among instantiations of a parameterized class. This makes the implementation more costly and the virtual machine more complicated. Any change to the bytecodes and bytecode verifier will require all safety and security aspects of the system to be reevaluated.

 

Stanford University Project

This project was done by Ole Agesen of Sun Microsystems Lab, Stephen N. Freund and John C. Mitchell of CS in Stanford University. It uses only type relations implements and extends, which are familiar to Java programmers and essentially similar to the extension considered in Pizza and GJ, to extend a parameterized type for the Java programming language. However, this mechanism was not so powerful as the where clauses does in PolyJ.

In comparison with other projects described here, the main advantage of this project is its implementation based on inserting a preprocess step into the load phase of the Java Virtual Machine. By delaying instantiation until load time, we are able to achieve an appropriate balance between language expressiveness, run-time efficiency and compiled code size. This will be easy for Java programmers to adopt and use effectively, and interfere minimally with any current or future developments in Java implementation.

Actually, our extension of parameterized type for Java will combine both the design advantage of where clauses in PolyJ and the implementation advantage of Load-time preprocessor mechanism.

 

Proposed Solution

Semantics: Type Parameters

Type parameterization is a kind of polymorphism that can be used to implement generic programming at compile-time and thus incurs no run-time overhead. It is a kind of polymorphism. Although the complete class must be reinstantiated for each different type used with a parameterized class, rather than merely reuse of the same code in the way of deriving, it provides convenience to the programmer and obtains more type safety and run-time efficiency.

Different languages have different ideas about what information should be passed as parameterized type information. Some require one and only one parameter: the type. C++ allows value information to be included along with type information. Multiple type parameters can be passed as well. The type parameter can itself be a parameterized type in certain instances. Also, in some languages, type information can be removed in cases where the compiler can implicitly deduce it from other information.

We chose not to allow value information to be coupled with type information in the parameters, opposing C++-style parameterization. Encapsulating value information with type information adds no new power to the generic code-any value information can just as easily be passed as a functional parameter through a constructor. Rather, this encapsulation is harmful because it blurs the distinction between type and value parameters, and we prefer this distinction to be clear.

 For example, this declaration:

template<class T, int i> T* anyArray() {

return malloc(sizeof(T) * i);

}

could have just as easily been written as:

template<class T> T* anyArray( int i ) {

return malloc(sizeof(T) * i);

}

and the latter version maintains a healthy separation of type and value parameterization.

We chose to allow multiple type parameters to be passed in a single instantiation of a parameterized class. In addition, we chose to allow type parameters to themselves be of a parameterized type. This is necessary for any non-trivial application of generic classes, and is why we included this feature. One could easily imagine a large-scale application that takes advantage of passing parameterized classes as type parameters.

We chose not to require explicit type information to be supplied when it is not necessary. This adds programmer flexibility in that a programmer does not have to explicitly declare what type will be instantiated at compile time if this information is clearly available elsewhere. This feature has proven to be useful in C++. For instance, consider this templated function:

template<class T, class U> T implicit_cast(U u)

{

return u;

}

int g(int i)

{

//implicit_cast(i); // (1) error: cannot deduce T

implicit_cast<double> (i); // (2) T is double, U is int

implicit_cast<char, double>(i);// (3) T is char, U is double

implicit_cast<char*, int>(i); // (4) T is char*, U is int,

// error: cannot convert

}

This demonstrates the use of multiple type parameters, as well as implicit type parameterization. In (2), the type of U is inferred from the type of i. This is a valuable feature that allows for more programmer flexibility.

 

Semantics: Type Constraints

"Where" clauses, such as those used in CLU and PolyJ, allow the programmer to state that a class conforms to a certain constraint without explicitly declaring the relationship when the class is defined. Basically, the information in the where clause serves to isolate the implementation of a parameterized definition from its uses (i.e., instantiations). Thus, the where clauses permit separate compilation of parameterized implementations and their uses, which is impossible in C++ and Modula3. We have chosen to support the where clause in our implementation for such reasons.

In implementing where clauses, the compiler not only checks whether a type t has particular method m declared in the where clause, but also checks the type of the arguments of m. It verifies whether a hypothetical call to m will succeed. Though CLU provided the ability of applying additional constraints to individual methods, the ability also adds substantial complexity to the design and implementation. We omit it in our implementation. The following is an example of additional constraints that we will not support:

interface SortedList{T}

Where T { boolean lt (T t); }

{

...

void output(OutputStream s) // there is additional constraint to // this method

where T { void output (OutputStream s); }

// effects: Send a textual representation of this to s.

}

 

Semantics: Nesting

We plan to allow the nesting of templated classes in Java, as this is a popular feature in many applications (especially large-scale ones). The rules of nesting follow the scoping rules of names in Java. All the parameterized types used within a class must be declared explicitly at the beginning of the class. Recursive definitions of parameterized class in the following form is allowed.

class <T> <U> X{

class <T> X x;

class <U> Y y;

boolean m(x);

}

Finally, we will support classes with type parameterization as well as methods within a class that use type parameterization.

 

Implementation: Homogeneous and Heterogeneous Code Translation

There are two ways in which Java may simulate parametric polymorphism - heterogeneous and homogeneous translations. A heterogeneous translation produces a specialized copy of code for each type at which it is used. A homogeneous translation uses a single copy of the code with a universal representation. We give an example to illustrate the difference of these two methods.

Consider an algorithm to swap a pair of elements, both of the same type. Ignoring the syntax problem, the code for this task appears in the following:

class Pair<elem>{

elem x; elem y;

Pair(elem x, elem y) { this.x = x; this.y = y;}

Void swap(){ elem t = x; x = y; y = t;}

}

Pair<String> p = new Pair("world!", "Hello");

p.swap();

System.out.println(p.x + p.y);

 

Pair<int> q = new Pair(22, 64);

q.swap();

System.out.println(q.x - q.y);

The class Pair takes a type parameter elem. A pair has two fields x and y, both of type elem. The constructor Pair takes two elements and initializes the fields. The method swap interchange the field contents, using a local variable t also of type elem. The test code at the end creates a pair of integers and a pair of strings, and prints

Hello world!

42

to the standard output.

The first method to simulate it is to macro expand a new version of the Pair class for each type at which it is instantiated. We call this the heterogeneous translation, and it is shown in the following:

class Pair_String{

String x; String y;

Pair(String x, String y) { this.x = x; this.y = y;}

Void swap(){ String t = x; x = y; y = t;}

}

class Pair_int{

int x; int y;

Pair(int x, int y) { this.x = x; this.y = y;}

Void swap(){ int t = x; x = y; y = t;}

}

Pair_String p = new Pair_String("world!", "Hello");

p.swap();

System.out.println(p.x + p.y);

 

Pair_int q = new Pair_int(22, 64);

q.swap();

System.out.println(q.x - q.y);

 

The appearance of parameterized classes Pair<String> and Pair<int> causes the creation of the expanded classes Pair_String and Pair_int, within which each occurrence of the type variable elem is replaced by the types String and int, respectively.

The second method is to replace the type variable elem by the class Object, the top of the class hierarchy. We call this the homogeneous translation, and it is shown in the following:

 class Pair{

Object x; Object y;

Pair(Object x, Object y) { this.x = x; this.y = y;}

Void swap(){ Object t = x; x = y; y = t;}

}

class Integer{

int i;

Integer( int i) { this.i = i; }

int intValue() { return i; }

}

 

Pair p = new Pair ((Object)"world!", (Object)"Hello");

p.swap();

System.out.println((String)p.x + (String)p.y);

 

Pair q = new Pair ((Object)new Integer(22),

(Object)new Integer64));

q.swap();

System.out.println(((Integer)(q.x)).intValue() -

((Integer)(q.y)).intValue());

 

The key to this translation is that a value of any type may be converted into type Object, and later recovered. Every type in Java is either a reference type or one of the eight base types, such as int. Each base type has a corresponding reference type, such as Integer, the relevant fragment of which appears in the above program.

If v is a variable of reference type, say String, then it is converted into a object o by widening (Object)s, and converted back by narrowing (String)o. If v is a value of base type, say int, then it is converted to an object o by (Object)(new Integer(v)), and converted back by ((Integer)o).intValue(). (In Java, widening may be implicit, but we write the cast (Object) explicitly for clarity.)

In our implementation, we will use the heterogeneous style of translation. Typically the heterogeneous translation yields code that runs faster, while the homogeneous translation yields code that is more compact. The heterogeneous method will have some memory consumption increase at run time compared with a homogeneous method, because a new class is generated for each instantiation. The memory increases proportional to the number of instantiations of the parameterized class. Currently we have no exact idea of to what extent parameterized types will be used in typical Java applications, but we expect that the memory increase will be small relative to the memory requirements for the system as a whole. This expectation seems to be confirmed by other systems, such as C++ and Ada, that use heterogeneous method of parameterization mechanisms.

 

Implementation: Load-time Preprocessor

To set the context for a discussion of our implementation technique, the following figure shows a schematic diagram of the Java Virtual Machine:

 

 

The load-time preprocessor is a techniques that adds a preprocess phase to the Java Virtual Machine loader. Parameterized classes can be implemented by using a heterogeneous implementation based on load-time expansion. The basic ideas are:

 

  1. Compiling a parameterized class into an extended form of the class file. In addition to all the information usually found in a class file, the extended file includes information about parameters and constraints.
  2.  

  3. When JVM attempts to load an instantiation of a parameterized class, a loader preprocess phase transforms the parameterized class file into the desired instantiation and then declares it as if it were a normal class. In other words, by using heterogeneous method, one "template" class file is used to generate regular, non-parameterized Class objects for each instantiation of a parameterized class.
  4.  

  5. By expanding parameterized classes to a form used by non-parameterized classes allows current Java Virtual Machine verifiers, interpreters, and just-in-time compilers to remain unchanged. In addition to retaining compatibility, we also avoid security and other issues that would arise if we changed the Java execution model. Postponing expansion from compile time to load time reduces the size of compiled code. This could be significant when compiled code is transmitted over a network.

  

Research Plan

Our implementation will be based on heterogeneous model. In this model, it will be easy to implement the use of primitive types as type parameters, efficiently eliminating the overhead of explicit type casting and run-time type checking. The heterogeneous implementation also makes the where clause constraint mechanism possible. We considered the tradeoff between programmer convenience, run-time efficiency, and implementation complexity, and chose to implement:

  1. both parameterized classes and parameterized methods
  2. using regular class types, primitive types, and parameterized types as the parameters of parameterized classes or methods
  3. implicit deduction from the concrete type parameters to the types they denote
  4. nesting, as explained before
  5. type checking which follows Java's type equivalence based on the inheritance relationship
  6. and of course, user-defined parameterized methods/classes

 

Evaluation Plan

The goal of this project is to allow common parameterized classes to be programmed in Java. In this project, we are most interested in making it easy to handle common, useful cases rather than maximizing absolute language expressiveness. The language extension should be implemented so as try to preserve Java's code executing efficiency without changing Java's robustness.

In our evaluation, we plan on determining the usefulness of a limited extension to Java for parameterized type support. We wish to show where our implementation provides useful extensions to the language, and where it fails to do so.

In order to systematically show where our extension is most beneficial, we will examine other existing code that uses type parameterization. We will then attempt to convert these examples into our ParTy language. We hope to show that the majority of cases will be convertible to ParTy code, and that the minority that cannot be converted are rare exceptions that are not often used in everyday programming.

We hope that our extensions will be most valuable to users, and we hope to show that the extensions we do not support are also extensions that are not necessary tools for most programmers.

Also, we will measure the performance cost of our implementation, and compare it with that of previous ones. In this project, we run the risk of not adding any performance benefits above any existing extensions. If our performance benefits are found to be insufficient, we will conduct a performance analysis, and list methods by which we could improve our implementation further. However, the speed of code execution is of secondary importance to our primary goal of providing only the most valuable extensions.

 

Schedule and Division of Labor

Our next short term goal is to formally design the syntax and semantics of the language extensions. Next, we will begin our implementation. Hexin Wang will work on load-time preprocessor issues specifically, and the rest of the group will work on analyzing GJ and PolyJ specifications. We will then use these examples to build our implementation.

We hope to have a working implementation finished by April 24th. This is an ambitious goal. In the case that we are not able to complete the implementation, we will still have a very good set of ideas about language design for such an extension, and we will focus on describing these goals in our final report.

 

References in the Preliminary Report

[1] Sun web page excerpt

[2] Cohoon and Davidson. C++ Program Design.

[3] http://www.ugrad.cs.ubc.ca/spider/cs312/Lectures/CS312_12.html

[4] An overview of Ada

[5] Ada 95 Problem Solving and Program Design

[6] http://www.cosc.canterbury.ac.nz/~wolfgang/122-97/haskellTut.html

[7] OW97

[8] GJ: Extending Java with Type Parameters

 


University of Virginia
CS 655: Programming Languages