Generics

The implementation of parameterized types in Tiger is called generics. Generics allow you to build parameterized types that are checked at compile time. In addition to the new parameterized collection types that are implemented as part of Tiger, you can define new parameterized types to fit your needs.

Screenshot

For those who want to look up the specifications of generics, they can be found under JSR 14. However, as of this writing, the version included in the public prototype implementation is old, and some parts of it are inaccurate.


One of the most persistent problems in Java is the lack of type safety in collections. For example, you can declare a new Set that should contain Address objects in the following manner:

Set addresses = new HashSet( ); // Component type Address


Once this set is declared, you are basically trusting your users never to place anything other than an Address in the set. If a user adds something other than an address to the set, the most fortunate result that could occur would be your program crashing outright on a ClassCastException. However, if you are not so fortunate, you could end up with irreparable data corruption. If this set is used as a part of a business-critical data model, such as the Online Only Bank data model in , you could end up destroying the entire business by implementing this software. Therefore, whenever someone calls a method to set your addresses property, you should check every member of the collection to make sure that it is a legal type (see ). The problem with checking elements in a collection at runtime is that it is extremely expensive; the order of efficiency is only O(n). If you have only 10 addresses in your collection, checking elements is easy. However, if the collection contains 15,000 addresses, then you would incur a significant overhead whenever someone calls the setter. On the other hand, if you can prevent users from placing anything other than an address in your collection at compile time, then you wouldn't have to check the types at runtime. If they try to give you something that isn't an address, then the compiler will reject the attempt. This is exactly what parameterized types do. Parameterized types allow you to declare a type that requires an additional parameter of another type to be complete. Using parameterized types, the addresses property can be declared in the following manner:

Set<Address> addresses = new HashSet<Address>( );


Now, if anyone tries to use another type as a component to addresses, the compiler will indicate an error on that line:

addresses.add(new Integer(5)); // <== Compiler error.


When compiled, this would produce the following result:

>javac oracle/hcj/tiger/GenericSyntax.java oracle/hcj/tiger/GenericSyntax.java:24: cannot find symbol symbol : method add(java.lang.Integer)
location: interface java.util.Set<oracle.hcj.tiger.Address>
 addresses.add(new Integer(5));
 ^


The error specifies that the compiler cannot find an add method appropriate to the type. Since the user can't put anything into the set that doesn't belong there, you shouldn't have to check the values within the collection to make sure they are the right type.

Screenshot

Unfortunately, the error message is a bit difficult to read. It should probably say that Integer is not applicable to the add method. This would be far more intuitive. In the meantime, watch out for this error message and make sure you don't spend hours checking methods in your generic classes before you check to see whether the user called them with the correct type.


The Syntax of Generics

The general syntax of generics has two parts: the declaration and the usage. You have already seen how to use a generic type. To declare a generic type, use the following basic syntax:

class SomeClass<Type> {
 Type value = null; public Type getValue( ) {
 return this.value( );
 }
 public void setValue(final Type value) {
 this.value = value;
 } }


This syntax declares a generic type that works with the type that the user passes when the instance is constructed. The Type parameter can be any constructed type in Java, but you can't use primitives as component types for generics. This is considered to be a major limitation of the generics implementation. In the declaration of SomeType, wherever the compiler sees the word Type in the body of the class, it will substitute Type with the type parameter given at the time of construction. For example, consider how this generic type is used:

SomeClass<String> someVar = new SomeClass<String>( ); someVar.setValue("Hello");


This code would cause the compiler to consider your generic type in the following expansion:

class SomeClass<String> {
 String value = null; public String getValue( ) {
 return this.value( );
 }
 public void setValue(final String value) {
 this.value = value;
 } }


Screenshot

You may have noted that I didn't use the word "generated" here. This is because the generics mechanism in Tiger doesn't generate anything. This topic is covered later in the chapter.


Not only does this give you type safety with your generic class, it allows you to reuse code. The implementation is rather simple. However, some generic types can easily have thousands of lines of code.

Nested generics

In your implementation, note that you can use any nonprimitive type as the Type argument to SomeClass. This includes other generic types:

SomeClass<Collection<String>> someVar = new SomeVar<Collection<String>>( );


If you use this syntax for SomeClass, the code will be expanded properly so that you can call setValue( ) only with collections of strings. This nesting can extend as deep as you want. However, for the sake of readability, I recommend you keep generic nesting to a minimum. In addition to nesting generic type declarations, you can use the Type parameter to create other generic instances:

