Making collection containers remember element Types is one of the main reasons Generic Types are introduced in jdk1.5.
Generics seem to parameterize the actual type so that it can be passed on at the time of use, or inferred as to what type it represents (such as ArrayList). But the JVM doesn’t recognize ArrayList as a type, per se. It’s just a Java syntactic sugar, a representation at the source level that is just an ArrayList when compiled and loaded by the JVM.
1. Why generics
Let’s start with an example:
List list = new ArrayList();
list.add(100);
list.add("100");
// The first element is of type int, OK
System.out.println((int)list.get(0) + 1);
// The second element is actually String, so ClassCastException is raised
System.out.println((int)list.get(1) + 1);
Copy the code
Before the introduction of generics, the element type of the list was fixed to Object, so you could add elements of any type to the list. This would compile without problems, but it would make sense to fetch them from Object to the actual type, which would easily cause runtime cast exceptions. It is especially hard to tell the real type apart after a loop or multiple passes as a method parameter.
From a practical point of view, we would prefer a container to store elements of the same type or class (including subclasses). Compile-time checking of generics helps avoid accidentally adding elements of other types. Java generics are syntactic sugar that takes the form of type erasure, so Java generics are pseudo-generics, also made to be compatible with older versions.
2. Generic classes, generic interfaces
We can declare line arguments of type on interfaces and classes and generalize them:
public interface Collection<E> extends 可迭代<E> {
boolean add(E e); . }public class HashMap<K.V> extends AbstractMap<K.V>
implements Map<K.V>, Cloneable.Serializable {
public V put(K key, V value) {
/ /...}}Copy the code
Classes with generics need to pass in the actual type when subclassing, or without generics:
class Base<T> {}
/ / error
class Sub extends Base<T> {}
// Ok
class Sub extends Base<String> {}
// Ok
class Sub extends Base {}
// Ok
class Sub<T> extends Base<T> {}
Copy the code
Extends specifies boundaries for generics:
class Base<T extends Comparable & Serializable & Cloneable> {}
class Base<T extends ArrayList & Comparable & Serializable & Cloneable> {}
Copy the code
T is qualified to implement the specified class or interface. Multiple interfaces can be specified, but only one class can be specified and the class must be the first. At compile time, the type of T is replaced with the first class or interface type after extends.
-
Base class hijacks interfaces
abstract class Animal implements Comparable<Animal> {} class Dog extends Animal implements Comparable<Dog> { /** If the CompareTo parameter is Dog or Animal, it does not work @Override public int compareTo(Dog o) { return 0; }}Copy the code
Dog implements Comparable and the generic argument is Dog, but its base class Animal also implements the Comparable interface and inherits a different generic parameter Animal, resulting in a type conflict for the compareTo argument. This phenomenon is known as base class hijacking the interface.
3. Generic methods
Another scenario for using generics is generic methods. If you do not define generic parameters on an interface or class, but want to use generics in a method, you can define a generic method as follows:
public static <T> Set<T> synchronizedSet(Set<T> s) {
return new SynchronizedSet<>(s);
}
// Specify generic parameter types
Collections.<String>synchronizedSet(new HashSet<>());
// Used implicitly, the compiler deduces the actual type
Collections.synchronizedSet(new HashSet<String>());
Copy the code
4. Type wildcard
Suppose there is a way to count the frequency of numbers (<100) in a list:
public static Map<Number, Long> count(List<Number> list) {
return list.stream()
.filter(n -> n.intValue() < 100)
.collect(Collectors.groupingBy(l -> l, Collectors.counting()));
}
Copy the code
Expectations can be like accepting any number of lists:
List<Integer> numsA = Arrays.asList(1.2.3.100.200.300);
/ / error
Map<Number, Long> countA = count(numsA);
List<Double> numsB = Arrays.asList(1D, 2D, 3.55D, 100D, 200D, 330D);
/ / error
Map<Number, Long> countB = count(numsB);
Copy the code
List
,List
is not a subtype of List
. Changing the method arguments to count(List
This compile-time check increases the security of the program, but reduces the flexibility of the code. If there are more than one type to count, we have to write a count method for each type. Code like this.
4.1 the wildcard
To solve the above problem, we can use wildcards:
// List elements can be of any type
public static Map<Number, Long> count(List
list) {
return list.stream()
.map(n -> (Number)n)
.filter(n -> n.intValue() < 100)
.collect(Collectors.groupingBy(l -> l, Collectors.counting()));
}
Copy the code
? It’s a wildcard. It stands for any type. AsList (“1”, “2”, “3”, “4”, “5”) : array
s = Arrays. What happens when you go in?
4.2 Wildcard upper bound
Continuing with the above problem, our real requirement is not to pass on any type, but a subclass of any Number. Further restrictions can be placed on wildcards:
public static Map<Number, Long> count(List<? extends Number> list) {
return list.stream()
.filter(n -> n.intValue() < 100)
.collect(Collectors.groupingBy(l -> l, Collectors.counting()));
}
Copy the code
specifies that the list element of the descendant must be Number and its subclasses, i.e.? The upper bound of the type represented is Number. The wildcard upper bound can also be applied to class or interface generic definitions.
List.add (1) still cannot be passed in the count method; Add a Number or subclass element, WHY?
4.3 Wildcard lower bound
List<? super Number> list = new ArrayList<>();
list.add(Integer.valueOf(1));//ok
list.add(Long.valueOf(2L));//ok
// Because only the next is specified, the element type is Object
Object object = list.get(0);
Copy the code
indicates that the element type of List is Number and its base class, i.e.? The lower bound is Number, and the wildcard lower bound can also be applied to class or interface generic definitions. Why can a wildcard upper bound subclass of Number be added to it?
List
Indicates that the elements in the List must be Number or its base class. Integer and Long are subclasses of Number and must also be subclasses of Number’s parent class. A is A subclass of B, B is A subclass of C, and A must be A subclass of C. So according to THE LSP this is possible.
4.4 Inverter and covariant
Contravariant: A is covariant when A type A can be replaced by A subclass B.
Covariant: When A type A can be replaced by its base class B, A is contravariant.
List
List = new ArrayList
(); List
List = new ArrayList
(); , because Java generics are designed to be immutable (except arrays).
But we can do contravariant and covariant things with wildcards:
/ / covariant
List<? extends Number> list = new ArrayList<Integer>();
/ / inverter
List<? super Integer> list = new ArrayList<Number>();
Copy the code
Another example:
class Animal {}
class Pet extends Animal {}
class Cat extends Pet {}
static class Person<T extends Animal> {
T pet;
}
/ / covariant
Person<? extends Pet> lily = new Person<Cat>();
// error
lily.pet = new Cat();
/ / inverter
Person<? super Pet> alien = new Person<Animal>();
// ok
alien.pet = new Cat();
Copy the code
- When generic parameters are the same, covariance is supported on generic classes, as in
ArrayList<String> -> List<String> -> Collection<String>
- When wildcards are used for generic parameters, covariation is supported both on the generic class itself and on the generic parameter type, as in
Collection<? extends Number>
, subtypes can beList<? extends Number>
.Set<? extends Number>
It could beCollection<Integer>
.Collection<Long>
We know by passingHashSet<Long>
isCollection<? extends Number>
Subtype of. - Contains multiple generic type parameters, applying the above rules to each type parameter separately,
HashMap<String, Long>
isMap<? extends CharSequence, ? extends Number>
Subtype of.
4.5 PECS
When should I use the wildcard upper bound and when should I use the wildcard lower bound? Effective Java introduces PECS (producer-extends, consumer-super), which uses extends when an object generates generic data and super when an object receives (consumes) generic data.
/** * Collections #copy method * SRC generates generic data required by copy, uses extens * dest to consume generic data generated by copy, and uses super */
public static <T> void copy(List<? super T> dest, List<? extends T> src)
Copy the code
4.6 Wildcards and generic methods
Implement the previous count method with the generic method:
/** Performs the same function as the previous wildcard, and you can add a new element */ to the method
public static <T extends Number> Map<T, Long> count(List<T> list) {
return list.stream()
.filter(n -> n.intValue() < 100)
.collect(Collectors.groupingBy(l -> l, Collectors.counting()));
}
Copy the code
Here’s another 🌰, assuming a utility class method that adds a non-empty number to the list of inheritors:
public static void safeAdd(List<? extends Number> list, Number num) {
if (num == null) {
return;
}
//error, although the scope of generics is limited with wildcards, the exact type is still uncertain
list.add(num);
}
// Replace it with:
public static <T extends Number> void safeAdd(List<T> list, T num) {
if (num == null) {
return;
}
Num is the same type as the list element
list.add(num);
}
Copy the code
Conclusion:
- Wildcards are used when there is no need to change the container in the method, and generic methods are used otherwise
- Use generic methods when the return value of other method parameters has a dependency on the generic parameter
5. Type Erasure
Generic parameters are all syntactically defined in Java and are compiler oriented. When run in the JVM, there are no generics, the types are erased, and all generic types are replaced with Object or wildcard upper bound types, or List if they are container types such as List.
ArrayList<Integer> listA = new ArrayList<>();
ArrayList<String> listB = new ArrayList<>();
/ / listA and by runtime type is Java. Util. ArrayList. Class, return true
System.out.println(listA.getClass() == listB.getClass());
Copy the code
Because of type erasure, you cannot use generics in static variables, static methods, static initializer blocks, or obj instanceof java.util.ArrayList
to determine generic classes defined in the interface.
6. Get generic information through reflection
The reason for generic erasure is that the runtime does not have access to generic information on a class. However, for the field of the class, the generic information on the method of the class is stored in the class file constant pool (Signature Attrbute, to be exact) when the compiler compiles it, so the generic information of the field and method can be obtained by reflection.
Java.lang. reflect provides Type(which is the parent interface for all types in Java, and class implements Type) and several subinterfaces to get relevant generic information, using List as an example:
TypeVariable: indicates the TypeVariable, E
ParameterizedType: indicates type parameters, such as List. The parameter is String
WildcardType: WildcardType, such as List
? Extends Number>? ,? extends Number
GenericArrayType: a generic array, such as List[], whose base type is ParameterizedType List< java.lang.integer >
See Javadoc for a simple demonstration of the API:
public class GenericCls<T> {
private T data;
private List<String> list;
private List<Integer>[] array;
public <T> List<String> strings(List<T> data) {
return Arrays.asList(data.toString());
}
public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException {
Class<GenericCls> cls = GenericCls.class;
System.out.println("============== class - GenericCls ==============\n");
TypeVariable<Class<GenericCls>> classTypeVariable = cls.getTypeParameters()[0];
System.out.println(classTypeVariable.getName());
Field field = cls.getDeclaredField("list");
Type genericType = field.getGenericType();
ParameterizedType pType = (ParameterizedType) genericType;
System.out.println("============== filed - list ==============\n");
System.out.println("type: " + genericType.getTypeName());
System.out.println("rawType: " + pType.getRawType());
System.out.println("actualType: " + pType.getActualTypeArguments()[0]);
Method method = cls.getDeclaredMethod("strings", List.class);
Type genericParameterType = method.getGenericParameterTypes()[0];
ParameterizedType pMethodType = (ParameterizedType) genericParameterType;
System.out.println("============== method - strings parameter ==============\n");
System.out.println("type: " + genericParameterType.getTypeName());
System.out.println("rawType: " + pMethodType.getRawType());
System.out.println("actualType: " + pMethodType.getActualTypeArguments()[0]);
Field array = cls.getDeclaredField("array");
GenericArrayType arrayType = (GenericArrayType) array.getGenericType();
System.out.println("============== filed - array ==============\n");
System.out.println("array type: " + arrayType.getTypeName());
ParameterizedType arrayParamType = (ParameterizedType) arrayType.getGenericComponentType();
System.out.println("type: " + arrayParamType.getTypeName());
System.out.println("rawType: " + arrayParamType.getRawType());
System.out.println("actualType: " + arrayParamType.getActualTypeArguments()[0]); }}Copy the code
I’ll cover reflection and generics in more detail in another article
7. Generics and arrays
Java arrays are covariant :Pet[] Pets = new Cat[10]; You can create an array without generics and then strong-roll it. You can also declare a reference to a generic array.
Person<Pet>[] people = new Person<Pet>[10];//error
Person<Pet>[] people = new Person[10];//ok
Person<Pet>[] people = (Person<Pet>[])new Person[10];//ok
public static void consume(Person<? extends Pet>[] people){}//ok
Copy the code
Question: Why can’t exception classes use generics?
Class (bytecode) files
Welcome to my personal wechat blog