Thursday 7 April 2011

Generics 101, Part 3: Exploring Generics Through a Generic Stack Type

Java 2 Standard Edition 5.0 introduced generics to Java developers. Since their inclusion in the Java language, generics have proven to be controversial. In the last of his three-part series, Jeff Friesen introduces you to the need for generic methods, focused on how generics are implemented to explain why you couldn’t assign new E[size] to elements.

Generics are language features that many developers have difficulty grasping. Removing this difficulty is the focus of this three-part series on generics.

Part 1 introduced generics by explaining what they are with an emphasis on generic types and parameterized types. It also explained the rationale for bringing generics to Java.

Part 2 dug deeper into generics by showing you how to codify a generic Stack type, and by exploring unbounded and bounded type parameters, type parameter scope, and wildcard arguments in the context of Stack.

This article continues from where Part 2 left off by focusing on generic methods as it explores several versions of a copy() method for copying one collection to another.

Also, this article digs into the topic of arrays and generics, which explains why you could not assign new E[size] to elements in Listing 1’s Stack type – see Part 2.

Finally, to reinforce your understanding of the material presented in all three parts of this series, this article closes with an exercises section of questions to answer.
Generic Copy Method

Suppose you want to create a method for copying one collection (perhaps a set or a list) to another collection. Your first impulse might be to create a void copy(Collection<Object> src, Collection<Object> dest) method. However, such a method's usefulness would be limited because it could only copy collections whose element types are Object[md]collections of Strings couldn't be copied, for example.

If you want to pass source and destination collections whose elements are of arbitrary type (but their element types agree), you need to specify the wildcard character as a placeholder for that type. For example, the following code fragment reveals a copy() method that accepts collections of arbitrary-typed objects as its arguments:

public static void copy(Collection<?> src, Collection<?> dest)
{
   Iterator<?> iter = src.iterator();
   while (iter.hasNext())
      dest.add(iter.next());
}

Although this method's parameter list is now correct, there is a problem, and the compiler outputs an add(capture#469 of ?) in java.util.Collection<capture#469 of ?> cannot be applied to (java.lang.Object) error message when it encounters dest.add(iter.next());.

This error message appears to be incomprehensible, but basically means that the dest.add(iter.next()); method call violates type safety. Because ? implies that any type of object can serve as a collection's element type, it's possible that the destination collection's element type is incompatible with the source collection's element type.

For example, suppose you create a List of String as the source collection and a Set of Integer as the destination collection. Attempting to add the source collection’s String elements to the destination collection, which expects Integers violates type safety. If this copy operation was allowed, a ClassCastException would be thrown when trying to obtain the destination collection's elements.

You could avoid this problem by specifying void copy(Collection<String> src, Collection<String> dest), but this method header limits you to copying only collections of String. Alternatively, you might restrict the wildcard argument, which is demonstrated in the following code fragment:

public static void copy(Collection<? extends String> src,
                        Collection<? super String> dest)
{
   Iterator<? extends String> iter = src.iterator();
   while (iter.hasNext())
      dest.add(iter.next());
}

This code fragment demonstrates a feature of the wildcard argument: You can supply an upper bound or (unlike with a type parameter) a lower bound to limit the types that can be passed as actual type arguments to the generic type. Specify an upper bound via extends followed by the upper bound type after the ?, and a lower bound via super followed by the lower bound type after the ?.

You interpret ? extends String to mean that any actual type argument that is String or a subclass can be passed, and you interpret ? super String to imply that any actual type argument that is String or a superclass can be passed. Because String cannot be subclassed, this means that you can only pass source collections of String and destination collections of String or Object.

We still haven't solved the problem of copying collections of arbitrary element types to other collections (with the same element type). However, there is a solution: Use a generic method (a static or non-static method with a type-generalized implementation). Generic methods are syntactically expressed as follows:

<formal_type_parameter_list> return_type identifier(parameter_list)

The formal_type_parameter_list is the same as when specifying a generic type: it consists of type parameters with optional bounds. A type parameter can appear as the method's return_type, and type parameters can appear in the parameter_list. The compiler infers the actual type arguments from the context in which the method is invoked.

You'll discover many examples of generic methods in the collections framework. For example, its Collections class provides a public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp) method for returning the maximum element in the given Collection according to the ordering specified by the supplied Comparator.

We can easily convert copy() into a generic method by prefixing the return type with <T> and replacing each wildcard with T. The resulting method header is <T> void copy(Collection<T> src, Collection<T> dest), and Listing 1 presents its source code as part of an application that copies a List of String to a Set of String.
Listing 1—Copy.java

// Copy.java
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
public class Copy
{
   public static void main(String[] args)
   {
      List<String> planetsList = new ArrayList<String>();
      planetsList.add("Mercury");
      planetsList.add("Venus");
      planetsList.add("Earth");
      planetsList.add("Mars");
      planetsList.add("Jupiter");
      planetsList.add("Saturn");
      planetsList.add("Uranus");
      planetsList.add("Neptune");
      Set<String> planetsSet = new TreeSet<String>();
      copy (planetsList, planetsSet);
      Iterator<String> iter = planetsSet.iterator();
      while (iter.hasNext())
         System.out.println(iter.next());
   }
   public static <T> void copy(Collection<T> src, Collection<T> dest)
   {
      Iterator<T> iter = src.iterator();
      while (iter.hasNext())
         dest.add(iter.next());
   }
}

Within the copy() method, notice that type parameter T appears in the context of Iterator<T>, because src.iterator() returns elements of the type passed to T, which happens to match the type of src's elements. Otherwise, the method remains unchanged from its previous incarnations.

Listing 1 generates the following output:

Earth
Jupiter
Mars
Mercury
Neptune
Saturn
Uranus
Venus

No comments:

Post a Comment