class SomeClass<Type> {
 List<Type> value = null; public List<Type> getValue( ) {
 return this.value( );
 }
 public void setValue(final List<Type> value) {
 this.value = value;
 } }


Just as before, the word Type is expanded wherever it is seen inside the class. This scenario includes the creation of the generic list instance for setValue( ).

Multi-generics

When declaring generics, you aren't limited to one generic type. In fact, you can declare several parameters to your generic type using the following syntax:

public class Pair<LeftType, RightType> {
 private LeftType left = null;
 private RightType right = null; public LeftType left( ) {
 return this.left;
 }
 public void left(final LeftType left) {
 this.left = left;
 }
 public RightType right( ) {
 return this.right;
 }
 public void Right(final RightType right) {
 this.right = right;
 }
}


Once declared, the class is used as follows:

Pair<String, Integer> nvp = new Pair<Integer, String>( );
nvp.left("John");
nvp.right(25); // <== note the use of autoboxing.


This class works just like generic types that have only one parameter. Wherever the compiler sees LeftType, it substitutes the type given in the construction of the generic instance—String, in this case. Similarly, RightType is substituted with the second type given in the construction. If the user of the generic class gives only one parameter within the angle brackets, the compiler will indicate an error.

Bounds

When creating a generic type, it is often useful to restrict the type of object that is passed to the generic. For example, if you are developing a generic type that works with the MutableObject class from , you should make sure that the user can use MutableObject classes only as a parameter to your generic type. These restrictions are called bounds and are defined with the following syntax:

public class SomeClass<Type extends MutableObject> {
 // ...
}


This syntax restricts the user, allowing him to give only subclasses of the MutableObject class in the Type parameter to the generic class. Similarly, you can use this syntax with interfaces as shown here:

public class SomeClass<Type implements Comparable> {
 // ...
}


With this declaration, the user must give a type for Type that implements the Comparable interface. In addition to restricting the user to one type, the bounds syntax allows you to call methods on the type without knowing exactly what it is. For example, you can declare a class in the following manner:

public class SomeClass<Type implements Comparable> {
 Type value = null; SomeClass(final Type value) {
 this.value = value;
 }
 public void someMethod( ) {
 Integer someInteger; // ... code
 int result = this.value.compareTo(someInteger);
 // ... other code
 }
}


Since this generic type knows that all users will implement the Comparable interface, it can safely call the compareTo( ) method declared in Comparable. Sometimes it may be necessary to declare bounds that implement a type and extend another type. This can be done with a special syntax:

public class Bounds<Type extends Number & Comparable> {
}


The users of this class must give a type that extends Number and implements Comparable. Any other types will result in an error. Within the class body of Bounds, you can use any members of Number or Comparable. If you don't declare bounds on your types, you can't use any of the methods within that type. Essentially, you can't assume anything about Type that isn't declared, with one exception. The second bounds constraint is called an additional bound. You can add as many additional bounds as you want to specify other interfaces that you want to implement:

public class Bounds<Type extends Number & Comparable & Serializable> {
}


In this code, the type used as a parameter must be a subclass of Number and implement Serializable and Comparable. You can use only one extends bound. This is consistent with the prohibition of multiple inheritance in Java. Since no class can ever extend two different types, it makes no sense to allow declaration of multiple extends bounds. All generic types implicitly declare the bound of extends, java.lang.Object, as normal classes do. Therefore, the following declarations are equivalent:

public class SomeClass<Type> { ... }
public class SomeClass<Type extends Object> { ... }


The fact that these declarations are equivalent has two implications. First, it allows the programmer of the generic type to call methods such as hashCode( ) and equals( ) from the Object class on instances of Type. Second, this bounds restriction prevents the user of the generic type from using primitive types as a parameter.

Screenshot

Given the autoboxing functionality in Java, I don't see why Sun would prevent someone from declaring a generic to use primitives. It would seem that the compiler could simply convert List<int> to List<Integer> automatically, since accessing the methods would autobox the ints.


Wildcards

One problem with generics is figuring out how to deal with the passing of generic classes to other generic classes. For example, suppose a class is designed to manage a collection of list objects. Each of the lists may be a different generic type:

