[Java Basics - Detailed Explanation of Generics Mechanism]

Why are generics introduced?

The essence of generics is to parameterize types (control the type of specific restrictions on formal parameters through different types specified by generics without creating a new type). That is to say, during the use of generics, the data type of the operation is specified as a parameter. This parameter type can be used in classes, interfaces, and methods, and are called generic classes, generic interfaces, and generic methods respectively.

The significance of introducing generics is:

  • Execute the same code for multiple data types

Let's illustrate it through an example. Let's first look at the following code:

private static int add(int a, int b) {
    System.out.println(a + "+" + b + "=" + (a + b));
    return a + b;
}

private static float add(float a, float b) {
    System.out.println(a + "+" + b + "=" + (a + b));
    return a + b;
}

private static double add(double a, double b) {
    System.out.println(a + "+" + b + "=" + (a + b));
    return a + b;
}

If there are no generics, to implement different types of addition, each type needs to overload an add method; through generics, we can reuse it as one method:

private static <T extends Number> double add(T a, T b) {
    System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue()));
    return a.doubleValue() + b.doubleValue();
}
  • The type in a generic is specified when used, and no cast is required (Type safety, the compiler will< a i=3>Check type)

Take a look at this example:

List list = new ArrayList();
list.add("xxString");
list.add(100d);
list.add(new Person());

When we use the above list, the elements in the list are all of Object type (the types cannot be constrained), so when taking out the collection elements, we need to artificially force the type to convert to a specific target type, and it is easy to occur. java.lang.ClassCastExceptionException.

Introduce generics, which will provide type constraints and provide pre-compilation checks:

List<String> list = new ArrayList<String>();

// list中只能放String, 不能放其它类型的元素

Basic usage of generics

Generic class

  • Starting with a simple generic class:
class Point<T>{         // 此处可以随便写标识符号,T是type的简称  
    private T var ;     // var的类型由T指定,即:由外部指定  
    public T getVar(){  // 返回值的类型由外部决定  
        return var ;  
    }  
    public void setVar(T var){  // 设置的类型也由外部决定  
        this.var = var ;  
    }  
}  
public class GenericsDemo06{  
    public static void main(String args[]){  
        Point<String> p = new Point<String>() ;     // 里面的var类型为String类型  
        p.setVar("it") ;                            // 设置字符串  
        System.out.println(p.getVar().length()) ;   // 取得字符串的长度  
    }  
}
  • polygeneric
class Notepad<K,V>{       // 此处指定了两个泛型类型  
    private K key ;     // 此变量的类型由外部决定  
    private V value ;   // 此变量的类型由外部决定  
    public K getKey(){  
        return this.key ;  
    }  
    public V getValue(){  
        return this.value ;  
    }  
    public void setKey(K key){  
        this.key = key ;  
    }  
    public void setValue(V value){  
        this.value = value ;  
    }  
} 
public class GenericsDemo09{  
    public static void main(String args[]){  
        Notepad<String,Integer> t = null ;        // 定义两个泛型类型的对象  
        t = new Notepad<String,Integer>() ;       // 里面的key为String,value为Integer  
        t.setKey("汤姆") ;        // 设置第一个内容  
        t.setValue(20) ;            // 设置第二个内容  
        System.out.print("姓名;" + t.getKey()) ;      // 取得信息  
        System.out.print(",年龄;" + t.getValue()) ;       // 取得信息  
  
    }  
}

 Generic interface

  • Simple generic interface
interface Info<T>{        // 在接口上定义泛型  
    public T getVar() ; // 定义抽象方法,抽象方法的返回值就是泛型类型  
}  
class InfoImpl<T> implements Info<T>{   // 定义泛型接口的子类  
    private T var ;             // 定义属性  
    public InfoImpl(T var){     // 通过构造方法设置属性内容  
        this.setVar(var) ;    
    }  
    public void setVar(T var){  
        this.var = var ;  
    }  
    public T getVar(){  
        return this.var ;  
    }  
} 
public class GenericsDemo24{  
    public static void main(String arsg[]){  
        Info<String> i = null;        // 声明接口对象  
        i = new InfoImpl<String>("汤姆") ;  // 通过子类实例化对象  
        System.out.println("内容:" + i.getVar()) ;  
    }  
}  

 Generic methods

Generic methods specify the specific type of the generic when calling the method.

When defining a generic method, you must add a<T> in front of the return value to declare that this is a generic method and holds a generic typeT, then you can use generic T as the return value of the method.

Class<T>The function of is to specify the specific type of the generic, and the variable c of type Class<T> can be used to create objects of the generic class.

Why use variable c to create objects? Since it is a generic method, it means that we do not know what the specific type is, nor what the construction method is, so there is no way to create a new object, but we can use the newInstance method of the variable c to create the object, that is, using reflection to create it. object.

The parameter required by the generic method is of type Class<T>, and the return value of the Class.forName() method is also Class<T>, so it can UseClass.forName() as parameter. Among them, the type of the parameters in the forName() method is the type of the returned Class<T>. In this example, the forName() method is passed in the full path of the User class, so an object of type Class<User> is returned. Therefore, when calling the generic method, The type of variable c is Class<User>, so the generic T in the generic method is specified as User, so the type of variable obj is User.

Of course, a generic method can not only have one parameterClass<T>, other parameters can be added as needed.

Why use generic methods? Because a generic class needs to specify the type when it is instantiated, if you want to change to a different type, you have to create it again, which may not be flexible enough; while a generic method can specify the type when it is called, which is more flexible.

 Upper and lower bounds for generics

  • First look at the following code, it will obviously report an error (please refer to the following article for the specific error reasons).
class A{}
class B extends A {}

// 如下两个方法不会报错
public static void funA(A a) {
    // ...          
}
public static void funB(B b) {
    funA(b);
    // ...             
}

// 如下funD方法会报错
public static void funC(List<A> listA) {
    // ...          
}
public static void funD(List<B> listB) {
    funC(listB); // Unresolved compilation problem: The method doPrint(List<A>) in the type test is not applicable for the arguments (List<B>)
    // ...             
}

So how to solve it?

In order to solve the implicit conversion problem in generics, Java generics add an upper and lower boundary mechanism for type parameters. <? extends A> indicates that the type parameter can be A (upper boundary) or a subclass type of A. Type A is erased during compilation, that is, type A is used instead of type parameters. This method can solve the problem encountered at the beginning. The compiler knows the range of the type parameters. If the incoming instance type B is within this range, conversion is allowed. At this time, only one type conversion is required, and the object will be converted at runtime. Treat it as an instance of A.

public static void funC(List<? extends A> listA) {
    // ...          
}
public static void funD(List<B> listB) {
    funC(listB); // OK
    // ...             
}
  • The introduction of generic upper and lower bounds

When using generics, we can limit the upper and lower boundaries of the incoming generic type actual parameters. For example, type actual parameters are only allowed to pass in a certain type of parent class or a certain type of subclass.

upper limit

class Info<T extends Number>{    // 此处泛型只能是数字类型
    private T var ;        // 定义泛型变量
    public void setVar(T var){
        this.var = var ;
    }
    public T getVar(){
        return this.var ;
    }
    public String toString(){    // 直接打印
        return this.var.toString() ;
    }
}
public class demo1{
    public static void main(String args[]){
        Info<Integer> i1 = new Info<Integer>() ;        // 声明Integer的泛型对象
    }
}

lower limit

class Info<T>{
    private T var ;        // 定义泛型变量
    public void setVar(T var){
        this.var = var ;
    }
    public T getVar(){
        return this.var ;
    }
    public String toString(){    // 直接打印
        return this.var.toString() ;
    }
}
public class GenericsDemo21{
    public static void main(String args[]){
        Info<String> i1 = new Info<String>() ;        // 声明String的泛型对象
        Info<Object> i2 = new Info<Object>() ;        // 声明Object的泛型对象
        i1.setVar("hello") ;
        i2.setVar(new Object()) ;
        fun(i1) ;
        fun(i2) ;
    }
    public static void fun(Info<? super String> temp){    // 只能接收String或Object类型的泛型,String类的父类只有Object类
        System.out.print(temp + ", ") ;
    }
}

summary

<?> 无限制通配符
<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
<? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类

// 使用原则《Effictive Java》
// 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限
1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
2. 如果它表示一个 T 的消费者,就使用 < ? super T>;
3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。
  • Let’s look at another practical exampleto deepen our impression
private  <E extends Comparable<? super E>> E max(List<? extends E> e1) {
    if (e1 == null){
        return null;
    }
    //迭代器返回的元素属于 E 的某个子类型
    Iterator<? extends E> iterator = e1.iterator();
    E result = iterator.next();
    while (iterator.hasNext()){
        E next = iterator.next();
        if (next.compareTo(result) > 0){
            result = next;
        }
    }
    return result;
}

