Generics
Item26: Don't use raw types
The raw type of List<E>
is List
.
The use of raw types means that the object like List would accept all kinds of instances. The insert process would work woithout errors, but when we retrieve the isnatnce from the List and cast the instance to a certain type, which would generally pre-defined and fixed, a ClassCastException would be thrown.
In this way, the use of raw type makes us lose the safety and expressiveness benefits of generics. If arbitatry objects would be inserted into a collection, it could be made like this: List<Object>
. The key is, though String
is the subclass of Object
, the List<String>
is not the subclass of List<Object>
, and in this way the former would not be converted to the latter, which guarantees the safety.
Sometimes we don't know the exact types when writing code, and then we could use ?
to replace the exact type in generic objects: Set<?>
. This is called unbounded wildcard types. No elements other than null could be put into a Collections<?>, which means it could only be read. This is not for raw type Collection.
Only use raw types in class literals, like List.class
, or more detailed:
if(o instanceof Set) { // raw type
Set<?> s = (Set<?>) o; // wildcard type
}
Item27: Eliminate unchecked warngings
By eliminating unchecked warn gins, we can ensure our code is typesafe. Like:
Set<Lark> exaltation = new HashSet(); // this would generate a unchecked warnging
Set<Lark> exaltation = new HashSet<>(); // this is typesafe
If an unchecked warning could not be eliminated, and we can prove our code is type safe, we can use the annotation: @SuppressWarngins("unchecked")
. But never overuse of it and constrain it in the smallest scope. And we had better document the details of the usage, like reasons.
Item28: Prefer lists to arrays
First, arrays are covariant. If A is a subtype of B, then A[] is also subtype of B[]. But generics are not that same.
The advantage of generic is then obvious: we could find errors at compile time by using generics, instead of runtime with arrays.
// fails at runtime
Object[] objectArray = new Long[1];
objectArray[0] = "xxx"; // ArrayStoreException.
// fails at runtime
List<Object> ol = new ArrayList<Long>(); // incompatiable type
ol.add("xxx");
Second, arrays are reified. So arrays know and enforce their element type at runtime. But with erasure, generics lose the informationof their elements and can only enforce them at compile time. So in this way, it would be illegal to create an array of generic type, a parameterized type, or a type parameter, like new List<E>[]
, new List<String>[]
and new E[]
.
If this is legal, then the below code would fail at runtime:
List<String>[] stringLists = new List<String>[1]; // assume it is legal
List<Integer> intList = List.of(42); // legal
Object[] objects = stringLists; // legal
objects[0] = intList; // legal
String s = stringLists[0].get(0); // legal at comile time, but fails at runtime
The key is, the runtime type of List<String>[]
is simply List[]
. These non-reifiable type's runtime representation contains less information than its compile-time representation.
In a word, prefer lists to arrays, and never use them in mix.
Item29: Favor generic types
Do not use something like Object[]
, but use the generic form: E[]
, which would compile, but we can put it further: (E[]) new Object[]
. Though we would get an uncheck cast warning, we know it is typesafe and we can ignore the warning. Also, we can solve it by casting the type when retrieving elements: return (E)elements[index]
.
The two methods: the formmer one does the unchecked cast when storing elements, the latter one does it when retriecing elements. Generally speaking, the former one is preferred.
We can use bounded type parameter to ensure the actual type parameter E to be a subtype of some certain types bu using this: List<E extends xxx>
.
Item30: Favor generic methods
Static utility methods that operate on parameterized types are usually generic.
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
Among the code, the first <E>
in between the modifier and return type is the type parameter list. The flaw is that all the three sets, including two input parameters and one return type, are of the same type. We can deal with it by using bounded wildcard types.
A type parameter could be bounded by some expression involving that type parameter itself. This is called recursive type bound:
public static <E extends Comparable<E>> E max(Collection<E> c);
The E in the method declaration indicates that E must be a subclass of E that implemets Comparable interface. This is why it is called recursive type bound.
Item31: Use bounded wildcards to increase API flexibility
For producer:
// retrieve elements from src and add to self-contained collection
public void pushAll(Iterator<E> src); // only accept one certain type at runtime
public void pushAll(Iterator<? extends E> src); // accept E and all its subclasses
For consumer:
// pop elements from self-contained collection to dst
public void popAll(Collection<E> dst); // deficient
public void popAll(Collection<? super E> dst); // good
The basic rule is: PECS, which stands for producer-extends
, consumer-super
.
For users of the class, they should think little about the wildcard types, like they do not exist, and use the class like it always is.
Tips: Comparables are always consumers, as well as Comparators.
The wildcard is required to support types that do not implement Comparable or Comparator directly but extend a type that does.
Another rule: If a type parameter appears only once in a method declaration, replace it with a wildcard. Unbounded wildcard for unbunded type parameter, and bounded wildcard for bounded parameter.
Item32: Combine generics and varargs judiciously
Whenever a varargs method is created, a related array is created to store the arguments. But we know generics are not compatiable with arrays, so a waring about heap pollution would be generated, which indicates that a variable of parameterized type refers to an object that is not of that type. It is unsafe to store a value in a generic varargs array parameter.
The only reason we get a warning instead of an error is: it would be useful in practice. We can use @SafeVarargs
annotation to avoid the warning.
The key to ensure the varargs parameter array is safe: only to transimit a variable number of arguments from the caller to the method.
Generally speaking, it is unsafe to give nother method access to a generic varargs parameter array. But it is safe to pass the array to another varargs method that is annotated with @SafeVarargs
and pass the array to a non-varargs method that merely computes some function of the contents of the array.
The rule of using @SafeVarargs
: Use it on every method with a varargs parameter of a generic or parameterized type. This annotation is only valid on methods that can't be overridden.
Also, one alternative is to replace array with list:
static <T> List<T> flatten(List<List<? extends T>> lists) {
// ommited
}
audience = flatten(List.of(friends, romans, countrymen));
The complier can prove that the method is typesafe.
Item33: Consider typesafe heterogeneous containers
The idea is to parameterize the key instead of the container. For example, we can have the class objects for the type to play the part of the parameterized key, because the class Class
is generic. That is to say, the type of a class literal is not simply Class
, but Class<T>
. It is called type token.
// typesafe heterogeneous container pattern
public class Favorites {
public <T> void putFavorite(Class<T> type, T instance);
public <T> T getFavorite(Class<T> type);
}
public static void main(String[] args) {
Favorites f = new Favorites();
f.putFavorite(String.class, "Java");
f.putFavorite(Class.class, Favorites.class);
String favoriteString = f.getFavorite(String.class);
Class<?> favoriteClass = f.getFavorite(Class.class);
}
This is typesafe, because it would always return the right type instance. It is also heterogenous, because all the keys are different types. So that the Favorites
is a typesafe heterogenous container.
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), instance);
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}
It s not the type of the map that is a wildcard type but the type of its key.
The Map does not guarantee the type relationship between keys and values, because the value is only Object
. Also, we need to use dynamic cast in getFavorite
method to cast the Object
to the type stored in the key.
We can have this container to achieve runtime safety:
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(type, type.cast(instance));
}