public void someMManagerMethod( ) {
 ListManager<List<Integer>> integerMgr = new ListManager<List<Integer>>( );
 ListManager<List<String>> stringMgr = new ListManager<List<String>>( );
 // . . . other code.
}


In this case, you may want to use your list manager to manage a list of String objects and a list of Integer objects. However, you should make sure that the manager is given only a List and not something else, such as a Class object. This is not a problem, since you can declare a wildcard bounds on an interface. To do this, use the wildcard in the definition of ListManager:

public class ListManager<Type extends List<?>> {
}


The emphasized syntax means that the type the user gives you must be a parameterized version of the List class. Furthermore, in the class body, it doesn't matter what the contained type in the List is. You can declare bounds on wildcards as well:

public class ListManager<Type extends List<? extends Number>> {
}


This syntax allows your list manager to work only with List types that were parameterized using a subtype of Number. The semantics are identical to the bounds discussed earlier.

Lower bounds

Earlier, we discussed a type of bounding called an upper bound of a type. However, there is another type called a lower bound, which can be used on a wildcard:

public class ListManager<Type extends List<? super Integer>> {
}


This syntax allows the user to use the list manager with any List that was parameterized with Integer or a supertype of Integer. In this case, you can use List<Integer>, List<Number>, or List<Object>. However, as with upper bounds, you can't use any of the information implied by a lower bounds.

Raw types

Raw types are collection classes that are not parameterized. These correspond to old types used in JDK 1.4 and earlier. There is no difference in how these old types and raw types are used.

List list2 = new ArrayList( ); list2.add(new Integer(3));
list2.add(new String("Hello"));


However, if you use this code in your program, the compiler will spit out the following warning:

