How to use Java generics to avoid ClassCastExceptions



void copy(List<?> src, List<?> dest, Filter filter)
{
   for (int i = 0; i < src.size(); i++)
      if (filter.accept(src.get(i)))
         dest.add(src.get(i));
}

This method’s parameter list is correct, but there’s a problem. According to the compiler, dest.add(src.get(i)); violates type safety. The ? implies that any kind of object can be the list’s element type, and it’s possible that the source and destination element types are incompatible.

For example, if the source list was a List of Shape and the destination list was a List of String, and copy() was allowed to proceed, ClassCastException would be thrown when attempting to retrieve the destination list’s elements.

You could partially solve this problem by providing upper and lower bounds for the wildcards, as follows:

void copy(List<? extends String> src, List<? super String> dest, Filter filter)
{
   for (int i = 0; i < src.size(); i++)
      if (filter.accept(src.get(i)))
         dest.add(src.get(i));
}

You can provide an upper bound for a wildcard by specifying extends followed by a type name. Similarly, you can supply a lower bound for a wildcard by specifying super followed by a type name. These bounds limit the types that can be passed as actual type arguments.

In the example, you can interpret ? extends String as any actual type argument that happens to be String or a subclass. Similarly, you can interpret ? super String as any actual type argument that happens to be String or a superclass. Because String is final, which means that it cannot be extended, only source lists of String objects and destination lists of String or Object objects can be passed, which isn’t very useful.

You can fully solve this problem by using a generic method, which is a class or instance method with a type-generalized implementation. A generic method declaration adheres to the following syntax:

<formalTypeParameterList> returnType identifier(parameterList)

A generic method’s formal type parameter list precedes its return type. It consists of type parameters and optional upper bounds. A type parameter can be used as the return type and can appear in the parameter list.

Listing 5 demonstrates how to declare and invoke (call) a generic copy() method.

Listing 5. GenDemo.java (version 5)

import java.util.ArrayList;
import java.util.List;
public class GenDemo
{
   public static void main(String[] args)
   {
      List<Integer> grades = new ArrayList<Integer>();
      Integer[] gradeValues = 
      {
         Integer.valueOf(96),
         Integer.valueOf(95),
         Integer.valueOf(27),
         Integer.valueOf(100),
         Integer.valueOf(43),
         Integer.valueOf(68)
      };
      for (int i = 0; i < gradeValues.length; i++)
         grades.add(gradeValues[i]);
      List<Integer> failedGrades = new ArrayList<Integer>();
      copy(grades, failedGrades, new Filter<Integer>()
                                 {
                                    @Override
                                    public boolean accept(Integer grade)
                                    {
                                       return grade.intValue() <= 50;
                                    }
                                 });
      for (int i = 0; i < failedGrades.size(); i++)
         System.out.println(failedGrades.get(i));
   }
   static <T> void copy(List<T> src, List<T> dest, Filter<T> filter)
   {
      for (int i = 0; i < src.size(); i++)
         if (filter.accept(src.get(i)))
            dest.add(src.get(i));
   }
}
interface Filter<T>
{
   boolean accept(T o);
}

In Listing 5 I’ve declared a <T> void copy(List<T> src, List<T> dest, Filter<T>
filter)
generic method. The compiler notes that the type of each of the src, dest, and filter parameters includes the type parameter T. This means that the same actual type argument must be passed during a method invocation, and the compiler infers this argument by examining the invocation.

If you compile Listing 5 (javac GenDemo.java) and run the application (java GenDemo) you should observe the following output:

27
43

About generics and type inference

The Java compiler includes a type inference algorithm for identifying the actual type argument(s) when instantiating a generic class, invoking a class’s generic constructor, or invoking a generic method.

Generic class instantiation

Before Java SE 7, you had to specify the same actual type argument(s) for both a variable’s generic type and the constructor when instantiating a generic class. Consider the following example:

Map<String, Set<String>> marbles = new HashMap<String, Set<Integer>>();

The redundant String, Set<String> actual type arguments in the constructor invocation clutter the source code. To help you eliminate this clutter, Java SE 7 modified the type inference algorithm so that you can replace the constructor’s actual type arguments with an empty list (<>), provided that the compiler can infer the type arguments from the instantiation context.

Informally, <> is referred to as the diamond operator, although it isn’t a real operator. Use of the diamond operator results in the following more concise example:

Map<String, Set<String>> marbles = new HashMap<>();

To leverage type inference during generic class instantiation, you must specify the diamond operator. Consider the following example:

Map<String, Set<String>> marbles = new HashMap();

The compiler generates an “unchecked conversion warning” because the HashMap() constructor refers to the java.util.HashMap raw type and not to the Map<String, Set<String>> type.

