Generic Types
A generic type is a generic class or interface that is parameterized over types. The following Box class will be modified to demonstrate the concept.
A Simple Box Class
Begin by examining a nongeneric Box class that operates on objects of any type. It only needs to provide two methods: set, which adds an object to the box, and get, which retrieves it:
public class Box { private Object object; public void set(Object object) { this.object = object; } public Object get() { return object; } }
Since its methods accept or return an Object, you are free to pass in whatever you want, provided that it is not one of the primitive types. At compile time, there is no way to verify how the class is used. One part of the code may place an Integer in the box and expect to get Integers out of it, while another part of the code may mistakenly pass in a String, resulting in a runtime error.
A Generic Version of the Box Class
A generic class is defined with the following format:
class name<T1, T2, . . . , Tn> { /* . . . */ }
The type parameter section, delimited by angle brackets (<>), follows the class name. It specifies the type parameters (also called type variables) T1, T2, . . ., and Tn.
To update the Box class to use generics, you create a generic type declaration by changing the code public class Box to public class Box<T>. This introduces the type variable T, which can be used anywhere inside the class.
With this change, the Box class becomes the following:
/** * Generic version of the Box class. * @param <T> the type of the value being boxed */ public class Box<T> { // T stands for "Type" private T t; public void set(T t) { this.t = t; } public T get() { return t; } }
As you can see, all occurrences of Object are replaced by T. A type variable can be any nonprimitive type you specify: any class type, interface type, array type, or even another type variable. This same technique can be applied to create generic interfaces.
Type Parameter Naming Conventions
By convention, type parameter names are single, uppercase letters. This stands in sharp contrast to the variable naming conventions that you already know about, with good reason: without this convention, it would be difficult to tell the difference between a type variable and an ordinary class or interface name.
The most commonly used type parameter names are as follows:
- E—Element (used extensively by the Java Collections Framework)
- K—Key
- N—Number
- T—Type
- V—Value
- S, U, V, and so on—Second, third, and fourth types
You’ll see these names used throughout the Java SE Application Programming Interface (API) and the rest of this chapter.
Invoking and Instantiating a Generic Type
To reference the generic Box class from within your code, you must perform a generic type invocation, which replaces T with some concrete value, such as Integer:
Box<Integer> integerBox;
You can think of a generic type invocation as being similar to an ordinary method invocation, but instead of passing an argument to a method, you are passing a type argument (Integer in this case) to the Box class itself.
Like any other variable declaration, this code does not actually create a new Box object. It simply declares that integerBox will hold a reference to a “Box of Integer,” which is how Box<Integer> is read.
An invocation of a generic type is generally known as a parameterized type. To instantiate this class, use the new keyword, as usual, but place <Integer> between the class name and the parentheses:
Box<Integer> integerBox = new Box<Integer>();
The Diamond
In Java SE 7 and later, you can replace the type arguments required to invoke the constructor of a generic class with an empty set of type arguments (<>) as long as the compiler can determine, or infer, the type arguments from the context. This pair of angle brackets (<>) is informally called the diamond. For example, you can create an instance of Box<Integer> with the following statement:
Box<Integer> integerBox = new Box<>();
For more information on diamond notation and type inference, see the “Type Inference” section later on in this chapter.
Multiple Type Parameters
As mentioned previously, a generic class can have multiple type parameters. One example is the generic OrderedPair class, which implements the generic Pair interface:
public interface Pair<K, V> { public K getKey(); public V getValue(); } public class OrderedPair<K, V> implements Pair<K, V> { private K key; private V value; public OrderedPair(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public V getValue() { return value; } }
The following statements create two instantiations of the OrderedPair class:
Pair<String, Integer> p1 = new OrderedPair<String, Integer>("Even", 8); Pair<String, String> p2 = new OrderedPair<String, String>("hello", "world");
The code, new OrderedPair<String, Integer>, instantiates K as a String and V as an Integer. Therefore, the parameter types of OrderedPair’s constructor are String and Integer, respectively. Due to autoboxing, it is valid to pass a String and an int to the class.
As mentioned previously, because a Java compiler can infer the K and V types from the declaration OrderedPair<String, Integer>, these statements can be shortened using diamond notation:
OrderedPair<String, Integer> p1 = new OrderedPair<>("Even", 8); OrderedPair<String, String> p2 = new OrderedPair<>("hello", "world");
To create a generic interface, follow the same conventions as you would to create a generic class.
Parameterized Types
You can also substitute a type parameter (e.g., K or V) with a parameterized type (e.g., List<String>). Here is an example, using OrderedPair<V, K>:
OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>( . . . ));
Raw Types
A raw type is the name of a generic class or interface without any type arguments. Here is an example, given the generic Box class:
public class Box<T> { public void set(T t) { /* . . . */ } // . . . }
To create a parameterized type of Box<T>, you supply an actual type argument for the formal type parameter T:
Box<Integer> intBox = new Box<>();
If the actual type argument is omitted, you create a raw type of Box<T>:
Box rawBox = new Box();
Therefore, Box is the raw type of the generic type Box<T>. However, a nongeneric class or interface type is not a raw type.
Raw types show up in legacy code because lots of API classes (such as the Collections classes) were not generic prior to the Java SE Development Kit (JDK) 5.0. When using raw types, you essentially get pregenerics behavior: a Box gives you Objects. For backward compatibility, assigning a parameterized type to its raw type is allowed:
Box<String> stringBox = new Box<>(); Box rawBox = stringBox; // OK
However, if you assign a raw type to a parameterized type, you get a warning:
Box rawBox = new Box(); // rawBox is a raw type of Box<T> Box<Integer> intBox = rawBox; // warning: unchecked conversion
You also get a warning if you use a raw type to invoke generic methods defined in the corresponding generic type:
Box<String> stringBox = new Box<>(); Box rawBox = stringBox; rawBox.set(8); // warning: unchecked invocation to set(T)
The warning shows that raw types bypass generic type checks, deferring the catch of unsafe code to runtime. Therefore, you should avoid using raw types.
The “Type Erasure” section later on in this chapter has more information on how the Java compiler uses raw types.
Unchecked Error Messages
As mentioned previously, when mixing legacy code with generic code, you may encounter warning messages similar to the following:
Note: Example.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details.
This can happen when using an older API that operates on raw types, as shown in the following example:
public class WarningDemo { public static void main(String[] args){ Box<Integer> bi; bi = createBox(); } static Box createBox(){ return new Box(); } }
The term unchecked means that the compiler does not have enough type information to perform all type checks necessary to ensure type safety. The unchecked warning is disabled, by default, though the compiler gives a hint. To see all unchecked warnings, recompile with -Xlint:unchecked.
Recompiling the previous example with -Xlint:unchecked reveals the following additional information:
WarningDemo.java:4: warning: [unchecked] unchecked conversion found : Box required: Box<java.lang.Integer> bi = createBox(); ^ 1 warning
To completely disable unchecked warnings, use the -Xlint:-unchecked flag. The @SuppressWarnings("unchecked") annotation suppresses unchecked warnings. If you are unfamiliar with the @SuppressWarnings syntax, see Chapter 4, “Annotations.”