>c:\j2sdk1.5.0\bin\javac -source 1.5 oracle/hcj/tiger/*.java Note: oracle/hcj/tiger/ErasureDemo.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.


If you want more specific information about this warning, you can add the -Xlint:unchecked flag to the invocation of the compiler. Compiling with this flag results in the following:

>c:\j2sdk1.5.0\bin\javac -source 1.5 -Xlint:unchecked oracle/hcj/tiger/*.java oracle/hcj/tiger/ErasureDemo.java:57: warning: unchecked call to add(E) as a member of the raw type java.util.List
 list2.add(new Integer(3));
 ^
oracle/hcj/tiger/ErasureDemo.java:58: warning: unchecked call to add(E) as a member of the raw type java.util.List
 list2.add(new String("Hello"));
 ^


Since these warnings come whenever you use a raw type, cast anything to or from a raw type, or pass a raw type as a parameter, they are not particularly useful. Although they give you information about holes in your type safety, in a large project using legacy libraries, you will get hundreds of them. If there is a hole in your type safety (such as the one discussed in the next section), it isn't likely that the relevant warning will stand out in the crowd.

Erasure

Generics exist only for compile-time type safety. They provide absolutely no runtime type safety, which is one of the major drawbacks to the generics proposal. The reason why there is no runtime type safety is because of erasure. After the compiler has resolved the type safety introduced by generics, it erases the parameterization from the type. Therefore, the information is not available at runtime. The purpose of erasure, as stated by Sun, is to allow class libraries built with an older version of the JDK to be able to run on the JDK 1.5 virtual machine. Erasure is an important concept because it impacts runtime type safety as well as function-based polymorphism. For example, consider the following snippet of code:

public void someMethod(final List<Integer> list) {}
public void someMethod(final List<String> list) {}


Although both of these methods seem distinct, they actually are identical in the mind of the virtual machine. This is because the parameters to the generic classes are erased at runtime, and you end up with the following:

public void someMethod(final List list) {}
public void someMethod(final List list) {}


Since two methods cannot have the same signature, this would cause a compiler error:

C:\dev\hcj\tiger\src>c:\j2sdk1.5.0\bin\javac -source 1.5 -Xlint:unchecked oracle/hcj/tiger/*.java oracle/hcj/tiger/ErasureDemo.java:28: name clash: someMethod(java.util.List<java.
lang.Integer>) and someMethod(java.util.List<java.lang.String>) have the same erasure
 public void someMethod(final List<Integer> list) {}
 ^
oracle/hcj/tiger/ErasureDemo.java:29: name clash: someMethod(java.util.List<java.
lang.String>) and someMethod(java.util.List<java.lang.Integer>) have the same erasure
 public void someMethod(final List<String> list) {}
 ^
2 errors


The compiler complains because both types "have the same erasure." Basically, this means that after the erasure process, both methods look identical. Another naming conflict resulting from erasure is a bit less obvious:

public static Comparable someOtherMethod(final Comparable obj) { return null; }
public static <Type extends Comparable> Type someOtherMethod(final Type obj) { return null; }


Both of these types look radically different, but let's look at them after erasure takes place. With the first version of someOtherMethod( ), no erasure is needed because there are no abstract types. However, erasure needs to be done on the second version. The compiler knows that Type must be of type Comparable, so it erases the emphasized parts in the following snippet:

public static <Type extends Comparable> Type someOtherMethod(final Type obj) { return null; }


This results in:

public static Comparable Type someOtherMethod(final Comparable obj) { return null; }


Since this method is identical to the first version of someOtherMethod( ), the compiler issues an error saying that two methods have the same erasure. Unfortunately, there is no way to fix these problems short of a variety of complicated mechanisms. You could, for example, declare a useless boolean parameter on the second method or give the second method a different name than the first method. This problem is one of the major limitations of the Java generics implementation. Method overriding is one of the cornerstones of developing good quality software. Unfortunately, it cannot be done to methods that use generic types.

Cracking type safety

However, erasure introduces another, even more serious problem. See Example 12-1.

Example 12-1. Cracking type safety with reflection
package oracle.hcj.tiger;
import java.util.*;
import java.lang.reflect.*;
public class ErasureDemo {
 private static List<Integer> someList;
 public static void someMethod(final List<Integer> list) {
 for (Integer element : list) {
 element.intValue( );
 }
 }
 public static void main(final String[] args) {
 someList = new ArrayList<Integer>( );
 someList.add(new Integer(123));
 someList.add(new Integer(456));
 someList.add(new Integer(789));
 // Break type safety on a field.
 try {
 List list = (List)ErasureDemo.class.getDeclaredField("someList").get(null);
 list.add(new String("Hello"));
 for (Object element : list) {
 ((Integer)element).intValue( );
 }
 } catch (final Exception ex) {
 System.out.println( );
 ex.printStackTrace( );
 }
 // Break type safety on a method.
 try {
 List list2 = new ArrayList( ); list2.add(new Integer(3));
 list2.add(new String("Hello"));
 Class[] paramTypes = new Class[] {List.class};
 Object[] methargs = new Object[] {list2};
 Method meth = ErasureDemo.class.getDeclaredMethod(
 "someMethod", paramTypes);
 meth.invoke(null, methargs);
 } catch (final Exception ex) {
 System.out.println( );
 ex.printStackTrace( );
 }
 }
}


This example shows how you can completely crack the type safety of a generic field and a generic method without being blocked by the compiler. Here is the output of this program:

>c:\j2sdk1.5.0\bin\java oracle.hcj.tiger.ErasureDemo java.lang.ClassCastException: java.lang.String
 at oracle.hcj.tiger.ErasureDemo.main(ErasureDemo.java:47)
java.lang.reflect.InvocationTargetException
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
 at java.lang.reflect.Method.invoke(Method.java:326)
 at oracle.hcj.tiger.ErasureDemo.main(ErasureDemo.java:63)
Caused by: java.lang.ClassCastException: java.lang.String
 at oracle.hcj.tiger.ErasureDemo.someMethod(ErasureDemo.java:32)
 ... 5 more


The first ClassCastException is caused by the breaking of someList's type safety. To accomplish this, follow these steps:

  1. Declare a type-safe field called someList in the class scope.

  2. At the start of the main method, fill someList with 50 Integer objects.

  3. Retrieve someList using reflection. Since the resolution of the type happens at runtime, list is a plain old raw List object and therefore isn't type-safe.

  4. Add a String to list, which then adds it to someList, since they both are references to the same list.

Now when you loop through the elements of the list using the for-each loop, the program expects all the members of someList to be type-safe and of type Integer. As it goes through the list, it trips over the string you forced into the list, which causes a ClassCastException. The process of cracking someMethod is similar:

  1. Declare a method called someMethod( ), which takes a type-safe list of Integer objects as its list parameter.

  2. Declare a new list called list2 and fill it with one Integer and one String.

  3. Retrieve a reflective reference to someMethod( ).

  4. Invoke someMethod( ) with reflection and pass the corrupted list to someMethod( ).

During the execution of someMethod( ), the Java runtime trips over the String value in the list and throws the ClassCastException. Throughout this process, the compiler gives you only a warning telling you that you are using an unchecked type. Compiling the program produces only the following warnings:

>c:\j2sdk1.5.0\bin\javac -source 1.5 -Xlint:unchecked oracle/hcj/tiger/*.java oracle/hcj/tiger/ErasureDemo.java:45: warning: unchecked call to add(E) as a member of the raw type java.util.List
 list.add(new String("Hello"));
 ^
oracle/hcj/tiger/ErasureDemo.java:57: warning: unchecked call to add(E) as a member of the raw type java.util.List
 list2.add(new Integer(3));
 ^
oracle/hcj/tiger/ErasureDemo.java:58: warning: unchecked call to add(E) as a member of the raw type java.util.List
 list2.add(new String("Hello"));
 ^
3 warnings


Screenshot

If you hadn't added the -Xlint:unchecked flag to the compiler, you would have received even less information (see ).


Since you get these warnings whenever you deal with raw types, it is likely that the programmers of your app wouldn't pay much attention to them. Furthermore, in a large project in which you access legacy libraries that use only raw types, the likelihood that your programmers will pay attention to these warnings is extremely remote. Currently, Oracle does not have a solution to this massive hole in reflection. Therefore, generics are useless for enforcing type safety throughout a program; they are merely a mechanism to catch errors at compile time. When it comes to critical data models, such as the Online Only Bank data model from , you must still check each of the members of your collections individually. Since corruption at the business data model level can easily kill a business, you simply can't ignore them.

Screenshot

There have been many proposals to implement runtime type safety in which this problem as well as the function polymorphism problem would be prevented. However, they have all been rejected by Sun. In fact, in the generics specification, Sun emphatically states that runtime type safety is not a desirable thing. Obviously, I disagree.


Casting Generics

To cast a generic type from one type to another, it must share a subtype/supertype relationship with the type to which you want to cast it. This is no different from the normal rules of Java casting. However, there is one common mistake that programmers often make:

List<Integer> intlist = new ArrayList<Integer>( );
List<Number> numList = (List<Integer>) intlist; // <== Compiler error


It may seem that the second cast should work, but it doesn't. Although Integer is a subtype of Number, List<Integer> is not a subtype of List<Number>. Furthermore, you can always cast a generic to Object. However, if you try to subsequently cast it back to the generic type, the compiler will tell you that it can't resolve this at compile time by giving you a warning that the cast is unsafe. This also applies to casting a generic List to a nongeneric List. Finally, since all parameter types are erased at runtime, you can't cast a generic type to another type if there isn't a unique erasure for the cast. To illustrate, here is the example from the JSR 14 specification available from the Java Community Process home page:

class Dictionary<A,B> extends Object { ... }
class Hashtable<A,B> extends Dictionary<A, B> { ... }
Dictionary<String,Integer> d;
Object o;
// Then the following are legal:
(Hashtable<String,Integer>)d // legal, has type: Hashtable<String,Integer>
(Hashtable)o // legal, has type: Hashtable
// But the following are not:
(Hashtable<Float,Double>)d // illegal, not a subtype
(Hashtable<String,Integer>)o // illegal, not a unique subtype


Since Dictionary and Hashtable form a hierarchy in this example, you can cast a Dictionary<String, Integer> to a Hashtable<String, Integer> because they are in the same inheritance structure. However, you can't cast a Dictionary<String, Integer> to a Hashtable<Float, Double> because these two types are on different branches. Screenshot-1 shows the relationship between these two hierarchies.

Screenshot-1. Casting in generic hierarchies
Java figs/HCJ_1201.gif

Casting with the last class is a little different. In previous casts, the compiler could resolve the legality at compile time. However, with o, the compiler starts out as a plain Object, so it could be anything. Since the parameters of a generic type are erased at runtime, you essentially have two Hashtable classes in memory, and the compiler doesn't know which to choose. Therefore, the cast is ambiguous.

Generic Methods

In addition to defining generic types, you can define methods that are generic. For example, you may want to define a method that prints the contents of any numerical list to the console:

public class ListUtil {
 public void dumpList(final List<? extends Number> list) {
 int idx = 0;
 for (Object obj : list) {
 System.out.println("[" + idx + "] " + obj.toString( ));
 idx++;
 }
 }
}


This method can take any parameterized List that conforms to the bounds and then print the object to the console. The rules for defining bounds and other semantics are identical to those of generic types. You can also define methods with which you access the enclosed type of the parameterized type using the following syntax:

public final class ListUtil {
 public <Type extends Number> void dumpList2(final List<Type> list) {
 int idx = 0;
 for (Number num : list) {
 System.out.println("[" + idx + "] " + num.intValue( ));
 idx++;
 }
 }
}


In this case, you want to access the type of the component of the List, so you have to declare a name to hold the type that is used to call the method. To do this, place a name in the angled brackets after all of the method modifiers but before the return type of the method. Also, you can use normal bounds rules for types in these declarations.

Inference

Inference is the process by which a runtime environment guesses (infers) the best method to call based on the types of parameters supplied at runtime. Java does not allow inference; instead, you are required to explicitly state the types of your objects when you make a call. The opposite is done in languages such as Lisp, which allow you to define variables without indicating their type and then determine their type at runtime. Another example of a language that uses inference is Python:

C:\dev\jython-2.1>jython Jython 2.1 on java1.4.2_02 (JIT: null)
Type "copyright", "credits" or "license" for more information.
>>> someVar = 5;
>>> print someVar
5
>>> someVar = "Hi there";
>>> print someVar Hi there
>>> print someVar[0];
H


Screenshot

To run this example, use the Jython interpreter. Jython is a free Java implementation of Python that can be found at http://www.jython.org/.


In this Jython session, the variable someVar starts off as an int. Unlike Java, you don't have to declare the variable's type on the declaration line. Its type is inferred by the value given to it. Furthermore, you can change the variable's type in mid-session by assigning it a String value. Finally, you can treat the whole variable as an array by accessing it using square brackets. This is a perfect example of inference. The compiler determines the variable's type by figuring out how it is used. On the other hand, Java would require you to declare the type of someVar and would not allow you to change the type or use it as anything that isn't explicitly declared. However, generics muddies the water. With generics, it is difficult to know which method to call based on the types being sent to the method. Tiger solves this problem with a limited implementation of inference.

Simple generic inference

In Tiger, inference is difficult to understand because there is no runtime manifestation of inference; inference rules are used simply to pick the right method but aren't represented in code at all. To understand the concept of generic inference, study the declaration of the InferenceDemo class in Example 12-2.

Example 12-2. The InferenceDemo class
package oracle.hcj.tiger;
public class InferenceDemo {
 static int callCount = 0;
 /** First. */ public static <Type> Type someMethod(final Type obj) {
 System.out.print(" First==> ");
 System.out.println(obj.getClass( ));
 return obj;
 }
 /** Second. */ public static <Type extends Number> Type someMethod(final Type num) {
 System.out.print(" Second==> ");
 System.out.println(num.getClass( ));
 return num;
 }
 /** Third. */ public static <Type> Type someMethod(final Type obj, List<Type> list) {
 System.out.print(" Third==> ");
 System.out.println(obj.getClass( ));
 for (Type element : list) {
 System.out.println(element);
 }
 return obj;
 }
 /** Third. */ public static <Type> Type someMethod(final Type obj, List<Type> list) {
 System.out.print(" Third==> ");
 System.out.println(obj.getClass( ));
 for (Type element : list) {
 }
 return obj;
 }
 /** Fourth. */ public static <Type> List<Type> someMethod( ) {
 List<Type> result = new ArrayList<Type>( );
 System.out.print(" Fourth==> ");
 System.out.println(result.getClass( ).getComponentType( ));
 return result;
 }
 /** Fifth. */ public static void someMethod(final Object obj) {
 System.out.print(" Fifth==> ");
 System.out.println(obj.getClass( ));
 }
 /** Sixth. */ public static void someMethod(final String str) {
 System.out.print(" Sixth==> ");
 System.out.println(str.getClass( ));
 }
 private static class A {
 }
 private static class B extends A {
 }
 private static class C extends B {
 }
}