Generic constructor invocation

Generic and non-generic classes can declare generic constructors in which a constructor has a formal type parameter list. For example, you could declare the following generic class with a generic constructor:

public class Box<E>
{
   public <T> Box(T t) 
   {
      // ...
   }
}

This declaration specifies generic class Box<E> with formal type parameter E. It also specifies a generic constructor with formal type parameter T. Consider the following example:

new Box<Marble>("Aggies")

This expression instantiates Box<Marble>, passing Marble to E. Also, the compiler infers String as T’s actual type argument because the invoked constructor’s argument is a String object.

We can go further by leveraging the diamond operator to eliminate the Marble actual type argument in the constructor invocation, as long as the compiler can infer this type argument from the instantiation context:

Box<Marble> box = new Box<>("Aggies");

The compiler infers the type Marble for formal type parameter E of generic class Box<E>, and infers type String for formal type parameter T of this generic class’s constructor.

Generic method invocation

When invoking a generic method, you don’t have to supply actual type arguments. Instead, the type inference algorithm examines the invocation and corresponding method declaration to figure out the invocation’s type argument(s). The inference algorithm identifies argument types and (when available) the type of the assigned or returned result.

The algorithm attempts to identify the most specific type that works with all arguments. For example, in the following code fragment, type inference determines that the java.io.Serializable interface is the type of the second argument (new TreeSet<String>()) that is passed to select() — TreeSet implements Serializable:

Serializable s = select("x", new TreeSet<String>());
static <T> T select(T a1, T a2) 
{
   return a2;
}

I previously presented a generic static <T> void copy(List<T> src, List<T> dest,
Filter<T> filter)
class method that copies a source list to a destination list, and is subject to a filter for deciding which source objects are copied. Thanks to type inference, you can specify copy(/*...*/); to invoke this method. It’s not necessary to specify an actual type argument.

You might encounter a situation where you need to specify an actual type argument. For copy() or another class method, you would specify the argument(s) after the class name and member access operator (.) as follows:

GenDemo.<Integer>copy(grades, failedGrades, new Filter() /*...*/);

For an instance method, the syntax is nearly identical. Instead of following a class name and operator, however, the actual type argument would follow the constructor call and member access operator:

new GenDemo().<Integer>copy(grades, failedGrades, new Filter() /*...*/);

Type erasure and other limitations of generics in Java

While generics as such might not be controversial, their particular implementation in the Java language has been. Generics were implemented as a compile-time feature that amounts to syntactic sugar for eliminating casts. The compiler throws away a generic type or generic method’s formal type parameter list after compiling the source code. This “throwing away” behavior is known as type erasure (or erasure, for short). Other examples of erasure in generics include inserting casts to the appropriate types when code isn’t type correct, and replacing type parameters by their upper bounds (such as Object).

Erasure prevents a generic type from being reifiable (exposing complete type information at runtime). As a result, the Java virtual machine doesn’t know the difference between. Take, for example, Set<String> and Set<Marble>; at runtime, only the raw type Set is available. In contrast, primitive types, non-generic types (reference types prior to Java 5), raw types, and invocations of wildcards are reifiable.

The inability for generic types to be reifiable has resulted in several limitations:

  • With one exception, the instanceof operator cannot be used with parameterized types. The exception is an unbounded wildcard. For example, you cannot specify Set<Shape> shapes = null; if (shapes instanceof
    ArrayList<Shape>) {}
    . Instead, you need to change the instanceof expression to shapes instanceof ArrayList<?>, which demonstrates an unbounded wildcard. Alternatively, you could specify shapes instanceof ArrayList, which demonstrates a raw type (and which is the preferred use).
  • Some developers have pointed out that you cannot use Java Reflection to obtain generics information, which isn’t present in the class file. However, in Java Reflection: Generics developer Jakob Jenkov points out a few cases where generics information is stored in a class file, and this information can be accessed reflectively.
  • You cannot use type parameters in array-creation expressions; for example elements = new E[size];. The compiler will report a generic array creation error message if you try to do so.

Given the limitations of erasure, you might wonder why generics were implemented with erasure. The reason is simple: The Java compiler was refactored to use erasure so that generic code could interoperate with legacy Java code, which isn’t generic (reference types cannot be parameterized). Without that backward compatibility, legacy Java code would fail to compile in a Java compiler supporting generics.

Generics and heap pollution

While working with generics, you may encounter heap pollution, in which a variable of a parameterized type refers to an object that isn’t of that parameterized type (for instance if a raw type has been mixed with a parameterized type). In this situation, the compiler reports an “unchecked warning” because the correctness of an operation involving a parameterized type (like a cast or method call) cannot be verified. Consider Listing 6.