The range of the type parameter E in the above code is<E extends Comparable<? super E>>, we can check it step by step:

  • needs to be compared, so E needs to be a comparable class, so extends Comparable<…> is needed (note that this is not to be confused with the inherited extends, no Same)

  • Comparable< ? super E>To compare E, which is the consumer of E, you need to use super

  • The parameter List< ? extends E> indicates that the data to be operated on is a list of subclasses of E, and the upper limit is specified so that the container is large enough

  • Multiple restrictions

Use ampersand

public class Client {
    //工资低于2500元的上斑族并且站立的乘客车票打8折
    public static <T extends Staff & Passenger> void discount(T t){
        if(t.getSalary()<2500 && t.isStanding()){
            System.out.println("恭喜你!您的车票打八折!");
        }
    }
    public static void main(String[] args) {
        discount(new Me());
    }
}

 Generic array

For details, please refer to the understanding of generic arrays below.

First, our declaration related to the generic array:

List<String>[] list11 = new ArrayList<String>[10]; //编译错误,非法创建 
List<String>[] list12 = new ArrayList<?>[10]; //编译错误,需要强转类型 
List<String>[] list13 = (List<String>[]) new ArrayList<?>[10]; //OK,但是会有警告 
List<?>[] list14 = new ArrayList<String>[10]; //编译错误,非法创建 
List<?>[] list15 = new ArrayList<?>[10]; //OK 
List<String>[] list6 = new ArrayList[10]; //OK,但是会有警告

So how do we usually use it?

  • Cunning usage scenarios
public class GenericsDemo30{  
    public static void main(String args[]){  
        Integer i[] = fun1(1,2,3,4,5,6) ;   // 返回泛型数组  
        fun2(i) ;  
    }  
    public static <T> T[] fun1(T...arg){  // 接收可变参数  
        return arg ;            // 返回泛型数组  
    }  
    public static <T> void fun2(T param[]){   // 输出  
        System.out.print("接收泛型数组:") ;  
        for(T t:param){  
            System.out.print(t + "、") ;  
        }  
    }  
}
  • fair use
public ArrayWithTypeToken(Class<T> type, int size) {
    array = (T[]) Array.newInstance(type, size);
}

Please see the explanation below for details.

 Deep understanding of generics

hint

We further understand generics through type erasure and related issues behind generics. @pdai

 How to understand that generics in Java are pseudo-generics? Type erasure in generics

The feature of Java generics was only added in JDK 1.5. Therefore, in order to be compatible with previous versions, the implementation of Java generics adopts "pseudo-generic " strategy, that is, Java supports generics syntactically, but the so-called "type erasure" will be performed during the compilation phase ( Type Erasure), which replaces all generic representations (the content in angle brackets) with concrete types (their corresponding raw types), just like there are no generics at all. Understanding type erasure is very helpful for making good use of generics, especially some seemingly "difficult" problems. Once you understand type erasure, they will be easily solved.

The type erasure principle of genericsis:

  • Eliminate type parameter declaration, that is, delete<> and its surrounding parts.
  • Infer and replace all type parameters with the original type based on the upper and lower bounds of the type parameters: if the type parameter is an unlimited wildcard or has no upper and lower bounds, it will be replaced with Object. If there is an upper and lower bound, the type parameter will be taken according to the subclass replacement principle. The leftmost qualified type (that is, the parent class).
  • To ensure type safety, cast code is inserted when necessary.
  • Automatically generate "bridge methods" to ensure that the code after erasing the type still has generic "polymorphism".

So how to erase?

  • Erase type parameters in class definition - Unrestricted type erasure

When does not have any restrictions on the type parameters in the class definition, it is directly replaced by Object during type erasure, that is, in the form of <T> and <?> The type parameters are replaced with Object.

  • Erasing type parameters in class definition - restricted type erasure

When there are restrictions (upper and lower bounds) on the type parameters in the class definition, they are replaced with the upper or lower bounds of the type parameters during type erasure, such as in the form <T extends Number> and The type parameters of a><? extends Number> are replaced with Number, and <? super Number> are replaced with Object.

  • Erase type parameters in method definition

The principles of type parameters in the erasure method definition are the same as those in the erasure class definition. Here we only take the restricted type parameters in the erasure method definition as an example.

 How to prove type erasure?

We demonstrate type erasure of Java types through two examples

  • Primitive type equality
public class Test {

