|
|
|
Contents: |
|
|
|
Related content: |
|
|
|
Subscriptions: |
|
|
| A guide to generics in the Java Tiger version and the JSR-14
prototype compiler
Eric
E. Allen (mailto:eallen@cs.rice.edu?cc=&subject=Java
generics without the pain, Part 1) Ph.D. candidate, Java
programming languages team, Rice University 11 February 2003 Updated
20 May 2003
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.
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, String s, 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
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 int s to String s, 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 List s 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
- Participate in the discussion forum on this
article. (You can also click Discuss at the top or bottom of the
article to access the forum.)
- 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.
- Keith Turner offers another look at this topic in "Catching
more errors at compile time with Generic Java"
(developerWorks, March 2001).
- 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.
About the
author 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. |
|
|
|
|