Listing 6. Demonstrating heap pollution

import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;
public class HeapPollutionDemo
{
   public static void main(String[] args)
   {
      Set s = new TreeSet<Integer>();
      Set<String> ss = s;            // unchecked warning
      s.add(Integer.valueOf(42));    // another unchecked warning
      Iterator<String> iter = ss.iterator();
      while (iter.hasNext())
      {
         String str = iter.next();   // ClassCastException thrown
         System.out.println(str);
      }
   }
}

Variable ss has parameterized type Set<String>. When the Set that is referenced by s is assigned to ss, the compiler generates an unchecked warning. It does so because the compiler cannot determine that s refers to a Set<String> type (it does not). The result is heap pollution. (The compiler allows this assignment to preserve backward compatibility with legacy Java versions that don’t support generics. Furthermore, erasure transforms Set<String> into Set, which results in one Set being assigned to another Set.)

The compiler generates a second unchecked warning on the line that invokes Set’s add() method. It does so because it cannot determine if variable s refers to a Set<String> or Set<Integer> type. This is another heap pollution situation. (The compiler allows this method call because erasure transforms Set’s boolean add(E e) method to boolean add(Object
o)
, which can add any kind of object to the set, including the Integer subtype of Object.)

Generic methods that include variable arguments (varargs) parameters can also cause heap pollution. This scenario is demonstrated in Listing 7.

Listing 7. Demonstrating heap pollution in an unsafe varargs context

import java.util.Arrays;
import java.util.List;
public class UnsafeVarargsDemo
{
   public static void main(String[] args)
   {
      unsafe(Arrays.asList("A", "B", "C"),
             Arrays.asList("D", "E", "F"));
   }
   static void unsafe(List<String>... l)
   {
      Object[] oArray = l;
      oArray[0] = Arrays.asList(Double.valueOf(3.5));
      String s = l[0].get(0);
   }
}

The Object[] oArray = l; assignment introduces the possibility of heap pollution. A value whose List type’s parameterized type doesn’t match the parameterized type (String) of the varargs parameter l can be assigned to array variable oArray. However, the compiler doesn’t generate an unchecked warning because it has already done so when translating List<String>... l to List[] l. This assignment is valid because variable l has the type List[], which subtypes Object[].

Also, the compiler doesn’t issue a warning or error when assigning a List object of any type to any of oArray’s array components; for example, oArray[0] = Arrays.asList(Double.valueOf(3.5));. This assignment assigns to the first array component of oArray a List object containing a single Double object.

The String s = l[0].get(0); assignment is problematic. The object stored in the first array component of variable l has the type List<Double>, but this assignment expects an object of type List<String>. As a result, the JVM throws ClassCastException.

Compile the Listing 7 source code (javac -Xlint:unchecked UnsafeVarargsDemo.java). You should observe the following output (slightly reformatted for readability):

UnsafeVarargsDemo.java:8: warning: [unchecked] unchecked generic array 
creation for varargs parameter of 
type List<String>[]
      unsafe(Arrays.asList("A", "B", "C"),
            ^
UnsafeVarargsDemo.java:12: warning: [unchecked] Possible heap pollution 
from parameterized vararg type 
List<String>
   static void unsafe(List<String>... l)
                                      ^
2 warnings

Earlier in this article, I stated that you cannot use type parameters in array-creation expressions. For example, you cannot specify elements = new E[size];. The compiler reports a “generic array creation error” message when you try to do so. However, it’s still possible to create a generic array, but only in a varargs context, and that is what the first warning message is reporting. Behind the scenes, the compiler transforms List<String>...
l
to List<String>[] l and then to List[] l.

Notice that the heap pollution warning is generated at the unsafe() method’s declaration site. This message isn’t generated at this method’s call site, which is the case with Java 5 and Java 6 compilers.

Not all varargs methods will contribute to heap pollution. However, a warning message will still be issued at the method’s declaration site. If you know that your method doesn’t contribute to heap pollution, you can suppress this warning by declaring it with the @SafeVarargs annotation—Java SE 7 introduced the java.lang.SafeVarargs annotation type. For example, because there is no way for the Arrays class’s asList() method to contribute to heap pollution, this method’s declaration has been annotated with @SafeVarargs, as follows:

@SafeVarargs
public static <T> List<T> asList(T... a)

The @SafeVarargs annotation eliminates the generic array creation and heap pollution warning messages. It is a documented part of the method’s contract and asserts that the method’s implementation will not improperly handle the varargs formal parameter.

Do you want to practice more with Java generics? See How to use generics in your Java programs.

Latest articles

spot_imgspot_img

Related articles

Leave a reply

Please enter your comment!
Please enter your name here

spot_imgspot_img