Do you really understand generics in Java?
Generics are an important feature introduced in JDK1.5, and there is a lot of generics-related code in the JDK collection package. But do you really understand generics, and do you have any misconceptions about generics? For example, type erasure to generics.
public interface Container<K.V> {
V find(K key );
void add(K key, V value);
boolean contain(K key);
}
// The compiler erases the type after compiling, and the type parameter E becomes Object
public interface Container {
Object find(Object key );
void add(Object key, Object value);
boolean contain(Object key);
}
Copy the code
The Container<K, V> type parameter K and V cannot be retrieved by the runtime. If you think you can’t, then you really have a misunderstanding about type erasure of generics. Run the javap -v container. class command to view the section codes of container. class. The following is some information about the Container. Class section.
Constant pool:
/ /... Omit the part
#6= Utf8 (TK;) TV;/ /... Omit the part
#9= Utf8 (TK; TV;) V/ /... Omit the part
#12= Utf8 (TK;) Z #13= Utf8 <K:Ljava/lang/Object; V:Ljava/lang/Object; >Ljava/lang/Object;/ /... Omit the part
{
public abstract V find(K); descriptor: (Ljava/lang/Object;) Ljava/lang/Object; flags: ACC_PUBLIC, ACC_ABSTRACT Signature: #6 // (TK;) TV;
public abstract void add(K, V); descriptor: (Ljava/lang/Object; Ljava/lang/Object;) V flags: ACC_PUBLIC, ACC_ABSTRACT Signature: #9 // (TK; TV;) V
public abstract boolean contain(K); descriptor: (Ljava/lang/Object;) Z flags: ACC_PUBLIC, ACC_ABSTRACT Signature: #12 // (TK;) Z
}
Signature: #13 //
Ljava/lang/Object;
Copy the code
The actual types of generics you might see preserved by the class’s additional Signature information are the parameter types K and V, which in turn are stored in #13 of the constant pool.
#13= Utf8 <K:Ljava/lang/Object; V:Ljava/lang/Object; >Ljava/lang/Object;Copy the code
The additional Signature information of find, add, and contain in the Container class also contains the actual type information of the generic type. To obtain information about type parameters K and V of containers, perform the following operations:
public static void main(String args[]) {
TypeVariable<Class<Container>>[] tvArray = Container.class.getTypeParameters();
for(TypeVariable<Class<Container>> typeVariable : tvArray) { System.out.println(typeVariable.getTypeName()); }}Copy the code
The use of generic wildcards, for example, follows PECS rules, which use <? Extends T>, which uses <? Super T>, but why follow PECS principles? Does it always go wrong? This article delves into bytecode to see what the compiler does when dealing with generics. We will also introduce the invariance, covariance and contravariance of generics, and then analyze with examples why Java generics do not support covariance by themselves and why they support contravariance by introducing lower bound wildcards. Of course, this article also introduces generics related reflection knowledge, such as ParamerterizedType, TypeVariable, WildcardType, GenericArrayType, etc.
Why generics
Generics are also known as parameterized types. If they are called parameterized types, they must be related to the common understanding of parameters and types. A parameter, usually thought of as a variable in a method, is passed in when we define the method and the actual value is passed in when we call the method. A type is usually understood as the concept of class. The parameterized type can be interpreted as parameterizing a type, defining it without knowing what type it is, and passing in the type to be used when it is actually used. Normally method calls pass in the value of a variable, whereas generics pass in the concrete type.
1.1 Enhanced compiler type detection
Before answering the question of why generics were introduced into the Java language, let’s take a look at how some scenarios were handled before generics were introduced. Assume to calculate the specific salary of the sales staff, the salary of the sales staff includes basic salary, commission expenses, travel expenses and other expenses. You now define an interface to Calculator that defines a calculateFee method specifically for calculating fees. And presents the basic salary, fees, the cost of the business trip expenses corresponding computing implementation class BaseSalaryCalculator, CommissionFeeCalculator, TravelAllowanceCalculator respectively.
import java.math.BigDecimal;
public interface Calculator {
// Calculate the cost
BigDecimal calculateFee(a);
}
import java.math.BigDecimal;
// Basic salary calculation
public class BaseSalaryCalculator implements Calculator {
private final BigDecimal baseSalary;
public SalaryCalculator(BigDecimal baseSalary) {
this.baseSalary = baseSalary;
}
BigDecimal calculateFee(a) {
// The calculation logic is omitted....}}import java.math.BigDecimal;
// Commission fee calculation
public class CommissionFeeCalculator implements Calculator {
private final BigDecimal salesAmount;
public ValueCalculator(BigDecimal salesAmount) {
this.salesAmount = salesAmount;
}
BigDecimal calculateFee(a) {
// The calculation logic is omitted....}}public class TravelAllowanceCalculator implements Calculator {
private final Integer travelDays;
private final BigDecimal travelCost;
public TravelAllowance(Integer travelDays, BigDecimal travelCost) {
this.travelDays = travelDays;
this.travelCost = travelCost;
}
BigDecimal calculateFee(a) {
// The calculation logic is omitted....}}Copy the code
When calculating the salary of the salesman, a calculateTotalSalary method is defined, and the parameter calculatorContainer of this method is a List, which stores the basic salary, commission expense, travel expense, etc. The calculateTotalSalary method traverses the List internally and takes out the calculators stored in various cost calculations before. Without generics, only Object can be stored in the List, so the type of elements obtained from the List is also Object. However, since calculateFee method of Calculator would be called to calculate the cost, Object could only be forcibly converted into Calculator, and calculateFee method of Calculator would be called to calculate the cost, and the salary of the salesman would be returned after the sum.
BigDecimal calculateTotalSalary(List calculatorContainer) {
BigDecimal salary = BigDecimal.ZERO;
for (int i = 0 ; i < calculatorContainer.size(); i++) {
Calculator calculator = (Calculator) calculatorContainer.get(i);
salary = salary.add(calculator.calculateFee());
}
return salary;
}
Copy the code
The calculateTotalSalary method above seems fine, but what if someone calls the calculateTotalSalary method and passes an incorrect plus one object in the parameter calculatorContainer that is not of Calculator type? The (Calculator) CalculatorContainer.get (I) line above will throw a ClassCastException at runtime. The point is that this exception is only sensed at runtime, and the potential problem is not known at all. More stringent type checking, of course, should be caught by the compiler at compile time, rather than relegating the problem to runtime. One reason generics were introduced was to enhance type detection at compile time. With generics, the calculateTotalSalary method above can be modified to look like this.
BigDecimal calculateTotalSalary(List<Calculator> calculatorContainer) {
BigDecimal salary = BigDecimal.ZERO;
for (int i = 0 ; i < calculatorContainer.size(); i++) {
// No more casts are forced. The compiler inserts the corresponding casts
Calculator calculator = calculatorContainer.get(i);
salary = salary.add(calculator.calculateFee());
}
return salary;
}
Copy the code
If you want to add a non-Calculator object to calculateTotalSalary parameter calculatorContainer, the compiler will report an error during compilation.
1.2 Support generic programming reuse code
On the other hand, the introduction of generics has made code more reusable. In the preceding Container<K, V>, you can pass in a String for the type parameter K and an Entity for the type parameter V.
import java.util.concurrent.ConcurrentHashMap;
public abstract AbstractContainer interface Container<String.Entity> {
private Map<String, Entity> container = new ConcurrentHashMap();
Entity find(Entity key) { returncontainer.get(key); }void add(String key, Entity value) {container.put(key, value); }boolean contain(String key) { return container.containsKey(key);}
}
Copy the code
In this way, you do not need to define a certain Container interface for a specific Container. Instead, you can pass the specified type to type parameters K and V. The AbstractContainer implementation above is a Map. JDK collection types such as List have become much easier to handle with the introduction of generics.
What does compiler type erasure include
Generics were introduced into the Java language to support compile-time safer type detection and generic programming. To implement generics, the Java compiler uses type erasure. The Java compiler’s type erasures actually involve the substitution of type parameters in generics, the insertion of type conversions if necessary, and the generation of bridge methods that support generics polymorphism.
2.1 Type Parameter Replacement
The Java compiler replaces type parameters in generics during compilation. If a boundary is specified, the type argument is replaced with the first boundary. If no boundary is specified, the default Object parent is used.
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData(a) { return data; }
// ...
}
Copy the code
Because the type parameter T does not specify a boundary, the compiler replaces T with Object after type erasers.
public class Node {
private Object data;
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData(a) { return data; }
// ...
}
Copy the code
Look again at the example of a type parameter specifying a boundary.
public class Node<T extends Comparable<T> & Serializable> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData(a) { return data; }
// ...
}
Copy the code
The Java compiler replaces the type parameter T with its first type boundary, Comparable, at compile time. Here the type parameter T specifies that the upper bounds are both Comparable and Serializable. However, the first boundary Comparable is substituted when the type is erased.
public class Node {
private Comparable data;
private Node next;
public Node(Comparable data, Node next) {
this.data = data;
this.next = next;
}
public Comparable getData(a) { return data; }
// ...
}
Copy the code
Note: Although Java supports generics, generics are actually syntactic sugar because there is no equivalent generic type in the JVM and Java generics are handled at compile time, not run time. For example, if you define an instance variable List of type List, there is no way to add a value of type Integer directly to the List (the compiler will report an error), but you can still reflect a value of type Integer into the List.
2.2 Insert type conversion
When the Java compiler compiles the source code to handle generics, it inserts the Checkcast instruction into the generated bytecode if necessary. The checkcast directive checks whether a cast can be cast. If it can, the checkcast instruction does not change the operand stack, and subsequent assignments can, otherwise it throws a ClassCastException.
import java.util.List;
import java.util.ArrayList;
public class Main {
public static void main(String args[]) {
List<String> list = new ArrayList<>();
list.add("aaa");
String s = list.get(0); }}Copy the code
If you do not have a Java compiler to compile and insert the checkcast directive, you will get the following result based on the type parameter substitution described above.
import java.util.List;
import java.util.ArrayList;
public class Main {
public static void main(String args[]) {
List list = new ArrayList();
list.add("aaa");
// List.get (0) should return Object instead of String. Without the compiler's insert checkcast instruction, the compilation here will be an exception.
String s = list.get(0); }}Copy the code
Run the javac -g main. Java command to generate bytecode, where -g specifies the parameter to generate all debugging information. Then run javap -v main. calss to view the section information of main. class.
/ /... Omit the part
/ /... Constant pool information
Constant pool:
#1 = Methodref #9.#29 // java/lang/Object."<init>":()V
#2 = Class #30 // java/util/ArrayList
/ /... Constant pool information
#6 = InterfaceMethodref #32.#34 // java/util/List.get:(I)Ljava/lang/Object;
#7 = Class #35 // java/lang/String
/ /... Constant pool information
{ / /... Omit the part
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: ldc #4 // String aaa
11: invokeinterface #5.2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;) Z
16: pop
17: aload_1
18: iconst_0
19: invokeinterface #6.2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
24: checkcast #7 // class java/lang/String
27: astore_2
28: return
LineNumberTable:
line 5: 0
line 6: 8
line 7: 17
line 8: 28
LocalVariableTable:
Start Length Slot Name Signature
0 29 0 args [Ljava/lang/String;
8 21 1 list Ljava/util/List;
28 1 2 s Ljava/lang/String;
LocalVariableTypeTable:
Start Length Slot Name Signature
8 21 1list Ljava/util/List<Ljava/lang/String; >;/ /... Omit the part
}
Copy the code
LineNumberTable records the mapping between the start lines of source Code and the start lines of JVM instructions in bytecode Code. Corresponding to line 17 of the JVM directive. String s = list.get(0); Is broken down into the following JVM instructions
17: aload_1 18: iconst_0 19: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object; 24: checkcast #7 // class java/lang/String 27: astore_2
The corresponding JVM instructions have the following meanings:
17: aload_1 // Push the reference type variable list onto the operand stack. The 1 in ALOad_1 represents the variable with Slot 1 in LocalVariableTable, i.e. List.
18: iconst_0 // push the constant integer constant 0 onto the operand stack, where 0 is the constant 0 corresponding to list.get(0).
19: invokeinterface #6.2 Java /util/ list. get:(I)Ljava/lang/Object; It's just a get method on a List
24: checkcast #7 // Check whether the list. get result can be converted to String. If not, throw ClassCastExecption.
27: astore_2 // remove the top element of the operand stack and assign it to s in the LocalVariableTable. The 2 in astore_2 represents the variable with Slot 2 in LocalVariableTable (s).
Copy the code
Note:
1, LocalVariableTable is a LocalVariableTable, aload_n and astore_n, n are to operate the variables corresponding to solt is n in the LocalVariableTable.
Before invoking invokevirtual, the object reference and method parameters are pushed onto the operand stack. At the end of the call, the object reference and method parameters are pushed off the stack. If the method returns a value, the return value is pushed to the top of the operand stack.
The checkcast directive checks whether the cast can be performed. If it can, the checkcast directive does not change the operand stack, otherwise it throws a ClassCastException.
2.3 Generating bridge methods support polymorphism
First, let’s take a quick look at what a Bridge Method is, also known as a Synthetic Method. It is actually a Method synthesized by the compiler that calls the methods defined in our class. Let’s take a look at some examples and get a feel for bridging methods.
interface Animal {
Animal getAnimal(a);
}
class Dog implements Animal {
@Override
public Dog getAnimal(a) {
return newDog(); }}Copy the code
This defines an Animal interface, which defines a getAnimal method that returns Animal, and then implements Dog, which implements Animal, but shrinks the return type of getAnimal to Dog. If you are careful, you will notice that the Override annotation is added to the getAnimal method, but a method rewrite is defined only when the method name, return value, number of arguments, and type are the same. In theory, the code should display a red warning on IDEA, but in practice it does not. This is because the overridden definition has been expanded since JDK1.5, and the method return type can be changed, but it must be a subclass of the original method return value. The compiler generates the bridge method with the following pseudocode.
class Dog implements Animal {
public Dog getAnimal(a) {
return new Dog();
}
//Bridge method generated by the compiler
public Animal getAnimal(a) {
return ((Dog)this).getAnimal(); }}Copy the code
Looking at the bytecodes in javap -c doc.class, you can see that there are two getAnimal methods in the bytecodes.
public Dog getAnimal(a);
descriptor: ()LDog;
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: new #2 // class Dog
3: dup
4: invokespecial #3 // Method "<init>":()V
7: areturn
public Animal getAnimal(a);
descriptor: ()LAnimal;
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #4 // Method getAnimal:()LDog;
4: areturn
Copy the code
The second getAnimal method that returns type Animal is the bridge method generated by the compiler, and in its code attribute **1: invokevirtual #4 ** actually calls the getAnimal method that returns type Dog.
Note:
* * 1, descriptor: (a) LAnimal; For the method descriptor, () means that the parameter is null, L in LAnimal means that the return type is a reference type, and Animal in LAnimal means that the return reference type is Animal **
2, flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC indicate method accessors. ACC_PUBLIC indicates that the method is PUClic, ACC_BRIDGE indicates that the method is a bridge, and ACC_SYNTHETIC indicates that the method is generated by a compiler
To reinforce the case for generic-supported polymorphism in generating bridge methods, let’s see if the compiler does not generate bridge methods. What might be the problem with polymorphism of generics? We first define a generic Node interface with a setData method.
public class Node<T> {
private T data;
public void setData(T data) {
System.out.println("Node.setData");
this.data = data; }}Copy the code
Then we define the implementation class ConcreteNode, specify the type parameter as Integer, and override the setData method in the ConcreteNode class.
public class ConcreteNode extends Node<Integer> {
@Override
public void setData(Integer data) {
System.out.println("ConcreteNode.setData");
super.setData(data); }}Copy the code
The Node class should look something like this after the type substitution.
public class Node {
private Object data;
public void setData(Object data) { this.data = data;}
}
Copy the code
ConcreteNode setData(Integer data) cannot Override Node setData(Object data) because the ConcreteNode parameter type is Integer. The Node method takes an Object parameter. This makes setData(Integer Data) in ConcreteNode a method overload. Do setData(Integer data) in ConcreteNode override or override methods? If you are careful, you will notice that the setData(Integer data) method has Override annotation, and the above code does not generate a red warning in IDEA. It is clear that the compiler considers this to be a method Override and not a method Override. If it is a method Override and the IDEA annotation is Override, a red warning will appear.
Do the old concretenode. class javap -v concretenode. class to see what the compiler is doing.
public void setData(java.lang.Integer); descriptor: (Ljava/lang/Integer;) V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: invokespecial #2 // Method Node.setData:(Ljava/lang/Object;) V
5: return
LineNumberTable:
line 11: 0
line 12: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this LConcreteNode;
0 6 1 data Ljava/lang/Integer;
public void setData(java.lang.Object); descriptor: (Ljava/lang/Object;) V flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC Code: stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: checkcast #3 // class java/lang/Integer
5: invokevirtual #4 // Method setData:(Ljava/lang/Integer;) V
8: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this LConcreteNode;
Copy the code
As you can see, after compilation, there are two setData methods in the final bytecode. The first is the setData method in the ConcreteNode code, the second is the bridge method generated by the Java compiler, and the first setData method is called inside the second method, the following instruction corresponding to the second method.
5: invokevirtual #4 // Method setData:(Ljava/lang/Integer;) V
Copy the code
The actual bridge method pseudocode generated by the Java compiler looks like this.
public class ConcreteNode extends Node {
public void setData(Integer data) {
System.out.println("ConcreteNode.setData");
super.setData(data);
}
//Bridge method generated by the compiler
public void setData(Object data) { setData((Integer) data); }}}Copy the code
How to use wildcards in generics
One of the biggest headaches in learning generics is the upper bound wildcards and the lower bound wildcards. Before briefly introducing upper bound wildcards and lower bound wildcards. It is worth looking at the invariance, covariance, and contravariance of programming languages first.Copy the code
3.1 Invariant, covariant and contravariant
Essentially, invariant, covariant, and contravariant programming languages describe how subtype relationships are affected by type conversions, which actually revolves around the central question of whether subtypes can be implicitly converted to parent types, that is, whether variables of subtypes can be implicitly assigned to variables of parent types. If there are types A and B, and the type conversion F, and ≤ denotes A subtype relationship (e.g., A ≤ B denotes A subtype of B), then
- If from
A, B or less
To derive theF or less f (A) (B)
thef
It’s covariant.- If from
A, B or less
To derive theF or less f (B) (A)
thef
It’s contravariant.- If none of the above is true then f is constant.
I’m a little confused by the above definition, but let’s look at a concrete example. Suppose f(A) = List and List is defined as follows
class List<T> {... }Copy the code
So is F invariant, covariant, or contravariant? Invariant means List is a subclass of List; Contravariant means List is a subtype of List; Invariant means that there is no subtype relationship between lists. Generics are obviously immutable in Java because there are no subtype relationships between lists.
Let’s take another example. If f(A) = A[], is f invariant, covariant, or contravariant? Invariant implies that Integer[] is a subclass of Number[]; Inverse means that Number[] is a subtype of Integer[]; Invariant means that there is no subtype relationship between Integer[] and Number[]. Arrays are obviously covariant in Java, because Integer[] is a subtype of Number[]. If you’re still a little confused, take a look at whether core subtype variables can be implicitly assigned to parent variables.
List<Number> numberList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
Java generics are immutable, subtype variables cannot be implicitly assigned to parent variables
numberList = integerList;
Java generics are immutable. Variables of parent type cannot be implicitly assigned to variables of child type
integerList = numberList;
Number[] numberArray = new Number[]{};
Integer[] integerArray = new Integer[]{};
Java arrays are covariant and subtype variables cannot be implicitly assigned to parent variables
numberArray = integerArray;
Copy the code
So why doesn’t Java support assignment to a List variable? A container for Nubmer, not Integer?
List<Number> numberList = new ArrayList<>();
// Compile properly
numberList.add(new Integer(1));
List<Integer> integerList = new ArrayList<>();
Java generics are immutable, subtype variables cannot be implicitly assigned to parent variables
numberList = integerList;
Copy the code
A List can be put into an Integer, but you cannot assign a List variable to a List variable. Imagine what would happen if Java generics supported covariance?
List<Number> numberList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
// Assume that Java generics are covariant and compile successfully
numberList = integerList;
// It was compiled normally
numberList.add(new BigDecimal("9.9"));
Copy the code
If Java generics were covariant, then that would be equivalent to putting BigDecimal variables into an integerList container. This would contradict the previous statement that Java introduced generics to enforce type detection. BigDecimal can be considered a type of Integer, and subclasses are not.
As mentioned earlier, another reason Java introduced generics was to support generic programming. The biggest advantage of generic programming is that code is more reusable. Now if you want to define a way to sort an array of some type.
import java.util.Comparator;
public class Collections {
public static <T> void sort(List<T> list, Comparator<T> comparator) {...}
}
Copy the code
The sort method signature for the Collections class above seems to have met our needs by specifying both the list to be sorted in the sort method to be generic and the comparator comparator to be generic. This sort can handle sorting of arrays of different types by specifying the corresponding type when called and then implementing the comparator for that type. Is that really the case? Does this method need to be extended in terms of generality?
public class Assets {
private BigDecimal assetsValue; // Asset value
}
public class Food extends Assets {
private Integer retentionPeriod; / / shelf life
}
public class Meat extends Food {
private BigDecimal fatContent; // Fat content
public Meat(a) {}
public Meat(BigDecimal fatContent) { this.fatContent = fatContent;}
}
public class Fruit extends Food {
private BigDecimal vitaminContent; // Vitamin content
public Fruit(a) {}
public Fruit(BigDecimal vitaminContent) { this.vitaminContent = vitaminContent;}
}
// Asset value comparator
public class AssetsValueComparator implements Comparator<Assets> {
public int compare(Assets o1, Assets o2) {...}
}
// Food shelf life comparator
public class FoodRetentionPeriodComparator implements Comparator<Food> {
public int compare(Food o1, Food o2) {...}
}
// Fat content comparator
public class FatContentComparator implements Comparator<Meat> {
public int compare(Meat o1, Meat o2) {...}
}
// Vitamin content comparator
public class VitaminContentComparator implements Comparator<Fruit> {
public int compare(Fruit o1, Fruit o2) {...}
}
Copy the code
The categories of Assets, Food, Meat and Fruit are defined above. The parent of Food is Assets, and the parent of Meat and Fruit are all Assets. Then the comparators corresponding to Assets, Food, Meat and Fruit are defined according to their attributes. The previously defined Collections sort method, we can store the Food list sorted according to Food retention period, then to specify FoodRetentionPeriodComparator comparator.
List<Food> foods = new ArrayList<>();
foods.add(new Meat()); foods.add(new Fruit());
Collections.sort(foods, new FoodRetentionPeriodComparator());
Copy the code
To sort the Fruit list by the vitamin content of the Fruit, specify the VitaminContentComparator.
List<Fruit> fruit = new ArrayList<>();
fruit.add(new Fruit(new BigDecimal("3.00"))); fruit.add(new Fruit(new BigDecimal("7.00")));
Collections.sort(fruit, new VitaminContentComparator());
Copy the code
Now, what if we wanted to rank the asset value of the list of food stores? Is it ok to specify the comparator as AssetsValueComparator?
List<Food> foods = new ArrayList<>(); foods.add(new Meat()); foods.add(new Fruit()); // Compile error collections.sort (food, new AssetsValueComparator());Copy the code
Public static void sort(List List, Comparator Comparator) public static void sort(List List, Comparator Comparator) When AssetsValueComparator is defined, the specified type parameter is Assets. The actual generic type is Comparator. When a Comparator variable is assigned to a Comparator variable, it is not possible to assign a value to a Comparator variable. Unless generics introduce something new to support contravariance, we know that a Comparator variable can be assigned to a Comparator variable if the Comparator is contravariant. To support the above scenario, Java introduces the upper bound wildcard
with the lower bound card
to support covariant and contravariant generics respectively. With inversion, the compilation errors in the previous code disappear by simply changing the sort method signature for Collections above to the one below.
import java.util.Comparator;
public class Collections {
// Change the parameter to comparator
, let it support inverse
public static <T> void sort(List<T> list, Comparator<? super T> comparator) {... } } List<Food> foods =new ArrayList<>();
foods.add(new Meat()); foods.add(new Fruit());
// Compilation succeeded
Collections.sort(food, new AssetsValueComparator());
Copy the code
If you open the source of the JAVa.util.Collections class in the JDK and look at the sort method signature, you’ll see that the method signature is the same as our custom method signature above.
public static <T> void sort(List<T> list, Comparator<? super T> c) {... }Copy the code
3.2 Covariant and upper bound wildcards
With the above introduction to invariant, covariant, and contravariant, it is relatively easy to look at upper bound wildcards. The upper bound wildcard <? Extends T> is used to support covariation, which means that you can assign variables of type List to List<? Extends Food>. You can also assign variables of type List to List<? Extends Food> variable, as long as the specified type parameter is a subclass of Food. The upper bound wildcard implies at most the meaning of type T.
The lower bound wildcard Plate
covers the blue area in the image above.
Plate<? extends Fruit> fruitPlate;
Plate<Apple> applePlate = new Plate<>();
// Compile properly, upper bound wildcard support covariant
fruitPlate = applePlate;
Plate<Banana> bananaPlate = new Plate<>();
// Compile properly, upper bound wildcard support covariant
fruitPlate = bananaPlate;
Copy the code
Note, however, that since the compiler does not know the upper bound wildcard Plate<? FruitPlate extends Fruit> fruitPlate; fruitPlate extends Fruit> fruitPlate; fruitPlate; fruitPlate; In general, the upper bound wildcard <? Is defined in class and method bodies. Variables of extends T> are meaningless. Define the upper bound wildcard <? Extends T> parameter.
class Plate<T>{
private T item;
public Plate(T t){item=t; }public void set(T t){item=t; }public T get(a){return item;}
}
void tackleFruitPlate(Plate<? extends Fruit> fruitPlate) {
// Error compiling
fruitPlate.add(new Apple());
// Error compiling
fruitPlate.add(new Banana());
// Error compiling
fruitPlate.add(new Fruit());
// Compilation succeeded
Fruit fruit = fruitBasket.get();
}
Copy the code
The tackleFruitPlate method signature above allows it to receive both parameters for processing plates and parameters for processing plates because the upper bound wildcards support covariance.
3.3 Invert and Lower Bound Wildcards
I also mentioned inverters earlier, and the introduction of the lower bound wildcard <? Super T> to support inverse. The lower bound wildcard implies at least one meaning of type T. Such as Plate <? Super Fruit> plate, which means the concept of Fruit on the plate. Because the lower bound wildcard <? Super T> supports invert, so you can assign Plate< Fruit> type variables and Plate< Food> type variables to Plate<? Super Fruit> type variable.
Plate
cover the red area in the image below.
Plate<? super Fruit> plate = new Plate<>();
Plate<Fruit> fruitPlate = new Plate<>();
// Normal compilation, lower bound wildcard support invert
plate = fruitPlate;
Plate<Food> foodPlate = new Plate<>();
// Normal compilation, lower bound wildcard support invert
plate = foodPlate;
Plate<Apple> applePlate = new Plate<>();
// Failed to compile because lower wildcards do not support covariance
plate = applePlate;
Copy the code
Because the lower bound wildcard implies at least one meaning of type T, it can be stored as a concrete itself object or subclass object. However, it is important to note that it does not support storing superclass objects, and when an element is retrieved from them, it can only be of Object type.
/ / plate is put
Plate<? super Fruit> plate = new Plate<Fruit>();
// Compile properly, can be stored in its own object
p.add(new Fruit());
// It compiles properly and can be saved into the subclass object
p.add(new Apple());
// Failed to compile properly, cannot store to the parent object
p.add(new Food());
// Read things can only be stored in the Object class.
Apple newFruit3 = p.get(); //Error
Fruit newFruit1 = p.get(); //Error
Object newFruit2 = p.get();
Copy the code
3.4 The PECS principles
With all that said, let’s go back to the classic PECS principle, which states that to produce objects for retrieval, use
, which uses
. Oracle actually refers to this principle as the “in” and “out” principle in its official documentation, which is more straightforward. Java introduced the upper bound wildcard
extends T> with the lower wildcard
extends and super are pretty obscure. Covariant and contravariant methods were introduced to make method parameter variable assignments more extensible. The addition of extends and super makes Java generics look pretty confusing. In contrast, Kotlin’s generic out and in keywords are more apt and understandable. If variables are classified from the perspective of their functions, they can be classified into in-type variables and out-type variables.
- An IN-type variable provides data to the code. Imagine a copy method with two arguments: copy(SRC, dest). The SRC parameter provides the data to copy, so it is the “in” parameter.
- A variable of type “out”, which is typically used to hold data used elsewhere. In the copy example copy(SRC, dest), the dest parameter accepts data, so it is the “out” parameter.
There are also variables that are both in and out variables. Use **”in” and “out” principle** when deciding whether to use wildcards and which type of wildcard is appropriate. **”in” and “out” principle ** Follow the following rules:
- When a variable is of type IN, the extends keyword is defined using an upper bound wildcard, i.e.
, which is PE.- When a variable is of type out, use the super keyword and use the lower bound wildcard definition, i.e.
, which is CS.- If a variable of type IN can be accessed using a method defined in the Object class, use an unbounded wildcard.
- If you want to support access to variables as both in and out variables, do not use wildcards.
Method return parameters are not suitable for wildmatch conformance because it forces the developer to perform type conversions.
The copy method signature for Java.util. Collections shows us the **”in” and “out” principle**.
public static <T> void copy(List<? super T> dest, List<? extends T> src) {... }Copy the code
The dest parameter provides data only for the copy method and is an in variable, so the upper bound wildcard definition is used, i.e. <? Extends T >; The SRC parameter is only used to hold the result of the copy method and is an out variable, so it is defined with the lower bound wildcard, <? Super T >. The “in” and “out” principle** is also expressed in the sort method signature of java.util.Collections.
public static <T> void sort(List<T> list, Comparator<? super T> c) {... }Copy the code
The parameter c is used to hold the result of the comparison and is a variable of type out, so it is defined with the lower bound wildcard, <? Super T >.
Reflection gets generic information
4.1 Generic information saved on Class, Method, and Fileld
When we looked at the bytecode information in Javap -V, we saw that the generic information for classes, methods, and fields is actually stored in the additional attribute Signature of the bytecode. Open the JDK source code for the Class, Method, Constructor, and Fileld classes and you’ll find that they all contain the corresponding generic information.
public final class Class<T> implements Serializable.GenericDeclaration.Type.AnnotatedElement {
// Generic signature handling
private native String getGenericSignature0(a);
// Generic info repository; lazily initialized
private volatile transientClassRepository genericInfo; . }public final class Field extends AccessibleObject implements Member {
// Generics and annotations support
private transient String signature;
// generic info repository; lazily initialized
private transientFieldRepository genericInfo; . }public final class Method extends Executable {
// Generics and annotations support
private transient String signature;
// generic info repository; lazily initialized
private transientMethodRepository genericInfo; . }public final class Constructor<T> extends Executable {
// Generics and annotations support
private transient String signature;
// generic info repository; lazily initialized
private transientConstructorRepository genericInfo; . }Copy the code
You can see that the Class, Method, Constructor, and Fileld classes all have a corresponding Repository inside them to hold the signature information needed to retrieve the generic type. If you trace the calls to these Repository methods, you will eventually see public methods provided by the JDK to retrieve generic information from classes, methods, constructors, and fields.
The methods in the Class Class that can be used to retrieve generic information are as follows,
// Map
can be used to obtain K and V, if there is no generic information such as String, return an empty array.
,v>
public TypeVariable<Class<T>>[] getTypeParameters() {
ClassRepository info = getGenericInfo();
if(info ! =null)
return (TypeVariable<Class<T>>[])info.getTypeParameters();
else
// If it is a String class with no generic information, return an empty array.
return (TypeVariable<Class<T>>[])newTypeVariable<? > [0];
}
// It can be used to get generic information about the parent, or return the corresponding class type if the parent is not generic.
public Type getGenericSuperclass(a) {
ClassRepository info = getGenericInfo();
if (info == null) {returngetSuperclass(); }if (isInterface()) { return null; }return info.getSuperclass();
}
// It can be used to get generic information about the interface, or if the interface is not generic, return the corresponding class type.
public Type[] getGenericInterfaces() {
ClassRepository info = getGenericInfo();
return (info == null)? getInterfaces() : info.getSuperInterfaces(); }Copy the code
The related methods in the Field class that can be used to get generic information are,
Map
, T, etc., and return the corresponding class type for non-generic member variables such as String.
,v>
public Type getGenericType(a) {
if(getGenericSignature() ! =null)
return getGenericInfo().getGenericType();
else
// A non-generic member variable that returns the corresponding class type
return getType();
}
Copy the code
The methods in the Method class that can be used to retrieve generic information are
// Can be used to get the type parameters of a generic method, such as K and V in
, or return an empty array if not a generic method.
,>
public TypeVariable<Method>[] getTypeParameters() {
if(getGenericSignature() ! =null)
return (TypeVariable<Method>[])getGenericInfo().getTypeParameters();
else
// A non-generic method that returns an empty array
return (TypeVariable<Method>[])new TypeVariable[0];
}
// Can be used to get the return type parameter of a generic method, or the corresponding class type if it is not a generic method.
public Type getGenericReturnType(a) {
if(getGenericSignature() ! =null) {
return getGenericInfo().getReturnType();
} else {
// Non-generic method that returns the corresponding class type
returngetReturnType(); }}// Can be used to get generic information about method parameters, if the method parameters are not generic, return the corresponding class type
public Type[] getGenericParameterTypes() {
return super.getGenericParameterTypes();
}
Copy the code
Methods related to getting generic information from the Constructor class are,
public TypeVariable<Constructor<T>>[] getTypeParameters() {
if(getSignature() ! =null) {
return (TypeVariable<Constructor<T>>[])getGenericInfo().getTypeParameters();
} else
return (TypeVariable<Constructor<T>>[])new TypeVariable[0];
}
// Can be used to get generic information about constructor parameters, or return the corresponding class type if the constructor parameters are not generic
public Type[] getGenericParameterTypes() {
return super.getGenericParameterTypes();
}
Copy the code
The Class, Method, Constructor, and Fileld classes that can be used to retrieve generics have been concisely commented above. If the corresponding return does not contain generic information, the result is either an empty array or the Class Type of the corresponding Class, which is a subclass of Type.
4.2 Type system introduced in JDK1.5
When retrieving generic information from methods in Class, Method, and Fileld classes, you will often see the return Type as Type or TypeVariable. You’ve probably seen ParameterizedType, WildcardType, and GenericArrayType before. These classes in the JDK were introduced to support generics. The corresponding UML Class diagram below shows that Class, ParameterizedType, WildcardType, and GenericArrayType all implement or inherit types.
In Java, Type is a more abstract concept than Class, as described in the JDK source code.
Type is the common superinterface for all types in the Java programming language. These include raw types, parameterized types, array types, type variables and primitive types.
TypeVariable indicates a TypeVariable, ParameterizedType indicates a ParameterizedType, WildcardType indicates a WildcardType, and GenericArrayType indicates a generic array. Let’s give you an example. Let’s make it a little bit more intuitive.
// Class K and V are typevariables
public class Node<K.V extends Number> {
// The K type corresponding to key is TypeVariable
private K key;
// Data corresponds to V of type TypeVariable
private V data;
// subData corresponds to V[] of type GenericArrayType
private V[] subData;
// The originalList variable has a List
type of ParameterizedType
private List<V> originalList;
// the generic method copy. The type of T in
is TypeVariable
// The List
of type ParameterizedType for data in copy is ParameterizedType
//copy the c parameter Comparator
is ParameterizedType
public static <T> void copy(List<T> data, Comparator<? super T> c) {...}
}
Copy the code
Let’s look at the methods for TypeVariable, ParameterizedType, WildcardType, and GenericArrayType.
public interface TypeVariable<D extends GenericDeclaration> extends Type.AnnotatedElement {// Get the upper limit of the generic variable, as in List
>
// You will get arrays of Serializable and Comparable
Type[] getBounds();// Get the Class, Constructor, or Method from which this type variable is declared
D getGenericDeclaration(a);// Get the name of the variable declaring the type, such as Test, to get A
String getName(a); }public interface ParameterizedType extends Type {
// Get the actual type in <>, such as List
, to get String
// Map
Type[] getActualTypeArguments();
// Get the erased type, such as List
, to get List
Type getRawType(a);
// If the type belongs to a type, get the type without returning null; For example, map. Entry
, the Map is obtained
,v>
Type getOwnerType(a);
}
public interface GenericArrayType extends Type {
// Get the array element type, such as List
[], will get List
, such as T[], will get T
Type getGenericComponentType(a);
}
public interface WildcardType extends Type {
// Get the upper bound of a generic variable, such as List
gives Number
Type[] getUpperBounds();
// Get the lower bound of a generic variable such as List
, you get String
Type[] getLowerBounds();
}
Copy the code
4.3 Practical skills of using Type system
TypeVariable, ParameterizedType, WildcardType, and GenericArrayType in the Type system have been described. So what are the techniques for using this Type system? Let’s take a look at some of the techniques that I’ve personally summarized.
A superclass method returns an object of subtype
When we introduced the bridge method earlier, we used an Animal example. So here’s another implementation of Animal, Cat.
interface Animal {
Animal getAnimal(a);
}
class Dog implements Animal {
@Override
public Dog getAnimal(a) {
return newDog(); }}class Cat implements Animal {
@Override
public Cat getAnimal(a) {
return newCat(); }}Copy the code
We already know that because the compiler generates the bridge method, it is legal for the above subclasses Dog and Cat to shrink the return value type to Dog and Cat, respectively, when overriding getAnimal. But can we define Animal and shrink the return value type to some pending subclass of Animal? This way, when a subclass overrides a method, it doesn’t have to change its hand to the subclass itself every time. In fact, generics can be easily implemented by refactoring the interface definition above, and then transforming the implementation class. I’ve reduced this technique of using generics to superclass methods returning subtype objects.
interface Animal<T extends Animal > {
T getAnimal(a);
}
class Dog implements Animal<Dog> {
@Override
public Dog getAnimal(a) {
return newDog(); }}class Cat implements Animal<Cat> {
@Override
public Cat getAnimal(a) {
return newCat(); }}Copy the code
See how this technique is used on the core classes ServerBootstrap, Bootstrap, and AbstractBootstrap of the Netty framework. AbstractBootstrap ServerBootstrap is an abstraction of the Netty server boot class. Bootstrap is an abstraction of the Netty client boot class. AbstractBootstrap defines the common methods and properties of the Bootstrap class. Netty in order to make the user in the use of more convenient, using the Bulid mode, we can easily use the chain call to set various parameters and finally get a server or client boot class instance.
// Set various parameters through chain calls to build ServerBootstrap easily
ServerBootstrap serverBootstrap = new ServerBootstrap()
.group(new NioEventLoopGroup(), new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(newServerHandler()); }});// Set various parameters through chain calls to easily build Bootstrap
Bootstrap bootstrap = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline().addLast(newClientHandler()); }});Copy the code
In the above code, methods such as group, channel, option, etc. are called when building server and client bootstrap instances. These methods are all common methods. It is common practice to abstract them into AbstractBootstrap. However, in order to be compatible with the ServerBootstrap and Bootstrap types, the return type of these methods defined in AbstractBootstrap should be AbstractBootstrap. Netty uses its AbstractBootstrap method to return a subclass object. Its AbstractBootstrap method returns the corresponding subclass directly. If the subclass is ServerBootstrap, it returns ServerBootstrap. Return Bootstrap if the subclass is Bootstrap.
public abstract class AbstractBootstrap<B extends AbstractBootstrap<B.C>, C extends Channel> implements Cloneable {
public B group(EventLoopGroup group) {
ObjectUtil.checkNotNull(group, "group");
if (this.group ! =null) {
throw new IllegalStateException("group set already");
} else {
this.group = group;
return this.self(); }}// Return yourself
private B self(a) {
return (B) this; }... }Copy the code
AbstractBootstrap defines a generic type parameter with B extends AbstractBootstrap<B, C> and C extends Channel, and then defines a self method that returns itself.
AbstractBootstrap self in AbstractBootstrap will return Bootstrap
public class Bootstrap extends AbstractBootstrap<Bootstrap.Channel> {... }AbstractBootstrap self in AbstractBootstrap will return ServerBootstrap
public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap.ServerChannel> {... }Copy the code
Gets the actual type passed to the generic parent
Suppose we now define a generic AbastactRepository abstract class with the corresponding UserRepository and ClientRepository implementation classes, as follows.
public abstract class AbastactRepository<T> {
private Class<T> entityClass;
public abstract T findByKey(String key);
}
public class UserRepository extends AbastactRepository<UserEntity> {
public UserEntity findByKey(String key);
}
public class ClientRepository extends AbastactRepository<ClientEntity> {
public ClientEntity findByKey(String key);
}
Copy the code
How to get UserRepository and ClientRepository, and the classes corresponding to UserEntity and ClientEntity passed to the generic parent And is how we get the value of the entityClass member of AbastactRepository. Remember the Class Class method getGenericSuperclass no, The ParameterizedType can be obtained by calling getGenericSuperclass with the clientrepository. class method. We then use the getActualTypeArguments method of ParameterizedType to fetch ClientEntity to get the type information we want.
ParameterizedType type = (ParameterizedType) ClientRepository.class.getGenericSuperclass();
// Clazz instance is an instance of ClientEntity's corresponding class
Class clazz = (Class) type.getActualTypeArguments()[0];
Copy the code
Can you make it more generic? You can call etGenericSuperclass as above to get ParameterizedType as long as you have the class instance of the corresponding subclass in AbastactRepository. How do I get an instance of a subclass from AbastactRepository? The object.getClass () method gets the class instance of the runtime AbastactRepository. If the runtime AbastactRepository is a UserRepository Object, If AbastactRepository is a ClientRepository object, the class instance of UserRepository is returned.
public abstract class AbastactRepository<T> {
private Class<T> entityClass;
public abstract T findByKey(String key);
public AbastactRepository(a) {
this.entityClass = (Class<T>)((ParameterizedType) getClass()
.getGenericSuperclass()).getActualTypeArguments()[0]; }}Copy the code
I call the above technique getting the actual type passed to the generic parent.
Narrow the type of overriding method input arguments
Now we define a Context interface called Context and a tag build interface called LabelBuilder. The Context interface defines a getFutureTasks that returns some processing results of the Context, The LabelBuilder interface defines an interface to build a tag. The parameter type is Context. Now we want different implementations of LabelBuilder to narrow down the parameter types received to the corresponding Context.
public interface Context {
List<Future> getFutureTasks(a);
}
public class ScenarioAContext implements Context {
public List<Future> getFutureTasks(a) { return null; }}public class ScenarioBContext implements Context {
public List<Future> getFutureTasks(a) { return null;}
}
public interface LabelBuilder {
void build(Context context);
}
Copy the code
When, for example, the ScenarioAContext above, its corresponding ScenarioALableBuilder implementation accepts only the ScenarioAContext type parameter, rather than the too abstract Context, we have the following class definition.
public class ScenarioABuilder implements LabelBuilder {
// Failed to compile
@Override
public void build(ScenarioAContext context) {...}
}
Copy the code
I’m sorry it didn’t compile because the definition of the above method is no longer compliant with the overridden specification. How about removing the @override annotation? Unfortunately the compilation will tell you that there is still one method that is not implemented. So what’s the way to handle this scenario. In fact, it is very simple, I don’t know if you remember earlier said compiler type erasure includes what in the compiler generation bridge method part. If you remember, that’s normal for this scenario. So we modify the definition of a LabelBuilder interface with the definition of ScenarioABuilder.
public interface LabelBuilder<T extends Context> {
void build(T context);
}
public class ScenarioABuilder implements LabelBuilder<ScenarioAContext> {
// Compilation succeeded
@Override
public void build(ScenarioAContext context) {...}
}
Copy the code
All right, everything’s perfect. I’ve reduced the above technique to narrow the overriding method input type. Finally, I will leave you a question. What are the differences between LabelBuilder interface definitions?
public interface LabelBuilder<T extends Context> {
void build(T context);
}
public interface LabelBuilder<T> {
void build(T context);
}
Copy the code
conclusion
This article provides a detailed introduction to Java generics, including why generics are introduced, what the compiler does for generics, why generics themselves do not support covariation, how to make generics support covariation, and how to obtain generics information through reflection. Check out Oracle’s official Restrictions on Generics. In the next article I’ll look at Spring’s abstraction of generics from ResolvableType, which makes manipulating generic variables much simpler.
At the end
Original is not easy, praise, look, forwarding is a great encouragement to me, concern about the public number insight source code is my biggest support. At the same time believe that I will share more dry goods, I grow with you, I progress with you.
reference
**The Java™ Tutorials – Generics **
Java Generics – Bridge method?
Covariance, Invariance and Contravariance explained in plain English?
Why I distrust wildcards and why we need them anyway
** Bytecode enhancement technology exploration **
In-depth understanding of THE JVM (4) — virtual machine execution subsystem