    public static void main(String[] args) {

        ArrayList<String> list1 = new ArrayList<String>();
        list1.add("abc");

        ArrayList<Integer> list2 = new ArrayList<Integer>();
        list2.add(123);

        System.out.println(list1.getClass() == list2.getClass()); // true
    }
}

In this example, we define two ArrayList arrays, but one is of ArrayList<String> generic type and can only store strings; the other is ArrayList<Integer> Generic types can only store integers. Finally, we obtain their class information through the getClass() method of the list1 object and the list2 object, and finally find that the result is true. It shows that the generic types String and Integer have been erased, leaving only the original type.

  • Add other types of elements through reflection
public class Test {

    public static void main(String[] args) throws Exception {

        ArrayList<Integer> list = new ArrayList<Integer>();

        list.add(1);  //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer

        list.getClass().getMethod("add", Object.class).invoke(list, "asd");

        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }

}

defines a ArrayList generic type in the program and instantiates it as an Integer object. If the add() method is called directly , then only integer data can be stored, but when we use reflection to call the add() method, we can store strings, which shows that the Integer generic instance is being compiled It was later erased, leaving only the original type.

 How to understand the original type retained after type erasure?

In the above, primitive types are mentioned twice. What are primitive types?

Original type means that the generic information is erased, and finally the real type of the type variable in the bytecode. Whenever a generic is defined, the corresponding primitive type will Provided automatically, the type variable is erased and replaced with its qualified type (unqualified variables are Object).

  • Primitive type Object
class Pair<T> {  
    private T value;  
    public T getValue() {  
        return value;  
    }  
    public void setValue(T  value) {  
        this.value = value;  
    }  
} 

The original type of Pair is:

class Pair {  
    private Object value;  
    public Object getValue() {  
        return value;  
    }  
    public void setValue(Object  value) {  
        this.value = value;  
    }  
}

Because in Pair<T>, T is an undefined type variable, so replace it with Object. The result is an ordinary class, like a generic The type was implemented before it was added to the Java language. Different types of Pair can be included in the program, such as Pair<String> or Pair<Integer>, but after erasing the type, they become the original Pair type, and the original types are Object.

From the above chapter, we can also understand that after the ArrayList is erased, the original type also becomes Object, so we can store strings through reflection.

If the type variable is bounded, the original type is replaced by the first bounding type variable class.

For example: If Pair declares this

public class Pair<T extends Comparable> {}

Then the primitive type is Comparable.

Distinguish between primitive types and generic variable types.

When calling a generic method, you can specify a generic type or not:

  • Without specifying a generic, the type of a generic variable is the lowest level of the same parent class of several types in the method, up to Object
  • In the case of specifying a generic type, several types of the method must be the type of the instance of the generic type or its subclass
public class Test {  
    public static void main(String[] args) {  

        /**不指定泛型的时候*/  
        int i = Test.add(1, 2); //这两个参数都是Integer,所以T为Integer类型  
        Number f = Test.add(1, 1.2); //这两个参数一个是Integer,一个是Float,所以取同一父类的最小级,为Number  
        Object o = Test.add(1, "asd"); //这两个参数一个是Integer,一个是String,所以取同一父类的最小级,为Object  

        /**指定泛型的时候*/  
        int a = Test.<Integer>add(1, 2); //指定了Integer,所以只能为Integer类型或者其子类  
        int b = Test.<Integer>add(1, 2.2); //编译错误,指定了Integer,不能为Float  
        Number c = Test.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Float  
    }  

    //这是一个简单的泛型方法  
    public static <T> T add(T x,T y){  
        return y;  
    }  
}

In fact, in a generic class, when the generic type is not specified, it is almost the same, except that the generic type at this time is Object. For example, in ArrayList, if the generic type is not specified, then this ArrayList can store any object.

  • Object generic
public static void main(String[] args) {  
    ArrayList list = new ArrayList();  
    list.add(1);  
    list.add("121");  
    list.add(new Date());  
}  

 How to understand compile-time checking of generics?

Since type variables are said to be erased during compilation, why do we get an error when we add integers to objects created by ArrayList? Doesn't it mean that the generic variable String will be changed to Object type during compilation? Why can't I save other types? Since type erasure is done, how to ensure that we can only use types qualified by generic variables?

The Java compiler first checks the generic type in the code, then performs type erasure, and then compiles.

For example:

public static  void main(String[] args) {  

    ArrayList<String> list = new ArrayList<String>();  
    list.add("123");  
    list.add(123);//编译错误  
}

