
Java 5 brought generics to the Java language. In this article, I introduce you to generics and discuss generic types, generic methods, generics and type inference, generics controversy, and generics and heap pollution.
What are generics?
Generics are a collection of related language features that allow types or methods to operate on objects of various types while providing compile-time type safety. Generics features address the problem of java.lang.ClassCastException
s being thrown at runtime, which are the result of code that is not type safe (i.e., casting objects from their current types to incompatible types).
Consider the following code fragment, which demonstrates the lack of type safety (in the context of the Java Collections Framework’s java.util.LinkedList
class) that was common in Java code before generics were introduced:
List doubleList = new LinkedList(); doubleList.add(new Double(3.5)); Double d = (Double) doubleList.iterator().next();
Although the goal of the above program is to store only java.lang.Double
objects in the list, nothing prevents other kinds of objects from being stored. For example, you could specify doubleList.add("Hello");
to add a java.lang.String
object. However, when storing another kind of object, the final line’s (Double)
cast operator causes ClassCastException
to be thrown when confronted with a non-Double
object.
Because this lack of type safety isn’t detected until runtime, a developer might not be aware of the problem, leaving it to the client (instead of the compiler) to discover. Generics help the compiler alert the developer to the problem of storing an object with a non-Double
type in the list by allowing the developer to mark the list as containing only Double
objects. This assistance is demonstrated below:
List<Double> doubleList = new LinkedList<Double>(); doubleList.add(new Double(3.5)); Double d = doubleList.iterator().next();
List<Double>
now reads “List
of Double
.” List
is a generic interface, expressed as List<E>
, that takes a Double
type argument, which is also specified when creating the actual object. The compiler can now enforce type correctness when adding an object to the list — for instance, the list could store Double
values only. This enforcement removes the need for the (Double)
cast.
Discovering generic types
A generic type is a class or interface that introduces a set of parameterized types via a formal type parameter list, which is a comma-separated list of type parameter names between a pair of angle brackets. Generic types adhere to the following syntax:
class identifier<formalTypeParameterList> { // class body } interface identifier<formalTypeParameterList> { // interface body }
The Java Collections Framework offers many examples of generic types and their parameter lists (and I refer to them throughout this article). For example, java.util.Set<E>
is a generic type, <E>
is its formal type parameter list, and E
is the list’s solitary type parameter. Another example is java.util.Map<K, V>
.
A parameterized type is a generic type instance where the generic type’s type parameters are replaced with actual type arguments (type names). For example, Set<String>
is a parameterized type where String
is the actual type argument replacing type parameter E
.
The Java language supports the following kinds of actual type arguments:
- Concrete type: A class or other reference type name is passed to the type parameter. For example, in
List<Animal>
,Animal
is passed toE
. - Concrete parameterized type: A parameterized type name is passed to the type parameter. For example, in
Set<List<Shape>>
,List<Shape>
is passed toE
. - Array type: An array is passed to the type parameter. For example, in
Map<String, String[]>
,String
is passed toK
andString[]
is passed toV
. - Type parameter: A type parameter is passed to the type parameter. For example, in
class Container<E> { Set<E> elements; }
,E
is passed toE
. - Wildcard: The question mark (
?
) is passed to the type parameter. For example, inClass<?>
,?
is passed toT
.
Each generic type implies the existence of a raw type, which is a generic type without a formal type parameter list. For example, Class
is the raw type for Class<T>
. Unlike generic types, raw types can be used with any kind of object.
Declaring and using generic types in Java
Declaring a generic type involves specifying a formal type parameter list and accessing these type parameters throughout its implementation. Using the generic type involves passing actual type arguments to its type parameters when instantiating the generic type. See Listing 1.
Listing 1: GenDemo.java
(version 1)
class Container<E> { private E[] elements; private int index; Container(int size) { elements = (E[]) new Object[size]; index = 0; } void add(E element) { elements[index++] = element; } E get(int index) { return elements[index]; } int size() { return index; } } public class GenDemo { public static void main(String[] args) { Container<String> con = new Container<String>(5); con.add("North"); con.add("South"); con.add("East"); con.add("West"); for (int i = 0; i < con.size(); i++) System.out.println(con.get(i)); } }
Listing 1 demonstrates generic type declaration and usage in the context of a simple container type that stores objects of the appropriate argument type. To keep the code simple, I’ve omitted error checking.
The Container
class declares itself to be a generic type by specifying the <E>
formal type parameter list. Type parameter E
is used to identify the type of stored elements, the element to be added to the internal array, and the return type when retrieving an element.
The Container(int size)
constructor creates the array via elements = (E[]) new Object[size];
. If you’re wondering why I didn’t specify elements = new E[size];
, the reason is that it isn’t possible. Doing so could lead to a ClassCastException
.
Compile Listing 1 (javac GenDemo.java
). The (E[])
cast causes the compiler to output a warning about the cast being unchecked. It flags the possibility that downcasting from Object[]
to E[]
might violate type safety because Object[]
can store any type of object.
Note, however, that there is no way to violate type safety in this example. It’s simply not possible to store a non-E
object in the internal array. Prefixing the Container(int size)
constructor with @SuppressWarnings("unchecked")
would suppress this warning message.
Execute java GenDemo
to run this application. You should observe the following output:
North South East West
Bounding type parameters in Java
The E
in Set<E>
is an example of an unbounded type parameter because you can pass any actual type argument to E
. For example, you can specify Set<Marble>
, Set<Employee>
, or Set<String>
.
Sometimes you’ll want to restrict the types of actual type arguments that can be passed to a type parameter. For example, perhaps you want to restrict a type parameter to accept only Employee
and its subclasses.
You can limit a type parameter by specifying an upper bound, which is a type that serves as the upper limit on the types that can be passed as actual type arguments. Specify the upper bound by using the reserved word extends
followed by the upper bound’s type name.
For example, class Employees<E extends Employee>
restricts the types that can be passed to Employees
to Employee
or a subclass (e.g., Accountant
). Specifying new Employees<Accountant>
would be legal, whereas new Employees<String>
would be illegal.
You can assign more than one upper bound to a type parameter. However, the first bound must always be a class, and the additional bounds must always be interfaces. Each bound is separated from its predecessor by an ampersand (&
). Check out Listing 2.
Listing 2: GenDemo.java
(version 2)
import java.math.BigDecimal; import java.util.Arrays; abstract class Employee { private BigDecimal hourlySalary; private String name; Employee(String name, BigDecimal hourlySalary) { this.name = name; this.hourlySalary = hourlySalary; } public BigDecimal getHourlySalary() { return hourlySalary; } public String getName() { return name; } public String toString() { return name + ": " + hourlySalary.toString(); } } class Accountant extends Employee implements Comparable<Accountant> { Accountant(String name, BigDecimal hourlySalary) { super(name, hourlySalary); } public int compareTo(Accountant acct) { return getHourlySalary().compareTo(acct.getHourlySalary()); } } class SortedEmployees<E extends Employee & Comparable<E>> { private E[] employees; private int index; @SuppressWarnings("unchecked") SortedEmployees(int size) { employees = (E[]) new Employee[size]; int index = 0; } void add(E emp) { employees[index++] = emp; Arrays.sort(employees, 0, index); } E get(int index) { return employees[index]; } int size() { return index; } } public class GenDemo { public static void main(String[] args) { SortedEmployees<Accountant> se = new SortedEmployees<Accountant>(10); se.add(new Accountant("John Doe", new BigDecimal("35.40"))); se.add(new Accountant("George Smith", new BigDecimal("15.20"))); se.add(new Accountant("Jane Jones", new BigDecimal("25.60"))); for (int i = 0; i < se.size(); i++) System.out.println(se.get(i)); } }
Listing 2’s Employee
class abstracts the concept of an employee that receives an hourly wage. This class is subclassed by Accountant
, which also implements Comparable<Accountant>
to indicate that Accountant
s can be compared according to their natural order, which happens to be hourly wage in this example.
The java.lang.Comparable
interface is declared as a generic type with a single type parameter named T
. This interface provides an int compareTo(T o)
method that compares the current object with the argument (of type T
), returning a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object.
The SortedEmployees
class lets you store Employee
subclass instances that implement Comparable
in an internal array. This array is sorted (via the java.util.Arrays
class’s void sort(Object[] a, int fromIndex, int toIndex)
class method) in ascending order of the hourly wage after an Employee
subclass instance is added.
Compile Listing 2 (javac GenDemo.java
) and run the application (java GenDemo
). You should observe the following output:
George Smith: 15.20 Jane Jones: 25.60 John Doe: 35.40
Considering wildcards
Let’s say you want to print out a list of objects, regardless of whether these objects are strings, employees, shapes, or some other type. Your first attempt might look like what’s shown in Listing 3.
Listing 3: GenDemo.java
(version 3)
import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class GenDemo { public static void main(String[] args) { List<String> directions = new ArrayList(); directions.add("north"); directions.add("south"); directions.add("east"); directions.add("west"); printList(directions); List<Integer> grades = new ArrayList(); grades.add(new Integer(98)); grades.add(new Integer(63)); grades.add(new Integer(87)); printList(grades); } static void printList(List<Object> list) { Iterator<Object> iter = list.iterator(); while (iter.hasNext()) System.out.println(iter.next()); } }
It seems logical that a list of strings or a list of integers is a subtype of a list of objects, yet the compiler complains when you attempt to compile this listing. Specifically, it tells you that a list-of-string cannot be converted to a list-of-object, and similarly for a list-of-integer.
The error message you’ve received is related to the fundamental rule of generics: