Diagnosing Java code: Java generics without the pain, Part 1

A guide to generics in the Java Tiger version and the JSR-14 prototype compiler

This month's Diagnosing Java code introduces generic types and the features to support them scheduled for inclusion in Tiger, Java version 1.5, scheduled for release late in 2003. Eric Allen offers code samples that illustrate the ups and downs around generic types by focusing on such Tiger features as limitations on primitive types, constrained generics, and polymorphic methods. (Upcoming columns will discuss other features such as specific incarnations of generic types in Tiger and potential extensions to generic types beyond Tiger.) Share your thoughts on this article with the author and other readers in the discussion forum by clicking Discuss at the top or bottom of the article.

This article was updated to indicate that autoboxing has been added to the Java 1.5 spec.

Eric Allen (eallen@cs.rice.edu), Ph.D. candidate, Java programming languages team, Rice University

Eric Allen possesses a broad range of hands-on knowledge of technology and the computer industry. With a B.S. in computer science and mathematics from Cornell University and an M.S. in computer science from Rice University, Eric is currently a Ph.D. candidate in the Java programming languages team at Rice. Eric's research concerns the development of semantic models and static analysis tools for the Java language at the source and bytecode levels. He is also concerned with the verification of security protocols through semantic formalisms and type checking.
Eric is a project manager for and a founding member of the DrJava project, an open-source Java IDE designed for beginners; he is also the lead developer of the university's experimental compiler for the NextGen programming language, an extension of the Java language with added experimental features. Eric has moderated several Java forums for the online magazine JavaWorld. In addition to these activities, Eric teaches software engineering to Rice University's computer science undergraduates. You can contact Eric at eallen@cs.rice.edu.



20 May 2003 (First published 11 February 2003)

Also available in Japanese

J2SE 1.5 -- code-named Tiger -- is scheduled for release near the end of 2003. I'm always in favor of gathering as much advance information on upcoming technology as possible, so this article is the first in a series on the new and reformatted features available in version 1.5. Specifically, I'd like to talk about generic types and highlight the changes and tweaks in Tiger designed to support them.

In many ways, Tiger promises to be the biggest leap forward in Java programming so far, including significant extensions to the source language syntax. The most visible change scheduled to occur in Tiger is the addition of generic types, as previewed in the JSR-14 prototype compiler (which you can download right now for free; see Resources).

Let's start off with an introduction to what generic types are and what features are being added to support them.

Casts and errors

To understand why generic types are useful, we turn our attention to one of the most significant causes of bugs in the Java language -- the need to continually downcast expressions to datatypes more specific than their static types. (See "The Double Descent bug pattern," in Resources, for a discussion of some of the ways you can get into trouble with casts.)

Every downcast in a program is a potential hot spot for a ClassCastException and they should be avoided whenever possible. But they are often unavoidable in the Java language, even in very well-designed programs.

The most common reason to downcast in the Java language is that classes are often used in specialized ways that restrict the potential runtime types of arguments returned by method calls. For example, suppose we are adding and retrieving elements to and from a Hashtable. In a given program, the types of elements we use as keys, and the types of values we store in the hashtable, will not be arbitrary objects. Typically, all keys will be instances of a particular type. Similarly, the stored values will all share a common type more specific than Object.

But in the Java language versions that exist today, it is impossible to declare that the particular keys and elements of a hashtable have types more specific than Object. The type signatures on insertion and retrieval operations on hashtables tell us only that arbitrary objects are inserted and deleted. For example, the signatures of put and get operations are as follows:

class Hashtable { 
  Object put(Object key, Object value) {...} 
  Object get(Object key) {...} 
  ...
}

Thus, when we retrieve an element from an instance of class Hashtable, even if we know that we haven't put anything into that Hashtable but, say, Strings, the type system will only know that the retrieved value is of type Object. Before we can do anything String-specific with that retrieved value, we have to cast it to a String, even when the retrieved element was added in the same code block!

import java.util.Hashtable;

class Test { 
  public static void main(String[] args) { 
    Hashtable h = new Hashtable(); 
    h.put(new Integer(0), "value"); 
    String s = (String)h.get(new Integer(0)); 
    System.out.println(s); 
  } 
}

Notice the cast needed in the third line of the body of the main method. Because the Java type system is so weak, code tends to be riddled with casts like the one above. Not only do these casts make Java code wordier, they also diminish the value of static type checking (since each cast is a directive to selectively ignore static type checking). How can we extend the type system so that we don't have to circumvent it?