In the above program, use the add method to add an integer. In the IDE, an error will be reported directly, indicating that this is a check before compilation, because if it is checked after compilation, after type erasure, the original type is Object, which is Any reference type should be allowed to be added. But in fact this is not the case. This just shows that the use of generic variables will be checked before compilation.

So,who is this type check for? Let's first look at the compatibility between parameterized types and primitive types.

Take ArrayList as an example, the previous writing method:

ArrayList list = new ArrayList();  

Current writing:

ArrayList<String> list = new ArrayList<String>();

If it is compatible with the previous code, the following situations will inevitably occur between various reference and value transfers:

ArrayList<String> list1 = new ArrayList(); //第一种 情况
ArrayList list2 = new ArrayList<String>(); //第二种 情况

There is no error in this, but there will be a compile-time warning.

However, in the first case, the same effect as using generic parameters can be achieved, but in the second case, it has no effect.

Because type checking is completed at compile time, new ArrayList() just opens up a storage space in the memory, which can store any type of object. What really involves type checking is its reference, because we use it to refer to list1 to call it. method, such as calling the add method, so the list1 reference can complete the generic type check. The reference to list2 does not use generics, so it doesn't work.

for example:

public class Test {  

    public static void main(String[] args) {  

        ArrayList<String> list1 = new ArrayList();  
        list1.add("1"); //编译通过  
        list1.add(1); //编译错误  
        String str1 = list1.get(0); //返回类型就是String  

        ArrayList list2 = new ArrayList<String>();  
        list2.add("1"); //编译通过  
        list2.add(1); //编译通过  
        Object object = list2.get(0); //返回类型就是Object  

        new ArrayList<String>().add("11"); //编译通过  
        new ArrayList<String>().add(22); //编译错误  

        String str2 = new ArrayList<String>().get(0); //返回类型就是String  
    }  
} 

Through the above example, we can understand that Type checking is for references. Who is a reference and using this reference to call a generic method will call this reference. Method performs type checking regardless of the object it actually refers to.

Why does the parameter type in a generic type not consider the inheritance relationship??

In Java, passing by reference like the following is not allowed:

ArrayList<String> list1 = new ArrayList<Object>(); //编译错误  
ArrayList<Object> list2 = new ArrayList<String>(); //编译错误
  • Let’s look at the first situation first and expand the first situation into the following form:
ArrayList<Object> list1 = new ArrayList<Object>();  
list1.add(new Object());  
list1.add(new Object());  
ArrayList<String> list2 = list1; //编译错误

In fact, in the 4th line of code, there will be a compilation error. So, let's assume it compiles correctly. Then when we use the list2 reference to get the value using the get() method, all objects returned are String type objects (as mentioned above, type detection is determined based on the reference), but we have actually stored Object in it. Type object, so there will beClassCastException. So in order to avoid this extremely easy error, Java does not allow such reference transfer. (This is also the reason why generics appeared, to solve the problem of type conversion. We cannot violate its original intention).

  • Let’s look at the second situation again and expand the second situation into the following form:
ArrayList<String> list1 = new ArrayList<String>();  
list1.add(new String());  
list1.add(new String());

ArrayList<Object> list2 = list1; //编译错误

Yes, this situation is much better than the first one. At the very least, ClassCastException will not occur when we use list2 to get the value, because it is converted from String to Object. However, what is the point of doing this? The reason why generics appear is to solve the problem of type conversion.

We used generics, but in the end, we still had to force it ourselves, which violated the original intention of generic design. So java does not allow this. Besides, if you use list2 to add() a new object into it, how do I know whether what I get out is of type String or Object?

Therefore, special attention should be paid to the issue of reference passing in generics.

 How to understand generic polymorphism? Generic bridging method

Type erasure will cause polymorphic conflicts, and the JVM solution is the bridging method.

Now there is a generic class like this:

class Pair<T> {  

    private T value;  

    public T getValue() {  
        return value;  
    }  

    public void setValue(T value) {  
        this.value = value;  
    }  
}

Then we want a subclass to inherit it.

class DateInter extends Pair<Date> {  

    @Override  
    public void setValue(Date value) {  
        super.setValue(value);  
    }  

    @Override  
    public Date getValue() {  
        return super.getValue();  
    }  
}

In this subclass, we set the generic type of the parent class toPair<Date>. In the subclass, we override the two methods of the parent class. Our original intention is Like this: Limit the generic type of the parent class to Date, then the parameters of the two methods in the parent class are both of Date type.

public Date getValue() {  
    return value;  
}  

public void setValue(Date value) {  
    this.value = value;  
}

