Generic Types, Part 1

http://www.onjava.com/pub/a/onjava/excerpt/javaian5_chap04/index.html

Generic Types, Part 1

by David Flanagan

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, 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.

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.

Typesafe Collections

The java.utilpackage 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();

Understanding Generic Types

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

Raw types and unchecked warnings

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);

The parameterized type hierarchy

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.

Runtime type safety

As we've seen, a List<X> cannot be converted to a List<Y>, even when Xcan 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 of parameterized type

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.

Type Parameter Wildcards

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( ).

Bounded wildcards

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.


Footnotes

[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.

[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. [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. [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.

[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. [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. [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 the List implementations classes in java.util all provide working toString() methods. Notice also that the methods assume that the List implements RandomAccess and provides very poor performance on LinkedList instances.

[3] The three printList() methods shown in this section ignore the fact that the List implementations classes in java.util all provide working toString() methods. Notice also that the methods assume that the List implements RandomAccess and provides very poor performance on LinkedList instances. [3] The three printList() methods shown in this section ignore the fact that the List implementations classes in java.util all provide working toString() methods. Notice also that the methods assume that the List implements RandomAccess and provides very poor performance on LinkedList instances. [3] The three printList() methods shown in this section ignore the fact that the List implementations classes in java.util all provide working toString() methods. Notice also that the methods assume that the List implements RandomAccess and provides very poor performance on LinkedList 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.


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值