Generic types to the rescue!

A natural way to eliminate casts like the one above is to augment the Java type system with what are known as generic types. Generic types can be thought of as type "functions"; they are parameterized by type variables that can then be instantiated with various type arguments depending on context.

For example, instead of simply defining a class Hashtable, we could define a generic class Hashtable<Key, Value> in which Key and Value are type parameters. The syntax for defining such generic classes in Tiger is just like that for ordinary class definitions, except that the class name is followed by a sequence of type parameter declarations enclosed in angle brackets. For example, we could define our own generic Hashtable class as follows:

class Hashtable<Key, Value> { ... }

Then we can refer to these type parameters like we would ordinary types inside the body of the class definition, like this:

Listing 4. Referencing type parameters like ordinary types
class Hashtable<Key, Value> { 
  ... 
  Value put(Key k, Value v) {...} 
  Value get(Key k) {...} 
}

The scope of the type parameters is the body of the corresponding class definition, with the exception of static members. (In the next article, we'll discuss why a quirk of the Tiger implementation necessitates this restriction with static members. Stay tuned!)

When we create a new instance of a Hashtable, we have to pass type arguments to specify the types of Key and Value. How we do so depends on how we intend to use the Hashtable. In the example above, what we really wanted to do was to create an instance of a Hashtable that only mapped Integers to Strings. We could do so with our new Hashtable class:

import java.util.Hashtable;

class Test { 
  public static void main(String[] args) { 
    Hashtable<Integer, String> h = new Hashtable<Integer, String>(); 
    h.put(new Integer(0), "value"); 
    ...
  }
}

Now we don't need the cast anymore. Notice the syntax we've used to instantiate our generic class Hashtable. Just as the type parameters of a generic class are wrapped in angle brackets, the arguments of a generic type application are wrapped in angle brackets as well.

...
String s = h.get("key"); 
System.out.println(s);

Of course, it would be a significant amount of work for the programmer to have to redefine all of the standard utility classes -- such as Hashtable and List -- just to be able to use generic types. Luckily, Tiger provides users with generic versions of all of the Java collections classes, so we don't have to redefine them ourselves. What's more, these classes work seamlessly with both legacy code and new generic code (next month, we'll explain how that's possible).


Tiger's "primitive" limitation

Don't miss the rest of this series

Part 2, Extension limitations and implementation strategies (March 2003)

Part 3, Adding support for new operations (April 2003)

Part 4, Adding support for mixins through generic types (May 2003)

One limitation to type variables in Tiger is that they must be instantiated with reference types -- primitive types won't work. So, in the example above, if we wanted to instead create a Hashtable mapping ints to Strings, we couldn't do it.

That's unfortunate, because it means that you have to wrap primitive types whenever you want to use them as arguments to a generic type. On the other hand, that's no worse than the current situation; you can't pass an int as a key to Hashtable because all keys must be of type Object.

What we'd really like to see would be automatic boxing and unboxing of primitive types, similar to what is done in C# (except better). Unfortunately, Tiger is not scheduled to include autoboxing of primitives (but one can always hope for Java 1.6!).

Good news! After this article was written, autoboxing was added to the Java 1.5 spec!


Constrained generics

Sometimes we want to restrict the potential type instantiations of a generic class. In the above example, the type parameters of class Hashtable could be instantiated with any type arguments, which is what we'd like, but there are other classes where we will want to restrict the set of possible type arguments to subtypes of a given type bound.

For example, we may want to define a generic ScrollPane class that keeps a reference to an ordinary Pane that it decorates with scrolling functionality. The runtime type of the contained Pane will often be a subtype of class Pane, but the static type is simply Pane.

Sometimes we may want to retrieve the contained Pane with a getter, but we'd like the return type of the getter to be as specific as possible. We may want to add a type parameter MyPane to ScrollPane that can be instantiated with any subclass of Pane. Then we can place a bound on MyPane by annotating the declaration of MyPane with a clause of the form extends Bound:

class ScrollPane<MyPane extends Pane> { ... }

Of course, we could simply leave off the explicit bound and just make sure that we never instantiate the type parameter with an inappropriate type.

Why bother putting bounds on type parameters? There are a couple of reasons. First of all, the bounds give us added static type checking. With it, we're guaranteed that every instantiation of the generic type adheres to the bounds we place on it.

Second, because we know that every instantiation of the type parameter is a subclass of the bound, we can safely call any methods on an instance of the type parameter that appear in the bound. If we place no explicit bound on the parameter, then by default the bound is Object, meaning that we can't call any methods on an instance of the bound that don't appear in class Object.


Polymorphic methods

In addition to parameterizing classes by type parameters, it is often useful to parameterize a method by type parameters as well. In generic Java programming parlance, methods parameterized by type are called polymorphic methods.

The reason polymorphic methods are useful is that sometimes there will be operations we want to perform where the type dependencies between the arguments and the return value are naturally generic, but the generic nature doesn't rely on any class-level type information and will change from method call to method call.

For example, suppose we want to add a factory method to a List class. This static method would take a single argument, intended to be the sole element of the List (until others are added). Because we'd like our Lists to be generic in the type of element they contain, we'd like our static factory method to take an argument of type variable T and return an instance of List<T>.

But we'd really like this type variable T to be declared at the method level because it will change with every separate method call (also, as I will discuss in the next article, a quirk of the Tiger design dictates that static members are outside the scope of class-level type parameters). Tiger allows us to declare type parameters at the level of individual methods by prefixing them to method declarations. For example, we could do so for our factory method make as follows:

class Utilities {
   <T extends Object> public static List<T> make(T first) {
     return new List<T>(first);
   }
}

In addition to the added flexibility that polymorphic methods allow, there is an added benefit in Tiger. Tiger uses a type-inference mechanism to automatically infer the types of polymorphic methods based on the types of the arguments. This can greatly reduce the wordiness and complexity of a method call. For example, if we wanted to call our make method to construct a new instance of List<Integer> that contains an new Integer(0), we would simply write:

Utilities.make(Integer(0))

Then the type parameter instantiations would be inferred automatically from the method arguments.


Generically speaking

As we have seen, the addition of generic types to the Java language promises to greatly enhance our ability to leverage the static type system. Learning to use generic types is quite straightforward, but there are also pitfalls to be avoided. In the next few articles, we will discuss how to use to your advantage the particular incarnation of generic types that will appear in Tiger, as well as some of the pitfalls. We'll also examine the extensions to the generic Java type facilities that we can look forward to in versions of the Java platform still on the drawing boards.

Resources

  • Get a jump on generics in Java programming by downloading the JSR-14 prototype compiler (you must be a registered member of the Java Developer Connection). It includes the sources for a prototype compiler written in the extended language, a JAR file containing the class files for running and bootstrapping the compiler, and a JAR file containing stubs for the collection classes.
  • Eric Allen has a new book on the subject of bug patterns, Bug Patterns in Java (Apress, 2002), which presents a methodology for diagnosing and debugging computer programs by focusing on bug patterns, Extreme Programming methods, and ways to craft powerful testable and extensible software.
  • See "The Double Descent bug pattern" (developerWorks, April 2001) for a discussion of some of the ways you can get into trouble with casts.
  • IntelliJ's IDEA development environment -- which includes J2EE rapid Web-app development features, a powerful code inspection tool, and an Open API for third-party plug-in support -- is an "idea" worth examining.
  • And don't forget to try the high-performance code-analysis engine for both J2SE and J2EE development, CodeGuide from OmniCore. It already provides IDE support for generic types in Java code via the JSR-14 prototype compiler.
  • Martin Fowler's Web site contains much useful information about effective refactoring.
  • Examine seven principles to build a base for code design with testing in mind in "Designing 'testable' applications" (developerWorks, September 2001).
  • Explore the developerWorks repository of Eric Allen's columns -- from bug patterns to testability to design strategies -- in the Diagnosing Java code columns roundup.
  • Follow the discussion of adding generic types to Java code by reading the Java Community Process proposal, JSR-14.
  • The paper "Automatic Code Generation from Design Patterns" (PDF) from IBM Research describes the architecture and implementation of a tool that automates the implementation of design patterns.
  • These two articles in the Diagnosing Java code series can bolster your knowledge of generic types and the Java type system: "Killer combo -- Mixins, Jam, and unit testing" (December 2002) and "The case for static types" (June 2002).
  • Find hundreds more Java technology resources on the developerWorks Java technology zone.

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Java technology on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=10002
ArticleTitle=Diagnosing Java code: Java generics without the pain, Part 1
publish-date=05202003