So, there is no problem at all when we override these two methods in subclasses. In fact, as you can see from their @Override tags, there is no problem at all. Is this actually the case?

Analysis: In fact, after type erasure, all the generic types of the parent class become the original type Object, so the parent class will look like the following after compilation:

class Pair {  
    private Object value;  

    public Object getValue() {  
        return value;  
    }  

    public void setValue(Object  value) {  
        this.value = value;  
    }  
} 

Let’s look at the types of the two overridden methods of the subclass:

@Override  
public void setValue(Date value) {  
    super.setValue(value);  
}  
@Override  
public Date getValue() {  
    return super.getValue();  
}

Let’s first analyze the setValue method. The type of the parent class is Object, while the type of the subclass is Date, and the parameter types are different. If this is in a normal inheritance relationship, it will not be rewriting at all, but overloading. Let’s test it in a main method:

public static void main(String[] args) throws ClassNotFoundException {  
        DateInter dateInter = new DateInter();  
        dateInter.setValue(new Date());                  
        dateInter.setValue(new Object()); //编译错误  
}

If it is overloaded, then there are two setValue methods in the subclass, one is of parameter Object type, and the other is of Date type. However, we found that there is no such method for subclasses to inherit Object type parameters from the parent class. Therefore, it is rewritten, not overloaded.

Why is this happening?

The reason is this. The generic type we passed into the parent class is Date,Pair<Date>. Our original intention is to change the generic class into the following:

class Pair {  
    private Date value;  
    public Date getValue() {  
        return value;  
    }  
    public void setValue(Date value) {  
        this.value = value;  
    }  
}

Then rewrite the two methods whose parameter type is Date in the subclass to achieve polymorphism in inheritance.

However, due to various reasons, the virtual machine cannot change the generic type into Date. It can only erase the type and change it into the original type Object. In this way, our original intention is to rewrite and achieve polymorphism. But after type erasure, it can only become overloaded. In this way, type erasure conflicts with polymorphism. Does the JVM know what you mean? Know! ! ! But can it be realized directly? No! ! ! If it really can't be done, then how can we rewrite the method of the Date type parameter we want?

So the JVM adopted a special method to complete this function, which is the bridge method.

First, we use javap -c className to decompile the bytecode of the DateInter subclass. The results are as follows:

class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {  
  com.tao.test.DateInter();  
    Code:  
       0: aload_0  
       1: invokespecial #8                  // Method com/tao/test/Pair."<init>":()V  
       4: return  

  public void setValue(java.util.Date);  //我们重写的setValue方法  
    Code:  
       0: aload_0  
       1: aload_1  
       2: invokespecial #16                 // Method com/tao/test/Pair.setValue:(Ljava/lang/Object;)V  
       5: return  

  public java.util.Date getValue();    //我们重写的getValue方法  
    Code:  
       0: aload_0  
       1: invokespecial #23                 // Method com/tao/test/Pair.getValue:()Ljava/lang/Object;  
       4: checkcast     #26                 // class java/util/Date  
       7: areturn  

  public java.lang.Object getValue();     //编译时由编译器生成的桥方法  
    Code:  
       0: aload_0  
       1: invokevirtual #28                 // Method getValue:()Ljava/util/Date 去调用我们重写的getValue方法;  
       4: areturn  

  public void setValue(java.lang.Object);   //编译时由编译器生成的桥方法  
    Code:  
       0: aload_0  
       1: aload_1  
       2: checkcast     #26                 // class java/util/Date  
       5: invokevirtual #30                 // Method setValue:(Ljava/util/Date; 去调用我们重写的setValue方法)V  
       8: return  
}

Judging from the compilation results, we originally intended to rewrite the subclass of the setValue and getValue methods, but there are actually 4 methods. In fact, there is no need to be surprised. The last two methods are the bridge methods generated by the compiler itself. You can see that the parameter types of the bridge method are all Object. In other words, the two bridge methods that we cannot see in the subclass actually cover the two methods of the parent class. The @Oveerride on our own defined setvalue and getValue methods is just an illusion. The internal implementation of the bridge method is just to call the two methods we have rewritten.

Therefore, the virtual machine cleverly uses the bridge method to solve the conflict between type erasure and polymorphism.

However, one thing to mention is that the two bridge methods setValue and getValue have different meanings.

The setValue method is to resolve the conflict between type erasure and polymorphism.

But getValue has a universal meaning. How to put it, if this is an ordinary inheritance relationship:

Then the getValue method of the parent class is as follows:

public Object getValue() {  
    return super.getValue();  
}

The method overridden by subclasses is:

public Date getValue() {  
    return super.getValue();  
}

In fact, this is also a common rewriting in ordinary class inheritance. This is covariance.

Furthermore, there may be some doubts. The bridge methods Object getValue() and Date getValue() in the subclass exist at the same time, but if they are regular The method signatures of the two methods are the same, which means that the virtual machine cannot distinguish between the two methods at all. If we write Java code ourselves, such code cannot pass the compiler's inspection, but the virtual machine allows it, because the virtual machine determines a method through the parameter type and return type, so the compiler in order to implement generic Type polymorphism allows you to do this seemingly "illegal" thing, and then leaves it to the virtual machine to distinguish.

How to understand that basic types cannot be used as generic types?

For example, we don’t have ArrayList<int>, but only ArrayList<Integer>, why?

Because after type erasure, the original type of ArrayList becomes Object, but the Object type cannot store int values ​​and can only reference Integer values.

Also note that we can uselist.add(1) because of the automatic boxing and unboxing operations of Java basic types.

How to understand that a generic type cannot be instantiated?

Generic types cannot be instantiated, which is essentially due to type erasure:

We can see that the following code will report an error in the compiler:

T test = new T(); // ERROR

Because the generic parameterized type cannot be determined during Java compilation, the corresponding class bytecode file cannot be found, so naturally it will not work. In addition, because T is Erased as Object, if new T() is possible, it becomes new Object(), losing the original meaning. If we really need to instantiate a generic, how should we do it? This can be achieved through reflection:

static <T> T newTclass (Class < T > clazz) throws InstantiationException, IllegalAccessException {
    T obj = clazz.newInstance();
    return obj;
}

 Generic array: Can a specific generic type be used for initialization?

Let’s first look at an example provided by Oracle’s official website:

List<String>[] lsa = new List<String>[10]; // Not really allowed.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Unsound, but passes run time store check
String s = lsa[1].get(0); // Run-time error ClassCastException.

Due to the JVM generic erasure mechanism, the above code can assign oa[1] to an ArrayList without exception, but a type conversion is required when retrieving the data. So ClassCastException will appear. If a generic array can be declared, the above situation will not cause any warnings or errors during compilation. An error will only occur at runtime, but the generic array The emergence of type is to eliminate ClassCastException, so if Java supports generic array initialization operations, it will shoot itself in the foot.

This is true for the following code:

List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Correct.
Integer i = (Integer) lsa[1].get(0); // OK

Therefore, it is allowed to initialize a generic array using wildcards, because explicit type conversion is required for the final data taken out using wildcards, which is in line with the expected logic. The summary is that when Java's generic array is initialized, the array type cannot be a specific generic type, but can only be in the form of a wildcard, because the specific type will cause any type of object to be stored, and a type conversion exception will occur when it is taken out, which will conflict with the The design ideas of generics conflict, and the wildcard form needs to be forced to be converted by yourself, which is in line with expectations.

Going a step further, let's look at the following code:

List<String>[] list11 = new ArrayList<String>[10]; //编译错误,非法创建 
List<String>[] list12 = new ArrayList<?>[10]; //编译错误,需要强转类型 
List<String>[] list13 = (List<String>[]) new ArrayList<?>[10]; //OK,但是会有警告 
List<?>[] list14 = new ArrayList<String>[10]; //编译错误,非法创建 
List<?>[] list15 = new ArrayList<?>[10]; //OK 
List<String>[] list6 = new ArrayList[10]; //OK,但是会有警告

Because it is not possible to create an array of an exact generic type in Java, unless wildcards are used and explicit type conversion is required.

 Generic arrays: How to correctly initialize a generic array instance?

There is a warning whether we initialize a generic array instance in the form of new ArrayList[10] or in the form of a generic wildcard. That is to say, only the syntax is qualified, and there are potential risks at runtime. We need to bear it ourselves, so those methods of initializing generic arrays are not the most elegant way.

We should try to use list collection replacement when using generic arrays. In addition, we can also use the java.lang.reflect.Array.newInstance(Class<T> componentType, int length) method to create an array with specified types and dimensions, as follows:

public class ArrayWithTypeToken<T> {
    private T[] array;

    public ArrayWithTypeToken(Class<T> type, int size) {
        array = (T[]) Array.newInstance(type, size);
    }

    public void put(int index, T item) {
        array[index] = item;
    }