Try to call some methods within this class and see what happens. First, start with something simple:

someMethod("Hello");


Screenshot

For the sake of the following examples, assume that the code calling your methods is inside the main( ) method of InferenceDemo.


In this call, the compiler has a choice of three methods that would take a String; the First, Fifth, and Sixth methods can all take a single String parameter. The First method can take a String because a String can be a parameter to Type. The Fifth method can take a String because a String can be converted to an Object. The Sixth method specifically declares that it takes a String. The compiler will choose to call the Sixth method because the Sixth method is the most specific to the given parameters. Another example illustrates this behavior even more clearly:

someMethod(new Integer(5));


In this example, the compiler has two options: the First and Second methods. Since the compiler chooses the most specific method, it will pick the Second method because a generic type with a bounds constraint of extends Number is more specific than a generic type with no bounds constraints. This also works for primitives:

someMethod(5);


In this example, the compiler picks the Second method because the int parameter can be autoboxed into an Integer.

Masking methods with inference

There are some circumstances in which resolving the correct type is impossible:

someMethod(new C( ));


In this case, there is no specific method that accepts a parameter of type C. Therefore, the compiler looks for methods that can take a variable of type Object and finds two: First and Fifth. Since it doesn't have enough information to make a decision, the compiler gives up and issues an error stating that the call is ambiguous. Similarly, the following would fail:

someMethod(new Object( ));


This is a major problem with inference. In fact, the First and Fifth methods can never be called in this code. It would be beneficial if the compiler could tell you this with an error message such as "Method x and method y mask each other and can never be called." Unfortunately, it doesn't issue this error, but persistently issues errors on the attempts to access the Fifth method. However, this applies only to methods that work with the type Object. If you try make this type of masking declaration with another type, such as Comparable, you would get a different compiler error:

/** Seventh. */
public static <Type extends Comparable> void someOtherMethod(final Type num) {}
/** Eighth. */
public static void someOtherMethod(final Comparable num) {}


This would cause the compiler to complain that both types have the same erasure. This error is a little bit more intuitive than the previous error involving the masked methods.

Inference based on return type

One interesting example of inference occurs when there are no parameters to a generic method, such as with the Fourth method in Example 12-2:

 /** Fourth. */ public static <Type> List<Type> someMethod( ) {
 List<Type> result = new ArrayList<Type>( );
 System.out.print(" Fourth==> ");
 System.out.println(result.getClass( ).getComponentType( ));
 return result;
 }


If you call this method, the type of the return value of the method is used to determine the type in the list:

List<Integer> listOne = someMethod( );
listOne.add(new Integer(5));


In this situation, listOne is a list of Integer objects. Therefore, the Type parameter to Fourth becomes the type Integer. The method returns a List parameterized with Integer that can subsequently be filled. Similarly, you can make the method return a List of String objects with similar syntax:

