Detailed explanation of Java generics

First contact generic completion

When using container classes

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

list.add(123);

Integer i = list.get(0);

Define < > as Integer. When writing code, you can only pass Integer type data to list.

And you can receive the elements of the list directly through an Integer type variable.

`

ArrayList<Test> list = new ArrayList<>(); Test test = list.get(0);

Also defined as a custom type.

`

ArrayList list = new ArrayList(); String str = (String) list.get(0);

You can also not define a type. The default is Object.

How can the ArrayList class insert any data of the same type? Through polymorphism?

If it is polymorphic, how do you do Integer I = list get(0); Don't you need to convert Object to Integer?

It's actually through generics.

JDK 1.5 introduces generics. Generics are added to the definitions of all container classes.

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    
public class HashMap<K,V> extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable 
    
public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable

ArrayList::add(e),ArrayList::get(index)

public boolean add(E e) {...}
public E get(int index) {...}

Prior to JDK 1.5, ArrayList could only be cast.

public void add(Object obj){...}
public Object get(int index){...}
JDK1.5 front...
ArrayList list = new ArrayList();
list.add(123);
list.add("haha");
Integer o1 = (Integer) list.get(0);
//ClassCastException 
Integer o2 = (Integer) list.get(1);

This can easily cause ClassCastException type conversion exceptions.

·

What are generics?

generic paradigm

concept

That is, a wide range of types, which can be passed as parameters.

Types in Java are divided into basic data types and reference data types. Generics can only use reference data types (arrays also apply). No ArrayList < int >, ArrayList < double >

benefit

  • Delay determining type until class / method / interface is used. Corresponding generic class, generic method and generic interface

  • Type safety: advance the type conversion check to the compilation time, prohibit all operations violating type safety, and effectively avoid ClassCastException.

Integer i = list.get(0);
//The development environment and compiler will prompt for type errors
list.add("haha");
String str = list.get(1);
  • Simplify code and improve readability.
ArrayList list = new ArrayList();
list.add(123);
Integer o1 = (Integer) list.get(0);
----------------------------
ArrayList<Integer> list = new ArrayList<>();
list.add(123);
Integer i = list.get(0); //Redundant cast code is not required
  • The element types in the collection class must be the same, and the enhanced for loop can be used; for-each
for(Integer i:list){
	...
}

Generics have nothing to do with polymorphism

Generic: the actual type can be determined after compilation.

Polymorphism: the actual type cannot be determined during compilation until the runtime. Polymorphism is based on Inheritance and calling virtual methods

Generic class

Before the class name, you can define the generic < T > in the class. When using the UseGeneric class, you need to pass in the type parameter to instantiate the type of T, and the type of the member variable key is also determined.

public class UseGeneric<T>{
    private T key;

    public T getKey() {
        return key;
    }

    public void setKey(T key) {
        this.key = key;
    }
}

The test class GenericTest instantiates the generic type T as String, and the key also determines that the type is String.

public class GenericTest {
    public static void main(String[] args) {
        UseGeneric<String> useGeneric = new UseGeneric<>();
        useGeneric.setKey("123");
        String key = useGeneric.getKey();
    }
}

Basic principles

After decompilating javap of GenericTest::main method:

public static void main(java.lang.String[]) throws java.lang.Exception;
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
0: new           #2                  // class generic/UseGeneric
3: dup
4: invokespecial #3                  // Method generic/UseGeneric."<init>":()V
7: astore_1
8: aload_1
9: ldc           #4                  // String 123
11: invokevirtual #5                  // Method generic/UseGeneric.setKey:(Ljava/lang/Object;)V
14: aload_1
15: invokevirtual #6                  // Method generic/UseGeneric.getKey:()Ljava/lang/Object;
18: checkcast     #7                  // class java/lang/String
21: astore_2
22: return
	LocalVariableTable: Local variable table
        Start  Length  Slot  Name   Signature
            0      23     0  args   [Ljava/lang/String;
            8      15     1 useGeneric   Lgeneric/UseGeneric;
           22       1     2   key   Ljava/lang/String;
	LocalVariableTypeTable: Field signature table
        Start  Length  Slot  Name   Signature
            8      15     1 useGeneric   Lgeneric/UseGeneric<Ljava/lang/String;>;

In the Code attribute of the method table, you can see:

15:invokevirtual calls the virtual method getKey() to see that the return value of the method is java/lang/Object.

Note: after compilation, T will be deleted Generic erase Type java/lang/Object

18: Checkcast type conversion check: check whether the actual type of key is String. If not, throw a strong conversion exception ClassCastException. (in fact, because of the existence of generics, checkcast must succeed in the type conversion operation here.)

After T is erased by generics, the compiler also helps us add type conversion code:

String key = useGeneric.getKey(); —> String key = (String) useGeneric.getKey();

`

In addition to the Code attribute, the main method table also has;

1.LocalVariableTable 2.LocalVariableTypeTable

Of which 1 Is a local variable table used to store information such as variable name and variable descriptor 2 It is a field feature Signature table that stores information such as the Signature attribute that records generic information.

Understanding the Java virtual machine:

After adding generics in JDK 5, the Signature attribute is added to the Class file specification and can appear in the attributes of Class, field table and method table structure. After that, if the generic Signature of any Class, interface, initialization method or member contains type variables or parameterized types, the Signature property will record the generic Signature information for it.

Because the generic information is stored in the Signature attribute. Therefore, even if the generics are erased during compilation, the fields and methods in the Class file can be deleted during program operation Get generic information dynamically of

`

generic method

The getKey method is not a generic method, but a method in a generic class

public class UseGeneric<T>{
    private T key;

    public T getKey() {
        return key;
    }
}

Whether a method is a generic method has nothing to do with its class. Only methods that declare and use type parameters are generic methods.

public <T> T compareAndReturn (T t1,T t2){
        return t1.equals(t2) ? t1 : null;
}

`

public <H> void test(ArrayList<H> list){
        list.add(123); //compile error
        list.add(list.get(0));
    }

If the type of T is unknown during compilation, it will be erased as Object. We can do nothing but call the method of Object class.

Moreover, because the type of T is unknown, for example, in the case of test() method, we cannot make operations that may violate type safety.

By limiting the upper bound < T extends xx > when defining type parameters, it indicates that t can only be a subclass of xx. T is erased as xx after compilation

public class Test{
	
	public <T extends Number> T compareAndReturn (T t1,T t2){
        //T is erased as Number
		return t1.doubleValue() > t2.doubleValue() ? t1 : t2;
	}
    
}

Qualification of type parameters

Upper bound of generics

The upper bound can be an entity class

< T extends Son>

When declaring a generic, you can specify the upper bound of the generic, indicating that after the generic is erased, T will be modified to Son.

The compareAndReturn method will look like this after compilation:

public Number compareAndReturn (Number t1,Number t2){
		return t1.doubleValue() > t2.doubleValue() ? t1 : t2;
}

When using this method, the type parameters passed in are limited to Number and its subclasses, and the actual type of the returned value is also Number and its subclasses.

Double aDouble = compareAndReturn(100.1, 100.2);

The upper bound can be an interface

< T extends Comparable>

public <T extends Comparable<T>> T compareAndReturn (T t1,T t2){
        return t1.compareTo(t2) > 0 ? t1 : t2;
}

< T extends Comparable > indicates that the type parameter passed in is limited to the implementation class of Comparable and can only be compared with the same type.

The upper bound can be other generics

//Custom method
public class EasyArrayList<E> {
	public <T> void put(ArrayList<T> list){
        for(T t:list){
            add(t); // need cast to E
        }
	}
}

public static void main(String[] args) {
	EasyArrayList<Number> list = new EasyArrayList<>();
	EasyArrayList<Integer> integers = new EasyArrayList<>();
    //Inserting elements of different types is not allowed
	list.put(integers);//compile error
}

After modifying the put method, you can add a collection of subclasses of Number.

public <T extends E> void put(ArrayList<T> list){
        for(T t:list){
            add(t);
        }
	}

generic interface

Interface and capability oriented programming

There are interfaces before implementation classes. Interfaces Abstract capabilities, and specific classes implement interfaces to obtain capabilities. For example, Comparator interface, Comparable comparability interface, an interface that can be implemented by anyone. Implementing the compare method means that the entity class has the ability to compare; Implementing the Comparable interface means that entity classes can be compared.

Before no generics
public interface Comparator{
	boolean compare(Object o1,Object o2);
}

class A implements Comparator{
    int key;
    int compare(Object o1,Object o2){
        ...
    }
}

With generics, the readability is improved, and the implementation class can more clearly understand how to implement the method.

public interface Comparator<T>{
	boolean compare(T o1,T o2);
}

class A implements Comparator{
    int key;
    int compare(A o1,A o2){
        if(o1.key == o2.key) 
            return 0;
        return o1.key > o2.key ? 1 : -1;
    }
}

Generic wildcard

Infinite wildcard <? >

Why wildcards?

When instantiating a parameter type, if you are not sure, you can use wildcards? Represents any type.

public void test(ArrayList<?> list){
      ...
}

The test method can receive List objects with various types of parameters

test(new ArrayList<Integer>());
test(new ArrayList<String>());

More concise type parameter qualification can be achieved by using wildcards:

public <T extends E> void put(ArrayList<T> list){
        for(T t:list){
            add(t);
        }
	}

-------------------------------
    //Restrict the incoming ArrayList generic to subclasses of E
public void put(ArrayList<? extends E> list){
        for (E e : list) {
            add(e);
        }
}

This looks like infinity wildcards are powerful. But in fact, it hurts.

public void test(ArrayList<?> guys){
	Object o = guys.get(0);//compile success
}

To get the data in the list, you have to use object to receive it. Object o = list.get(0) this runs counter to the original intention of generics.

Finite definite wildcards and supertype wildcards are given? The meaning of existence.

`

Before understanding these two wildcards, know the PECS principles.

PECS(Producer Extends Consumer Super)

"PECS" is from the collection's point of view. If you are only pulling items from a generic collection, it is a producer and you should use extends; if you are only stuffing items in, it is a consumer and you should use super. If you do both with the same collection, you shouldn't use either extends or super.

If the current container is only used to obtain elements, the container is equivalent to a producer. You should use <? extends T>

If the current container is only used to fill elements, the container is equivalent to a consumer. You should use <? super T>

Finite wildcard <? extends >

public void test(ArrayList<? extends Father> guys){
	Father guy = guys.get(0);//compile success
	guys.add(new Son()); // compile error
}

The producer collection is instantiated as a collection of Father and one of its subclasses. It could be Son or daugher's Daughter

ArrayList<Son> guys = new ArrayList<>();
ArrayList<Daughter> guys = new ArrayList<>();

If it is not possible to determine which of the above two during compilation, the compiler cannot guarantee type safety if data is filled into the collection. Therefore, the compiler directly prohibits filling data into the "producer" collection to avoid type errors in the following cases:

ArrayList<Son> sons = new ArrayList<>();
sons.add(new Daughter());
ArrayList<Daughter> daughters = new ArrayList<>();
daughters.add(new Sons());

However, the compiler allows the following operations:

ArrayList<? extends Father> guys = new ArrayList<>();
ArrayList<Son> sons = new ArrayList<>();
sons.add(new Son());
guys = sons;

The collection sons of subclasses whose parameter type is Father can be assigned to fathers.

The compiler knows that sons is a collection of Son subclasses and can point to a subclass collection object on the premise of ensuring type safety. This is equivalent to informing the compiler during compilation that only Son type data may exist in the current collection:

ArrayList<? extends Father> guys ... -> ... -> ... -> new ArrayList<Son>();

The "producer" collection can ensure that all elements in the collection are subclasses of Father. Therefore, the data in the collection can be received uniformly as Father static type.

Father guy = guys.get(0);

As can be seen from reflection, the generic type is <? Extend Father > after being erased, the static type is Father

Class<? extends Father> clazz = ...Cannot determine during compilation;
Father genericFather = clazz.newInstance();

Super type wildcard <? super >

ArrayList<? super Father> guys = new ArrayList<>();
guys.add(new Father());//compile success
guys.add(new Son());//compile success
guys.add(new GrandFaher());//compile error
guys.add(new Object())//compile error

< ? Super Father > means that the type parameter is instantiated as Father and its parent class.

The compiler only knows that the current "consumer" collection may be a collection of the Father class or its parent class

ArrayList<Father> guys = new ArrayList<>();
ArrayList<GrandFather> guys = new ArrayList<>();

The compiler cannot determine, so it simply prohibits adding data that cannot guarantee type safety to the current collection. But! The compiler can ensure that the data added to Father and its subclasses are type safe. Therefore, the "consumer" collection can arbitrarily add data of subclasses of limited types.

ArrayList<Father> fathers = new ArrayList<>();
ArrayList<GrandFather> grandFathers = new ArrayList<>();
fathers.add(new Son());
grandFathers.add(new Daughter());

It is also allowed by the compiler to create consumer collections in the following ways:

ArrayList<? super Father> guys = new ArrayList<>();
ArrayList<GrandFather> fathers = new ArrayList<>();
guys = fathers;

After the guys pointer points to the new collection again (grandFather object collection - > father object collection), there is only grandFather data in the current collection. The collection can also add any subclass object:

guys.add(new Son());
guys.add(new Daughter());
guys.add(new Father());

Therefore, the data in the current collection can only be received by the unified parent class Object of all classes. Because it is impossible to determine what type of collection the guys are initialized to and what kinds of data exist in the collection during compilation. In order to ensure type safety, you can only let the Object boss out of the mountain.

Object o = guys.get(0);

From the reflection, we can see that the generic <? After super father > is erased, the static type is Object

Class<? super Father> clazz = ...Cannot determine during compilation;
Object guy = clazz.newInstance()

Generic erase

The Java compiler implements generic erasure by type conversion.

Generics only work during compilation and only exist in type information during runtime.

Why generic erasure?

Backward compatibility, for example, lower versions of collection classes do not have generics

ArrayList list = new ArrayList();

After the updated version, the lower version of java code can continue to run

ArrayList list = new ArrayList();
amount to
ArrayList<Object> list = new ArrayList<>();
ArrayList<Integer> list = new ArrayList<>();
Erase
ArrayList list = new ArrayList();
Integer i = list.get(0);

Get generic information at runtime

The Signature constant in the class file information stores generic information.

For example, a field contains generic information

public class GenericTest {
    UseGeneric<String> useGeneric = new UseGeneric<>();
}    

javap view class file information

generic.UseGeneric<java.lang.String> useGeneric;
descriptor: Lgeneric/UseGeneric;
flags: ACC_STATIC
Signature: #26                          // Lgeneric/UseGeneric<Ljava/lang/String;>;

For example, the local variable or parameter of a method has generic information, and the relationship between the Signature constant and the local variable exists in the LocalVariableTypeTable

public void test(ArrayList<String> list){
    UseGeneric<Integer> useGeneric = new UseGeneric<>();
}

javap view class file information

 public void test(java.util.ArrayList<java.lang.String>);
    descriptor: (Ljava/util/ArrayList;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=2
         0: new           #21                 // class generic/UseGeneric
         3: dup
         4: invokespecial #22                 // Method generic/UseGeneric."<init>":()V
         7: astore_2
         8: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lgeneric/GenericTest;
            0       9     1  list   Ljava/util/ArrayList;
            8       1     2 useGeneric   Lgeneric/UseGeneric;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       9     1  list   Ljava/util/ArrayList<Ljava/lang/String;>;
            8       1     2 useGeneric   Lgeneric/UseGeneric<Ljava/lang/Integer;>;
    Signature: #63       // (Ljava/util/ArrayList<Ljava/lang/String;>;)V

Get generic information at runtime

Field usegeneric = new usegeneric < > ();

//Gets the Type information of the field useGeneric 
Type useGeneric = GenericTest.class.getDeclaredField("useGeneric").getGenericType();
//Strong conversion to its subclass, parameterized type information, ParameterizedType
ParameterizedType type = (ParameterizedType) useGeneric;
//Call ParameterizedType method and getActualTypeArguments to get generic information
System.out.println(type.getActualTypeArguments()[0]);

//output
class java.lang.String

Method parameters

public void test(ArrayList<String> list, HashMap<String,Thread> map){
    UseGeneric<Integer> useGeneric = new UseGeneric<>();
}
//Get method information
Method test = GenericTest.class.getDeclaredMethod("test", ArrayList.class, HashMap.class);
//Gets generic information for method parameters
for (Type genericParameterType : test.getGenericParameterTypes()) {
    //Force to ParameterizedType
    ParameterizedType pt = (ParameterizedType) genericParameterType;
    for (Type actualTypeArgument : pt.getActualTypeArguments()) {
        System.out.println(actualTypeArgument);
    }
}

//output
class java.lang.String
class java.lang.String
class java.lang.Thread

Reflection cannot get the local variable information, so it cannot get the generic information of the local variable.

`

Although the runtime can get generic information through reflection, it has little effect

Generic and static types

Static members in generic class C cannot use generics declared by the current class.

Generic class C needs to specify a type parameter every time it is created and used. If the type of static member changes with it, it violates the semantics of static - binding with type, and all instances of the class correspond to unique static members.

For example, the following code:

public class UseGeneric<T>{
    @CompileError
    private static T key;

    public T getKey() {
        return key;
    }

    public void setKey(T key) {
        this.key = key;
    }
	
    @ComplieError
    public static T test(){
        ...
    }
    
    @ComplieError
    public static <H extends T> void test1(){
        ...
    }
    
    @ComplieSuccess
    public static <D,J,W> J staticTest(ArrayList<D> list){
        ...
    }

}

Static members cannot use type parameters of type. However, static methods can customize type parameters.


reference resources:
Old horse says programming - generics

Keywords: Java

Added by error_22 on Mon, 03 Jan 2022 20:44:21 +0200