    public T get(int index) {
        return array[index];
    }

    public T[] create() {
        return array;
    }
}
//...

ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100);
Integer[] array = arrayToken.create();

So using reflection to initialize a generic array is an elegant implementation, because the generic typeT can only be determined at runtime. If we can create a generic array, it must be running in Java Think of a way at runtime, and the best technology that works at runtime is reflection.

 How to understand static methods and static variables in generic classes?

Static methods and static variables in a generic class cannot use generic type parameters declared by the generic class.

for example:

public class Test2<T> {    
    public static T one;   //编译错误    
    public static  T show(T one){ //编译错误    
        return null;    
    }    
}

Because the instantiation of generic parameters in a generic class is specified when the object is defined, static variables and static methods do not need to be called using objects. The object has not been created, so how to determine the type of this generic parameter, so of course it is wrong.

But please pay attention to distinguish the following situations:

public class Test2<T> {    

    public static <T >T show(T one){ //这是正确的    
        return null;    
    }    
}

Because this is a generic method, the T used in the generic method is the T defined in the method, not the T in the generic class.

 How to understand the use of generics in exceptions?

  • Objects of generic classes cannot be thrown or captured. In fact, it is not legal for a generic class to extend Throwable. For example: The following definition will not compile:
public class Problem<T> extends Exception {

}

Why can't Throwable be extended? Because exceptions are caught and thrown at runtime, and during compilation, all generic information will be erased. So, assuming that the above compilation is feasible, then, look at the following definition:

try{

} catch(Problem<Integer> e1) {

} catch(Problem<Number> e2) {

} 

After the type information is erased, the catches in the two places become the original type Object. In other words, the catches in the two places become exactly the same, which is equivalent to the following

try{

} catch(Problem<Object> e1) {

} catch(Problem<Object> e2) {

}

Of course this won't work.

  • Generic variables cannot be used in catch clauses
public static <T extends Throwable> void doWork(Class<T> t) {
    try {
        ...
    } catch(T e) { //编译错误
        ...
    }
}

Because the generic information has been changed to the original type during compilation, that is to say, the above T will be changed to the original type Throwable. If the generic variable can be used in the catch clause, then what is the following definition:

public static <T extends Throwable> void doWork(Class<T> t){
    try {

    } catch(T e) { //编译错误

    } catch(IndexOutOfBounds e) {

    }                         
}

According to the principle of exception capture, the subclass must be in the front and the parent class in the back, so the above violates this principle. Even if you use T when using this static method, it will still become Throwable after compilation. is a subclass of IndexOutofBounds, which violates the rules of exception capture. in principle. Therefore, in order to avoid such a situation, Java prohibits the use of generic variables in the catch clause. ArrayIndexOutofBoundsArrayIndexOutofBounds

  • But type variables can be used in exception declarations. The following methods are legal.
public static<T extends Throwable> void doWork(T t) throws T {
    try{
        ...
    } catch(Throwable realCause) {
        t.initCause(realCause);
        throw t; 
    }
}

There is no problem in using it like this.

 How to get the parameter type of a generic?

Since the type has been erased, how to obtain the parameter type of the generic type? Generics can be obtained through reflection (java.lang.reflect.Type)

java.lang.reflect.TypeIt is a public high-level interface for all types in Java, representing all types in Java. Types in the Type system include: array type (GenericArrayType), parameterized type (ParameterizedType), type variable (TypeVariable), wildcard type (WildcardType), Primitive type (Class), basic type (Class), the above types all implement the Type interface.

public class GenericType<T> {
    private T data;

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public static void main(String[] args) {
        GenericType<String> genericType = new GenericType<String>() {};
        Type superclass = genericType.getClass().getGenericSuperclass();
        //getActualTypeArguments 返回确切的泛型参数, 如Map<String, Integer>返回[String, Integer]
        Type type = ((ParameterizedType) superclass).getActualTypeArguments()[0]; 
        System.out.println(type);//class java.lang.String
    }
}

Among ParameterizedType:

public interface ParameterizedType extends Type {
    // 返回确切的泛型参数, 如Map<String, Integer>返回[String, Integer]
    Type[] getActualTypeArguments();
    
    //返回当前class或interface声明的类型, 如List<?>返回List
    Type getRawType();
    
    //返回所属类型. 如,当前类型为O<T>.I<S>, 则返回O<T>. 顶级类型将返回null 
    Type getOwnerType();
}

 

Guess you like

Origin blog.csdn.net/abclyq/article/details/134688182