Chapter 4. Declarations
This chapter discusses how to declare a generic class. It describes constructors, static members, and nested classes, and it fills in some details of how erasure works.
Constructors
In a generic class, type parameters appear in the header that declares the class, but not in the constructor:
classPair<T,U>
{ private final T first; private final U second; publicPair
(T first, U second) { this.first = first; this.second = second; } public T getFirst() { return first; } public U getSecond() { return second; } }
The type parameters T
and U
are declared at the beginning of the class, not in the constructor. However, actual type parameters are passed to the constructor whenever it is invoked:
Pair<String, Integer> pair1 = new Pair<String, Integer>
("one",2);
assert pair1.getFirst().equals("one") && pair1.getSecond() == 2;
A common mistake is to forget the type parameters when invoking the constructor:
Pair pair2 = new Pair<String, Integer>("one",2);
This mistake produces a warning, but not an error. It is taken to be legal, because Pair
is treated as a raw type, a type containing no parametric type information but which can be converted to the corresponding parameterized type, generating only an unchecked warning. The effect is to turn off type checking for all subsequent uses of pair2
: it will be possible to assign Pair
values of any type to it without further warnings or errors. Raw types are discussed further in [Link to Come], and unchecked types in “Unchecked casts”.
Records can be parameterised in just the same way as regular classes:
record Pair<T,U>(T first, U second) {}
Static Members
Compilation by erasure means that at run time parameterised types are replaced by the corresponding raw type: for example, the interfaces List<Integer>
, List<String>
, and List<List<String>>
are all implemented by a single interface, namely List
:
List<Integer> ints = Arrays.asList(1,2,3); List<String> strings = Arrays.asList("one","two"); assert ints.getClass() == strings.getClass();
We see here that the class object associated with a list of integers at run time is the same as the one associated with a list of strings.
One consequence is that static members of a generic class are shared across all instantiations of that class, including instantiations at different types. Static members of a class cannot refer to the type parameter of a generic class, and when accessing a static member the class name should not be parameterized.
For example, here is a class, Cell<T>
, in which each cell has an integer identifier and a value of type T
:
class Cell<T> { private final int id; private final T value; private final static AtomicInteger count = new AtomicInteger(); private static int nextId() { return count.getAndIncrement(); } public Cell(T value) { this.value = value; id = nextId(); } public T getValue() { return value; } public int getId() { return id; } public static int getCount() { return count.get(); } }
A static field, count
, is used to allocate a distinct identifier to each cell. For the count, an AtomicInteger
is used to ensure that unique identifiers are generated even under concurrent access. The static getCount
method returns the current count.
Here is code that allocates a cell containing a string and a cell containing an integer, which are allocated the identifiers 0
and 1
, respectively:
Cell<String> a = new Cell<String>("one"); Cell<Integer> b = new Cell<Integer>(2); assert a.getId() == 0 && b.getId() == 1 && Cell.getCount() == 2;
Static members are shared across all instantiations of a class, so the same count is incremented when allocating either a string or an integer cell.
Because static members are independent of any type parameters, we are not permitted to follow the class name with type parameters when accessing a static member:
Cell.
getCount();// ok
Cell<Integer>
.getCount();// compile-time error
Cell<?>.
getCount();// compile-time error
The count is static, so it is a property of the class as a whole, not any particular instance.
For the same reason, you can’t refer to a type parameter anywhere in the declaration of a static member. Here is a second version of Cell
, which attempts to use a static variable to keep a list of all values stored in any cell:
class Cell2<T> { private final T value; privatestatic
List<T>
values = new ArrayList<T>
();// illegal
public Cell2(T value) { this.value=value; values.add(value); } public T getValue() { return value; } publicstatic
List<T>
getValues() { return values; }// illegal
}
Since the class may be used with different type parameters at different places, it makes no sense to refer to T
in the declaration of the static field values
or the static method getValues
, and these lines are reported as errors at compile time. If we want a list of all values kept in cells, then we need to use a list of objects, as in the following variant:
class Cell2<T> { private final T value; privatestatic
List<Object>
values = new ArrayList<Object>
();// ok
public Cell2(T value) { this.value=value; values.add(value); } public T getValue() { return value; } publicstatic
List<Object>
getValues() { return values; }// ok
}
This code compiles and runs with no difficulty:
Cell2<String> a = new Cell2<String>("one"); Cell2<Integer> b = new Cell2<Integer>(2); assert Cell2.getValues().equals(List.of("one",2));
Nested Classes
Java permits nesting one class inside another. If the outer class has type parameters and the inner class is a member class—that is, not static—then type parameters of the outer class are visible within the inner class.
Example 4-1 shows a class implementing collections as a singly-linked list. This class extends java.util.AbstractCollection
, so it only needs to define the methods size
, add
, and iterator
(see Chapter 16). It contains an inner class, Node
, for the list nodes, and an anonymous inner class implementing Iterator<E>
. The type parameter E
is in scope within both of these classes.
Example 4-1. Type parameters are in scope for member classes
class LinkedCollection<E> extends AbstractCollection<E> { private class Node { private E element; private Node next = null; private Node(E elt) { element = elt; } } private Node first = new Node(null); private Node last = first; private int size = 0; public LinkedCollection() {} public LinkedCollection(Collection<? extends E> c) { addAll(c); } public int size() { return size; } public boolean add(E elt) { last.next = new Node(elt); last = last.next; size++; return true; } public Iterator<E> iterator() { return new Iterator<E>() { private Node current = first; public boolean hasNext() { return current.next != null; } public E next() { if (current.next != null) { current = current.next; return current.element; } else throw new NoSuchElementException(); } public void remove() { throw new UnsupportedOperationException(); } }; } }
For contrast, Example 4-2 shows a similar implementation, but this time the inner Node
class is static, and so the type parameter E
is not in scope for this class. Instead, the inner class is declared with its own type parameter, T
. Where the previous version referred to Node
, the new version refers to Node<E>
. The anonymous iterator class in the preceding example has also been replaced by a static inner class, again with its own type parameter.
If the node classes had been made public rather than private, you would refer to the node class in the first example as LinkedCollection<E>.Node
, whereas you would refer to the node class in the second example as LinkedCollection.Node<E>
.
Example 4-2. Type parameters are not in scope for static inner classes
class LinkedCollection<E> extends AbstractCollection<E> { private static class Node<T> { private T element; private Node<T> next = null; private Node(T elt) { element = elt; } } private Node<E> first = new Node<E>(null); private Node<E> last = first; private int size = 0; public LinkedCollection() {} public LinkedCollection(Collection<? extends E> c) { addAll(c); } public int size() { return size; } public boolean add(E elt) { last.next = new Node<E>(elt); last = last.next; size++; return true; } private static class LinkedIterator<T> implements Iterator<T> { private Node<T> current; public LinkedIterator(Node<T> first) { current = first; } public boolean hasNext() { return current.next != null; } public T next() { if (current.next != null) { current = current.next; return current.element; } else throw new NoSuchElementException(); } public void remove() { throw new UnsupportedOperationException(); } } public Iterator<E> iterator() { return new LinkedIterator<E>(first); } }
Of the two alternatives described here, the second is preferable. Member classes are implemented by including a reference to the enclosing instance, since they may, in general, access components of that instance. Static inner classes are usually both simpler and more efficient.
How Erasure Works
The erasure of a type is defined as follows: drop all type parameters from parameterized types, and replace any type variable with the erasure of its bound, or with Object
if it has no bound, or with the erasure of the leftmost bound if it has multiple bounds. Here are some examples:
-
The erasure of
List<Integer>
,List<String>
, andList<List<String>>
isList
. -
The erasure of
List<Integer>[]
isList[]
. -
The erasure of
List
is itself, similarly for any raw type . -
The erasure of
int
is itself, similarly for any primitive type. -
The erasure of
Integer
is itself, similarly for any type without type parameters. -
The erasure of
T
in the definition oftoList
(see “Generic Methods and Varargs”) isObject
, becauseT
has no bound. -
The erasure of
T
in the definition ofmax
(see “Maximum of a Collection”) isComparable
, becauseT
has boundComparable<? super T>
. -
The erasure of
T
in the final definition ofmax
(see “Multiple Bounds”) isObject
, becauseT
has boundObject & Comparable<T>
and we take the erasure of the leftmost bound. -
The erasures of
S
andT
in the definition ofcopy
(see “Multiple Bounds”) areReadable and Appendable
, becauseS
has boundReadable & Closeable
andT
has bound Appendable & Closeable. -
The erasure of
LinkedCollection<E>.Node
orLinkedCollection.Node<E>
(see “Nested Classes”) isLinkedCollection.Node
.
In Java, two methods of the same class cannot have the same signature—that is, the same name and parameter types. Since generics are implemented by erasure, it also follows that two distinct methods cannot have signatures with the same erasure. A class cannot overload two methods whose signatures have the same erasure, and a class cannot implement two interfaces that have the same erasure.
For example, here is a class with two convenience methods. One adds together every integer in a list of integers, and the other concatenates together every string in a list of strings:
class Overloaded { public staticint sum(List<Integer>
ints) { int sum = 0; for (int i : ints) sum += i; return sum; } public staticString sum(List<String>
strings) { StringBuffer sum = new StringBuffer(); for (String s : strings) sum.append(s); return sum.toString(); } }
Here are the erasures of the declarations of the two methods:
int sum(List) String sum(List)
But it is the signatures alone, not the return types, that allow the Java compiler to distinguish different method overloads. In this case the erasures of the signatures of both methods are identical:
sum(List)
So a name clash is reported at compile time.
For another example, here is a bad version of the Integer
class, that tries to make it possible to compare an integer with either an integer or a long:
class Integer implementsComparable<Integer>, Comparable<Long>
{ //compile-time error, cannot implement two interfaces with same erasure
private final int value; ... public intcompareTo(Integer i)
{ return (value < i.value) ? -1 : (value == i.value) ? 0 : 1; } public intcompareTo(Long l)
{ return (value < l.intValue()) ? -1 : (value == l.intValue()) ? 0 : 1; } ... }
If this were supported, it would, in general, require a complex and confusing definition of bridge methods (see “Bridges”). The simplest and most understandable option by far is to ban this case.
Get Java Generics and Collections, 2nd Edition now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.