List<String> listTwo = someMethod( );
listTwo.add(new String("Hello"));


In this example, the call to someMethod( ) produced a list of strings because the return type is a List of Strings. Taking this idea further, what would happen if you didn't give a list as a return type:

Object listThree = someMethod( );
System.out.println(listThree.getClass( ));
//listThree.add( ... ); // Can't put anything into this list


Although this call will return the expected ArrayList, you can't actually place anything in the ArrayList, not even null. In a way, it's an orphaned parameterized type.

Generic parameter-based inference

In addition to using inference with normal types, you can use it with parameterized types. Here is the Third method of the InferenceDemo class:

/** Third. */ public static <Type> Type someMethod(final Type obj, List<Type> list) {
 System.out.print(" Third==> ");
 System.out.println(obj.getClass( ));
 for (Type element : list) {
 System.out.println(element);
 }
 return obj;
}


In this method, the type of the list has to be inferred from the types embedded in the parameterized type passed to the list. For example, you can call the method with the following syntax:

someMethod(5, new ArrayList<Integer>( ));


In this call to the method, Type is inferred to be an Integer because an Integer can be used to autobox the literal number 5. Also, the parameter list will be of type Integer because the Integer type is used to parameterize the ArrayList that is passed to the method. However, just because Type appears in both parameters doesn't mean they have to be the same type:

// -- Make some lists --
List<A> listOfA = new ArrayList<A>( );
listOfA.add(new A( ));
List<B> listOfA = new ArrayList<B>( );
listOfB.add(new B( ));
someMethod(new B( ), listOfA);


In this example, Third is called with two different types. The type of obj is B, but the type of list is List<A>. This works because B objects can always be demoted to A objects. However, opposite presents a different situation:

someMethod(new A( ), listOfB);


Since an A object is not necessarily a B object, the compiler can't choose either to use for the inference, so it infers Object for both types. It then calls the method that infers Object and a List<Object> for the parameter types.

Context-sensitive inference

In some situations, inference is based on the context of the call to another method. For example, consider the following calls:

someMethod(5, someMethod( ));
someMethod("Hello", someMethod( ));


In both of these calls, the Fourth method is called to produce a list that can be passed to the Third method. In the first call, the Fourth method produces a List of Integer objects, but in the second call, the Fourth method produces a List of String objects. The Fourth method makes a decision based on the needs of the Third method. Since the Third method needs a List of the type of the first parameter as the second parameter, the Fourth method gives it back a List of the first parameter's type.

Avoiding inference through explicit declaration

The specification for generics says that ambiguities in method calls can be cleared up with the following syntax:

A value = SomeClass.<A>someMethod(new B( ), new ArrayList<A>( )).getClass( ));


This says that the compiler should treat the B object as an A object and explicitly demote it. This syntax can be used to clear up ambiguities in which one or more types could fit the bill.

Screenshot

You have to use the class name for a static method or the keyword this for an instance method before the <A>, or the code won't compile:

A value = <A>someMethod( ); // <== won't compile!!!

Generic Gotchas

Now that you have mastered the intricacies of generics, you should be aware of their limitations, especially those introduced by erasure. There are several gotchas to watch out for when using generics, especially for a former C++ programmer.

Going overboard

You can do a lot of things with generics, both beneficial and damaging. While you can solidify your code into something approaching compile-time type safety, you can also easily turn it into an unreadable disaster by overusing generics and bounds on types. Using generics that have other nested generics is a prime target for bugs. For example, consider the following nested declaration:

public class Crazy<T extends Set<? extends List<? super Integer>> {}


To determine exactly which parameters Crazy takes may seem like a difficule task. But, in fact, it takes any class that extends a Set class that was composed of lists with types of Integer, Number, or Object. So the all of the following would work:

Crazy<Set<List<Integer>>> crazy;
Crazy<Set<List<Object>>> crazy1;
Crazy<Set<List<Number>>> crazy2;
Crazy<HashSet<List<Integer>>> crazy3;
Crazy<TreeSet<ArrayList<Number>>> crazy4;
Crazy<Set<LinkedList<Object>>> crazy5;
// . . . and more . . .


Although this syntax is hard to read, compared to some of the things I have seen on generics forums, it is relatively easy. Although type safety is an important concern in your code, you should watch out for complicated constructs such as the previous syntax and break them into smaller pieces if possible.

Compile-time class conformance

A common misconception about generic types is that they are generated at compile time into new classes. Unlike C++ templates, generics have no runtime analogy. This introduces limitations to generic classes. For example, consider the following code:

public class X {
 public void meth( ) {
 // . . . code
 }
}
public class Y {
 public void meth( ) {
 // . . . code
 }
}
public class SomeClass<Type> {
 Type value = null; public SomeClass(final Type value) {
 this.value = value;
 }
 public void someMethod( ) {
 value.meth( );
 }
}
public class MainClass( ) {
 public static void main(final String[] args) {
 SomeClass<X> x = new SomeClass<X>(new X( ));
 }
}


This code would work properly in C++ because during preprocessing, which occurs before compilation, the type of X would be replaced in SomeClass, and a new class would be generated with the result of the replacement. At the compilation phase, the value.meth( ) line resolves properly to call the meth( ) member of X. This type of late structure checking is called compile-time conformance checking. During this process, it is confirmed that the types used in the parameterized types conform to how the parameterized type uses them. However, in Java there is no preprocessing phase and no generated classes. Therefore, there is no way for the compiler to do compile-time conformance checking. The solution to this problem is to use a bound for the generic type that requires classes being used to implement a specific interface:

public interface LetterClass {
 public void meth( );
}
public class X implements LetterClass {
 public void meth( ) {
 // . . . code
 }
}
public class Y implements LetterClass {
 public void meth( ) {
 // . . . code
 }
}
public class SomeClass<Type implements LetterClass> {
 Type value = null; public SomeClass(final Type value) {
 this.value = value;
 }
 public void someMethod( ) {
 value.meth( );
 }
}
public class MainClass( ) {
 public static void main(final String[] args) {
 SomeClass<X> x = new SomeClass<X>(new X( ));
 }
}


In this case, the interface LetterClass defines meth( ), and classes implementing LetterClass have to implement meth( ). Furthermore, since the generic class knows that all types used in the generic class must implement LetterClass, it can call meth( ) without a problem.

      
Comments