Sign In/My Account | View Cart |
O'Reilly Book Excerpts: Java in a Nutshell, 5th Edition
Editor's note: Java in a Nutshell, 5th Edition covers the extensive changes and new features in 5.0, chief among them generic types. In part one of this two-part excerpt, author David Flanagan walks through how to use generic types, and in part two next week, he covers how to write your own generic types and generic methods.
Generic types and methods are the defining new feature of Java 5.0. A
generic type is defined using one or more type
variables and has one or more methods that use a type variable as a placeholder
for an argument or return type. For example, the type
java.util.List<E>
is a generic type: a list
that holds elements of some type represented by the placeholder
E
. This type has a method named
add()
, declared to take an argument of type
E
, and a method named get()
,
declared to return a value of type E
.
Related Reading ![]() Java in a Nutshell |
In order to use a generic type like this,
you specify actual types for the type variable (or variables),
producing a parameterized
type such as
List<String>
.
[1]
The reason to specify this extra type information is that
the compiler can provide much stronger compile-time type checking for
you, increasing the type safety of your programs. This type checking
prevents you from adding a String[]
, for example,
to a List
that is intended to hold only
String
objects. Also, the additional type
information enables the compiler to do some casting for you. The
compiler knows that the get( )
method of a
List<String>
(for example) returns a
String
object: you are no longer required to cast
a return value of type Object
to a
String
.
The
collections
classes of the java.util
package have been made
generic in Java 5.0, and you will probably use them frequently in
your programs. Typesafe collections are the canonical use case for
generic types. Even if you never define generic types of your own and
never use generic types other than the collections classes in
java.util
, the benefits of typesafe collections
are so significant that they justify the complexity of this major new
language feature.
We begin by exploring the basic use of generics in typesafe collections, then delve into more complex details about the use of generic types. Next we cover type parameter wildcards and bounded wildcards. After describing how to use generic types, we explain how to write your own generic types and generic methods. Our coverage of generics concludes with a tour of important generic types in the core Java API. It explores these types and their use in depth in order to provide a deeper understanding of how generics work.
The java.util
package includes the Java
Collections Framework for working with sets and lists of objects and
mappings from key objects to value objects. Collections are covered
in Chapter 5. Here, we discuss the fact that in
Java 5.0 the collections classes use type parameters to identify the
type of the objects in the collection. This is not the case in Java
1.4 and earlier. Without generics, the use of collections requires
the programmer to remember the proper element type for each
collection. When you create a collection in Java 1.4, you know what
type of objects you intend to store in that collection, but the
compiler cannot know this. You must be careful to add elements of the
appropriate type. And when querying elements from a collection, you
must write explicit casts to convert them from
Object
to their actual type. Consider the
following Java 1.4 code:
public static void main(String[] args) {
// This list is intended to hold only strings.
// The compiler doesn't know that so we have to remember ourselves.
List wordlist = new ArrayList();
// Oops! We added a String[] instead of a String.
// The compiler doesn't know that this is an error.
wordlist.add(args);
// Since List can hold arbitrary objects, the get() method returns
// Object. Since the list is intended to hold strings, we cast the
// return value to String but get a ClassCastException because of
// the error above.
String word = (String)wordlist.get(0);
}
Generic types solve the type safety problem illustrated by this code.
List
and the other collection classes in
java.util
have been rewritten to be generic. As
mentioned above, List
has been redefined in terms of a type
variable named E
that represents the type of the
elements of the list. The add( )
method is
redefined to expect an argument of type E
instead
of Object
and get( )
has been
redefined to return E
instead of
Object
.
In Java 5.0, when we declare a List
variable or
create an instance of an ArrayList
, we specify the
actual type we want E
to represent by placing the
actual type in angle brackets following the name of the generic type.
A List
that holds strings is a
List<String>
, for example. Note that this is
much like passing an argument to a method, except that we use types
rather than values and angle brackets instead of parentheses.
The elements of the java.util
collection classes
must be objects; they cannot be used with primitive values. The
introduction of generics does not change this. Generics do not work
with primitives: we can't declare a
Set<char>
, or a
List<int>
for example. Note, however, that
the autoboxing and autounboxing features of Java 5.0 make working
with a Set<Character>
or a
List<Integer>
just as easy as working
directly with char
and int
values. (See Chapter 2 for details on
autoboxing and autounboxing).
In Java 5.0, the example above would be rewritten as follows:
public static void main(String[] args) {
// This list can only hold String objects
List<String> wordlist = new ArrayList<String>();
// args is a String[], not String, so the compiler won't let us do this
wordlist.add(args); // Compilation error!
// We can do this, though.
// Notice the use of the new for/in looping statement
for(String arg : args) wordlist.add(arg);
// No cast is required. List<String>.get() returns a String.
String word = wordlist.get(0);
}
Note that this code isn't much shorter than the
nongeneric example it replaces. The cast, which uses the word
String
in parentheses, is replaced with the type
parameter, which places the word String
in angle
brackets. The difference is that the type parameter has to be
declared only once, but the list can be used any number of times
without a cast. This would be more apparent in a longer example. But
even in cases where the generic syntax is more verbose than the
nongeneric syntax, it is still very much worth using generics because
the extra type information allows the compiler to perform much
stronger error checking on your code. Errors that would only be
apparent at runtime can now be detected at compile time. Furthermore,
the compilation error appears at the exact line where the type safety
violation occurs. Without generics, a
ClassCastException
can be thrown far from the
actual source of the error.
Just as methods can have any number of arguments, classes can have
more than one type variable. The java.util.Map
interface is an example. A
Map
is a mapping from key objects to value
objects. The Map
interface declares one type
variable to represent the type of the keys and one variable to
represent the type of the values. As an example, suppose you want to
map from String
objects to
Integer
objects:
public static void main(String[] args) {
// A map from strings to their position in the args[] array
Map<String,Integer> map = new HashMap<String,Integer>();
// Note that we use autoboxing to wrap i in an Integer object.
for(int i=0; i < args.length; i++) map.put(args[i], i);
// Find the array index of a word. Note no cast is required!
Integer position = map.get("hello");
// We can also rely on autounboxing to convert directly to an int,
// but this throws a NullPointerException if the key does not exist
// in the map
int pos = map.get("world");
}
A parameterized type like List<String>
is
itself a type and can be used as the value of a type parameter for
some other type. You might see code like this:
// Look at all those nested angle brackets!
Map<String, List<List<int[]>>> map = getWeirdMap();
// The compiler knows all the types and we can write expressions
// like this without casting. We might still get NullPointerException
// or ArrayIndexOutOfBounds at runtime, of course.
int value = map.get(key).get(0).get(0)[0];
// Here's how we break that expression down step by step.
List<List<int[]>> listOfLists = map.get(key);
List<int[]> listOfIntArrays = listOfLists.get(0);
int[] array = listOfIntArrays.get(0);
int element = array[0];
In the code above, the get( )
methods of
java.util.List<E>
and
java.util.Map<K,V>
return a list or map
element of type E
and V
respectively. Note, however, that generic types can use their
variables in more sophisticated ways. Look up
List<E>
in the reference section of this
book, and you'll find that its iterator(
)
method is declared to return an
Iterator<E>
. That is, the method returns an
instance of a parameterized type whose actual type parameter is the
same as the actual type parameter of the list. To illustrate this
concretely, here is a way to obtain the first element of a
List<String>
without calling
get(0)
.
List<String> words = // ...initialized elsewhere...
Iterator<String> iterator = words.iterator();
String firstword = iterator.next();
This section delves deeper into the details of generic type usage, explaining the following topics:
The consequences of using generic types without type parameters
The parameterized type hierarchy
A hole in the compile-time type safety of generic types and a patch to ensure runtime type safety
Why arrays of parameterized types are not typesafe
Even though the Java collection classes have
been modified to take advantage of generics, you are not required to
specify type parameters to use them. A generic type used without type
parameters is known as a raw
type.
Existing pre-5.0 code continues to work: you simply write all the
casts that you're already used to writing, and you
put up with some pestering from the compiler. Consider the following
code that stores objects of mixed types into a raw
List
:
List l = new ArrayList();
l.add("hello");
l.add(new Integer(123));
Object o = l.get(0);
This code works fine in Java 1.4. If we compile it using Java 5.0, however, javac compiles the code but prints this complaint:
Note: Test.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
When we recompile with the -Xlint
option as
suggested, we see these warnings:
Test.java:6: warning: [unchecked]
unchecked call to add(E) as a member of the raw type java.util.List
l.add("hello");
^
Test.java:7: warning: [unchecked]
unchecked call to add(E) as a member of the raw type java.util.List
l.add(new Integer(123));
^
The compiler warns us about the add( )
calls
because it cannot ensure that the values being added to the list have
the correct types. It is letting us know that because
we've used a raw type, it cannot verify that our
code is typesafe. Note that the call to get( )
is
okay because it is extracting an element that is already safely in
the list.
If you get unchecked warnings on files that do not use any of the new
Java 5.0 features, you can simply compile them with the
-source 1.4
flag, and the compiler
won't complain. If you can't do
that, you can ignore the warnings, suppress them with an
@SuppressWarnings("unchecked")
annotation (see Section 4.3 later
in this chapter) or upgrade your code to specify a type
parameter.
[2]
The following code, for example,
compiles with no warnings and still allows you to add objects of
mixed types to the list:
List<Object> l = new ArrayList<Object>();
l.add("hello");
l.add(123); // autoboxing
Object o = l.get(0);
Parameterized types form a type hierarchy, just as normal types do. The hierarchy is based on the base type, however, and not on the type of the parameters. Here are some experiments you can try:
ArrayList<Integer> l = new ArrayList<Integer>();
List<Integer> m = l; // okay
Collection<Integer> n = l; // okay
ArrayList<Number> o = l; // error
Collection<Object> p = (Collection<Object>)l; // error, even with cast
A List<Integer>
is a
Collection<Integer>
, but it is not a
List<Object>
. This is nonintuitive, and it
is important to understand why generics work this way. Consider this
code:
List<Integer> li = new ArrayList<Integer>();
li.add(123);
// The line below will not compile. But for the purposes of this
// thought-experiment, assume that it does compile and see how much
// trouble we get ourselves into.
List<Object> lo = li;
// Now we can retrieve elements of the list as Object instead of Integer
Object number = lo.get(0);
// But what about this?
lo.add("hello world");
// If the line above is allowed then the line below throws ClassCastException
Integer i = li.get(1); // Can't cast a String to Integer!
This then is the reason that a List<Integer>
is not a List<Object>
, even though all
elements of a List<Integer>
are in fact
instances of Object
. If the conversion to
List<Object>
were allowed,
non-Integer
objects could be added to the list.
As we've
seen, a
List<X>
cannot be converted to a
List<Y>
, even when X
can be converted to Y
. A
List<X>
can be converted to a
List
, however, so that you can pass it to a legacy
method that expects an argument of that type and has not been updated
for generics.
This ability to convert parameterized types to nonparameterized types is essential for backward compatibility, but it does open up a hole in the type safety system that generics offer:
// Here's a basic parameterized list.
List<Integer> li = new ArrayList<Integer>();
// It is legal to assign a parameterized type to a nonparameterized variable
List l = li;
// This line is a bug, but it compiles and runs.
// The Java 5.0 compiler will issue an unchecked warning about it.
// If it appeared as part of a legacy class compiled with Java 1.4, however,
// then we'd never even get the warning.
l.add("hello");
// This line compiles without warning but throws ClassCastException at runtime.
// Note that the failure can occur far away from the actual bug.
Integer i = li.get(0);
Generics provide compile-time type
safety only. If you compile all your code with the Java 5.0 compiler
and do not get any unchecked warnings, these compile-time checks are
enough to ensure that your code is also typesafe at runtime. But if
you have unchecked warnings or are working with legacy code that
manipulates your collections as raw types, you may want to take
additional steps to ensure type safety at runtime. You can do this
with methods like
checkedList()
and checkedMap( )
of java.util.Collections
. These methods enclose
your collection in a wrapper collection that performs runtime type
checks to ensure that only values of the correct type are added to
the collection. For example, we could prevent the type safety hole
shown above like this:
// Here's a basic parameterized list.
List<Integer> li = new ArrayList<Integer>();
// Wrap it for runtime type safety
List<Integer> cli = Collections.checkedList(li, Integer.class);
// Now widen the checked list to the raw type
List l = cli;
// This line compiles but fails at runtime with a ClassCastException.
// The exception occurs exactly where the bug is, rather than far away
l.add("hello");
Arrays require special
consideration when working with generic types. Recall that an array
of type S[ ]
is also of type
T[]
, if T
is a superclass (or
interface) of S
. Because of this, the Java
interpreter must perform a runtime check every time you store an
object in an array to ensure that the runtime type of the object and
of the array are compatible. For example, the following code fails
this runtime check and throws an
ArrayStoreException
:
String[] words = new String[10];
Object[] objs = words;
objs[0] = 1; // 1 autoboxed to an Integer, throws ArrayStoreException
Although the compile-time type of objs
is
Object[]
, its runtime type is String[
]
, and it is not legal to store an
Integer
in it.
When we work with generic types, the runtime check for array store exceptions is no longer sufficient because a check performed at runtime does not have access to the compile-time type parameter information. Consider this (hypothetical) code:
List<String>[] wordlists = new ArrayList<String>[10];
ArrayList<Integer> ali = new ArrayList<Integer>();
ali.add(123);
Object[] objs = wordlists;
objs[0] = ali; // No ArrayStoreException
String s = wordlists[0].get(0); // ClassCastException!
If the code above were allowed, the runtime array store check would
succeed: without compile-time type parameters, the code simply stores
an ArrayList
into an
ArrayList[]
array, which is perfectly legal. Since
the compiler can't prevent you from defeating type
safety in this way, it instead prevents you from creating any array
of parameterized type. The scenario above can never occur because the
compiler will refuse to compile the first line.
Note that this is not a blanket restriction on using arrays with generics; it is just a restriction on creating arrays of parameterized type. We'll return to this issue when we look at how to write generic methods.
Suppose we
want to write a method to
display the
elements of a List
.
[3]
Before
List
was a generic type, we'd
just write code like this:
public static void printList(PrintWriter out, List list) {
for(int i=0, n=list.size(); i < n; i++) {
if (i > 0) out.print(", ");
out.print(list.get(i).toString());
}
}
In Java 5.0, List
is a generic type, and, if we
try to compile this method, we'll get unchecked
warnings. In order to get rid of those warnings, you might be tempted
to modify the method as follows:
public static void printList(PrintWriter out, List<Object> list) {
for(int i=0, n=list.size(); i < n; i++) {
if (i > 0) out.print(", ");
out.print(list.get(i).toString());
}
}
This code compiles without warnings but isn't very
useful because the only lists that can be passed to it are lists
explicitly declared of type List<Object>
.
Remember that List<String>
and
List<Integer>
(for example) cannot be
widened or cast to List<Object>
. What we
really want is a typesafe printList()
method to
which we can pass any List
, regardless of how it
has been parameterized. The solution is to use a wildcard as the type
parameter. The method would then be written like this:
public static void printList(PrintWriter out, List<?> list) {
for(int i=0, n=list.size(); i < n; i++) {
if (i > 0) out.print(", ");
Object o = list.get(i);
out.print(o.toString());
}
}
This version of the method compiles
without warnings and can be used the way we want it to be used. The
?
wildcard represents an unknown type, and the
type List<?>
is read as
"List of unknown."
As a general rule, if a type is generic and you
don't know or don't care about the
value of the type variable, you should always use a
?
wildcard instead of using a
raw type. Raw types
are allowed only for backward compatibility and should be used only
in legacy code. Note, however, that you cannot use a wildcard when
invoking a constructor. The following code is not legal:
List<?> l = new ArrayList<?>();
There is no sense in creating a List
of unknown
type. If you are creating it, you should know what kind of elements
it will hold. You may later want to pass such a list to a method that
does not care about its element type, but you need to specify an
element type when you create it. If what you really want is a
List
that can hold any type of object, do this:
List<Object> l = new ArrayList<Object>();
It should be clear from the printList( )
variants
above that a List<?>
is not the same thing
as a List<Object>
and that neither is the
same thing as a raw List
. A
List<?>
has two important properties that
result from the use of a wildcard. First, consider methods like
get()
that are declared to return a value of the
same type as the type parameter. In this case, that type is unknown,
so these methods return an Object
. Since all we
need to do with the object is invoke its
toString()
method, this is fine for our needs.
Second, consider List
methods such as
add()
that are declared to accept an argument
whose type is specified by the type parameter. This is the more
surprising case: when the type parameter is unknown, the compiler
does not let you invoke any methods that have a parameter of the
unknown type because it cannot check that you are passing an
appropriate value. A List<?>
is effectively
read-only since the compiler does not allow us to invoke methods like
add( )
, set()
, and
addAll( )
.
Let's continue now with a
slightly more complex variant of
our original example. Suppose that we want to write a
sumList()
method to compute the sum of a list of
Number
objects. As before, we could use a raw
List
, but we would give up type safety and have to
deal with unchecked warnings from the compiler. Or we could use a
List<Number>
, but then we
wouldn't be able to call the method for a
List<Integer>
or
List<Double>
, types we are more likely to
use in practice. But if we use a wildcard, we don't
actually get the type safety that we want because we have to trust
that our method will be called with a List
whose
type parameter is actually Number
or a subclass
and not, say, a String
. Here's
what such a method might look like:
public static double sumList(List<?> list) {
double total = 0.0;
for(Object o : list) {
Number n = (Number) o; // A cast is required and may fail
total += n.doubleValue();
}
return total;
}
To fix this method and make it truly typesafe, we need to use a
bounded wildcard that states that the type
parameter of the List
is an unknown type that is
either Number
or a subclass of
Number
. The following code does just what we want:
public static double sumList(List<? extends Number> list) {
double total = 0.0;
for(Number n : list) total += n.doubleValue();
return total;
}
The type List<? extends Number>
could be
read as "List
of unknown
descendant of Number
." It is
important to understand that, in this context,
Number
is considered a descendant of itself.
Note that the cast is no longer required. We don't
know the type of the elements of the list, but we know that they have
an "upper bound" of
Number
so we can extract them from the list as
Number
objects. The use of a
for/in
loop obscures the process of extracting
elements from a list somewhat. The general rule is that when you use
a bounded wildcard with an upper bound, methods (like the
get()
method of List
) that
return a value of the type parameter use the upper bound. So if we
called list.get( )
instead of using a
for/in
loop, we'd also get a
Number
. The prohibition on calling methods like
list.add( )
that have arguments of the type
parameter type still stands: if the compiler allowed us to call those
methods we could add an Integer
to a list that was
declared to hold only Short
values, for example.
It is also possible to specify a lower-bounded wildcard using the
keyword super
instead of
extends
. This technique has a different impact on
what methods can be called. Lower-bounded wildcards are much less
commonly used than upper-bounded wildcards, and we discuss them later
in the chapter.
[1] Throughout this chapter, I've tried to consistently use the term "generic type" to mean a type that declares one or more type variables and the term "parameterized type" to mean a generic type that has had actual type arguments substituted for its type varaiables. In common usage, however, the distinction is not a sharp one and the terms are sometimes used interchangeably.
[2] At the time of this writing, javac does not yet honor the
@SuppressWarnings
annotation. It is expected to do so in Java 5.1.[3] The three
printList()
methods shown in this section ignore the fact that theList
implementations classes injava.util
all provide workingtoString()
methods. Notice also that the methods assume that theList
implementsRandomAccess
and provides very poor performance onLinkedList
instances.
David Flanagan is the author of a number of O'Reilly books, including Java in a Nutshell, Java Examples in a Nutshell, Java Foundation Classes in a Nutshell, JavaScript: The Definitive Guide, and JavaScript Pocket Reference.
View catalog information for Java in a Nutshell, 5th Edition
Return to ONJava.com.
Showing messages 1 through 7 of 7.
public static void printList(PrintWriter out, List list) {
for(int i=0, n=list.size(); i < n; i++) {
if (i > 0) out.print(", ");
out.print(list.get(i).toString());
}
}
List<?>
. The trouble is it's not really true. Consider the List.clear()
method. It takes no parameters and thus you can call it on a List<?>
.Collections.unmodifiableList()
List<Object> l = new ArrayList<Object>();
l.add("Hello");
l.add(1); // Does autoboxing apply here?