JDK source code -- Generic principle

Posted by gyash on Tue, 23 Nov 2021 04:13:58 +0100

abstract

After JDK1.8, generics have been introduced into the JDK source code, but many beginners, junior engineers and even intermediate engineers do not understand this generics deeply enough. This paper introduces the concept and use of java generics in detail.

What are generics?

Generic, or "parameterized type". When referring to parameters, the most familiar thing is that there is a formal parameter when defining the method, and then the arguments are passed when the method is called. So what about parameterized types? As the name suggests, it is to parameterize the type from the original specific type, which is similar to the variable parameters in the method. At this time, the type is also defined in the form of parameters (which can be called type parameters), and then the specific type (type arguments) is passed in during use / call.

The essence of generics is to parameterize types (the types of formal parameters are controlled by different types specified by generics without creating new types). In other words, in the process of using generics, the data type of the operation is specified as a parameter. This parameter type can be used in classes, interfaces and methods, which are called generic classes, generic interfaces and generic methods respectively.

Code example

List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);

for(int i = 0; i< arrayList.size();i++){
    String item = (String)arrayList.get(i);
    Log.d("Generic testing","item = " + item);
}

There is no doubt that the running result of the program will end in a crash:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

ArrayList can store any type. In the example, a String type and an Integer type are added. When they are used again, they are used in the form of String, so the program crashes. In order to solve such problems (which can be solved at the compilation stage), generics came into being.

We initialize the first line declaration list If you change your code, the compiler will help us find such problems at the compilation stage.

List<String> arrayList = new ArrayList();

//arrayList.add(100);  During the compilation phase, the compiler will report an error

Type Erasure

Type erasure refers to associating generic type instances to the same bytecode through type parameter merging. The compiler generates only one bytecode for a generic type and associates its instance to the bytecode. The key to type erasure is to clear the relevant information of type parameters from generic types, and add type checking and type conversion methods when necessary.

Type erasure can be simply understood as converting generic java code into ordinary java code, but the compiler is more direct and directly converts generic java code into ordinary Java bytecode.

The main process of type erasure is as follows:

  • 1. Replace all generic parameters with their leftmost boundary (top-level parent type).
  • 2. Remove all type parameters.

Generics are only valid at compile time. Look at the following code:

List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();

Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();

if(classStringArrayList.equals(classIntegerArrayList)){
    Log.d("Generic testing","Same type");
}

Output results: D/Generic testing: Same type.

The above example can prove that the program will take de genericization measures after compilation. In other words, generics in Java are only valid at the compilation stage. In the compilation process, after the generic results are correctly verified, the relevant information of the generic will be erased, and the methods of type checking and type conversion will be added at the boundary where the object enters and leaves the method. That is, generic information does not enter the runtime phase.

This can be summed up in one sentence: generic types are logically regarded as multiple different types, which are actually the same basic types. Here is the type of erasure to be discussed below.

The process by which the Java compiler handles generics

public static void main(String[] args) {  
    Map<String, String> map = new HashMap<String, String>();  
    map.put("name", "chenHao");  
    map.put("age", "23");  
    System.out.println(map.get("name"));  
    System.out.println(map.get("age"));  
}
//After decompilation, we found that the generics were gone, the program changed back to the writing method before the emergence of Java generics, and the generic types changed back to the native types

public static void main(String[] args) {  
    Map map = new HashMap();  
    map.put("name", "chenHao");  
    map.put("age", "23"); 
    System.out.println((String) map.get("name"));  
    System.out.println((String) map.get("age"));  
}
interface Comparable<A> {
    public int compareTo(A that);
}

public final class NumericValue implements Comparable<NumericValue> {
    private byte value;

    public NumericValue(byte value) {
        this.value = value;
    }

    public byte getValue() {
        return value;
    }

    public int compareTo(NumericValue that) {
        return this.value - that.value;
    }
}
// After Decompilation 

interface Comparable {
  public int compareTo( Object that);
} 

public final class NumericValue
    implements Comparable
{
    public NumericValue(byte value)
    {
        this.value = value;
    }
    public byte getValue()
    {
        return value;
    }
    public int compareTo(NumericValue that)
    {
        return value - that.value;
    }
    public volatile int compareTo(Object obj)
    {
        return compareTo((NumericValue)obj);
    }
    private byte value;
}

  After the second generic class Comparable < A > is erased, A is replaced with the leftmost boundary Object. The type parameter NumericValue of Comparable < NumericValue > is erased, but this directly leads to that NumericValue does not implement the compareTo(Object that) method of the interface Comparable, so the compiler acts as A good person and adds A bridge method.

public class Collections {
    public static <A extends Comparable<A>> A max(Collection<A> xs) {
        Iterator<A> xi = xs.iterator();
        A w = xi.next();
        while (xi.hasNext()) {
            A x = xi.next();
            if (w.compareTo(x) < 0)
                w = x;
        }
        return w;
    }
}
// Decompile post-processing

public class Collections
{
    public Collections()
    {
    }
    public static Comparable max(Collection xs)
    {
        Iterator xi = xs.iterator();
        Comparable w = (Comparable)xi.next();
        while(xi.hasNext())
        {
            Comparable x = (Comparable)xi.next();
            if(w.compareTo(x) < 0)
                w = x;
        }
        return w;
    }
}

In the third example, the boundary of the type parameter < a extends comparable < a > > A is defined. A must be a subclass of comparable < a >. According to the type erasure process, first change all the type parameters ti to the leftmost boundary comparable < a >, and then remove the parameter type A to obtain the final erasure result.

Use of generics

There are three ways to use generics: generic class, generic interface and generic method

Generic class

Generic types are used in the definition of classes and are called generic classes. Through generics, you can complete the operation of a group of classes and open the same interface to the outside world. The most typical are various container classes, such as List, Set and Map.

class Class name <Generic ID: you can write any ID number to identify the type of the specified generic>{
  private Generic identity /*(Member (variable type)*/ var; 
  .....

  }
}
//Here, T can be written as any identifier. Common parameters such as T, E, K and V are often used to represent generics
//When instantiating a generic class, you must specify the concrete type of T
public class Generic<T>{ 
    //key the type of this member variable is t, and the type of T is specified externally  
    private T key;

    public Generic(T key) { //The type of generic constructor parameter key is also t, and the type of T is specified externally
        this.key = key;
    }

    public T getKey(){ //The return value type of the generic method getKey is t, and the type of T is specified externally
        return key;
    }
}


//Type parameters of generic types can only be class types (including custom classes), not simple types
//The type of the passed in argument must be the same as that of the generic type parameter, that is, Integer
Generic<Integer> genericInteger = new Generic<Integer>(123456);

//The input argument type must be the same as the type parameter type of the generic type, that is, String
Generic<String> genericString = new Generic<String>("key_vlaue");
Log.d("Generic testing","key is " + genericInteger.getKey());
Log.d("Generic testing","key is " + genericString.getKey());

  be careful:

  • The type parameter of a generic type can only be a class type, not a simple type.
  • The instanceof operation cannot be used on an exact generic type. If the following operation is illegal, an error will occur during compilation.   if(ex_num   instanceof Generic<Number>){ }

generic interface

The definition and use of generic interfaces and generic classes are basically the same. Generic interfaces are often used in various producers. See an example:

//Define a generic interface
public interface Generator<T> {
    public T next();
}

When a class implementing a generic interface does not pass in a generic argument:  

/**
 * When a generic argument is not passed in, it is the same as the definition of a generic class. When declaring a class, you need to add the declaration of the generic to the class
 * Namely: class fruitgenerator < T > implements generator < T >{
 * If you do not declare a generic type, such as: class fruitgenerator implements generator < T >, the compiler will report an error: "Unknown class"
 */
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}

When a class implementing a generic interface passes in a generic argument:

/**
 * When passing in generic arguments:
 * Define a producer to implement this interface, although we have only created a generic interface generator < T >
 * However, we can pass in countless arguments for T to form countless types of Generator interfaces.
 * When implementing a class to implement a generic interface, if a generic type has been passed into an argument type, all places where generics are used must be replaced with the passed in argument type
 * Namely: generator < T >, public T next(); The T in should be replaced with the incoming String type.
 */
public class FruitGenerator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}

Generic wildcard

We know that Integer is a subclass of Number. At the same time, we also verified that generic < Integer > and generic < Number > are actually the same basic type in the features section. So the question is, in the method using generic < Number > as a formal parameter, can you use the instance of generic < Integer >? Logically similar to generic < Number > and generic < Integer >, can they be regarded as generic types with parent-child relationship?

public void showKeyValue1(Generic<Number> obj){
    Log.d("Generic testing","key value is " + obj.getKey());
}


Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);

showKeyValue(gNumber);

// The compiler of the showKeyValue method will report an error for us: generic < Java. Lang. integer > 
// cannot be applied to Generic<java.lang.Number>
// showKeyValue(gInteger);

Through the prompt, we can see that generic < integer > cannot be regarded as a subclass of ` generic < number >. It can be seen that the same generic type can correspond to multiple versions (because the parameter type is uncertain), and different versions of generic class instances are incompatible. Back to the above example, how to solve the above problem? You can never define a new method to handle classes of generic < integer > type, which is obviously contrary to the concept of multiple machines in java. Therefore, we need a reference type that can logically represent both generic < integer > and generic < number > parent classes. This type wildcard came into being.

We can change the above method:

public void showKeyValue1(Generic<?> obj){
    Log.d("Generic testing","key value is " + obj.getKey());
}

Type wildcards are generally used? Instead of specific type arguments, note that '?' Is a type argument, not a type parameter  . Say it three times! Here '?' Is a type argument, not a type parameter  !  Here '?' Is a type argument, not a type parameter  ! To be more straightforward means, here? Like Number, String and Integer, it is an actual type, which can be set as "Number", "String" and "Integer"? As a parent of all types. Is a real type.

When the specific type is uncertain, this wildcard is  ?  ; When operating a type, only the functions in the Object class are used when the specific functions of the type do not need to be used. So you can use it? Wildcard to table unknown type.

generic method

Generic class indicates the specific type of the generic when instantiating the class; Generic method refers to the specific type of generic when calling the method  .

In java, the definition of generic classes is very simple, but generic methods are more complex. In particular, the member methods in most generic classes we see also use generics. Some generic classes even contain generic methods, so it is very easy for beginners to misunderstand generic methods.

/**
 * Basic introduction to generic methods
 * @param tClass Generic argument passed in
 * @return T The return value is of type T
 * explain:
 *     1)public < T > is very important in the middle of the return value, which can be understood as declaring this method as a generic method.
 *     2)Only methods that declare < T > are generic methods. Member methods that use generics in generic classes are not generic methods.
 *     3)<T>Indicates that the method will use generic type T. only then can generic type t be used in the method.
 *     4)Like the definition of generic classes, T here can be written as any identifier. Common parameters such as T, E, K, V, etc. are often used to represent generics.
 */
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
  IllegalAccessException{
        T instance = tClass.newInstance();
        return instance;
}

Basic usage of generic methods

public class GenericTest {
   //This class is a generic class, which has been introduced above
   public class Generic<T>{     
        private T key;

        public Generic(T key) {
            this.key = key;
        }

        //What I want to say is this. Although generics are used in the method, it is not a generic method.
        //This is just an ordinary member method in the class, but its return value is the declared generic of the declared generic class.
        //Therefore, we can continue to use the generic type T in this method.
        public T getKey(){
            return key;
        }

        /**
         * There is obviously a problem with this method. The compiler will prompt us with the error message "cannot reslove symbol E"
         * Because generic e is not declared in the class declaration, the compiler will not recognize it when using E as formal parameter and return value type.
        public E setKey(E key){
             this.key = keu
        }
        */
    }

    /** 
     * This is a real generic method.
     * First, the < T > between public and the return value is essential, which indicates that this is a generic method and declares a generic t
     * This T can appear anywhere in the generic method
     * The number of generics can also be any number 
     *    For example: public < T, k > k showkeyname (generic < T > container){
     *        ...
     *        }
     */
    public <T> T showKeyName(Generic<T> container){
        System.out.println("container key :" + container.getKey());
        //Of course, this example is not appropriate, just to illustrate the characteristics of generic methods.
        T test = container.getKey();
        return test;
    }

    //This is not a generic method, but a common method. It just uses the generic < number > class as a formal parameter.
    public void showKeyValue1(Generic<Number> obj){
        Log.d("Generic testing","key value is " + obj.getKey());
    }

    //This is not a generic method, it is also a common method, but uses generic wildcards?
    //At the same time, it also confirms that? Is a type argument described in the chapter on generic wildcards, which can be regarded as the parent of all classes such as Number
    public void showKeyValue2(Generic<?> obj){
        Log.d("Generic testing","key value is " + obj.getKey());
    }

     /**
     * There is a problem with this method. The compiler will prompt us with an error message: "unknown class' e '"
     * Although we declare < T >, we also show that this is a generic method that can handle generic types.
     * However, only the generic type T is declared, and the generic type E is not declared, so the compiler does not know how to deal with the type E.
    public <T> T showKeyName(Generic<E> container){
        ...
    }  
    */

    /**
     * There is also a problem with this method. The compiler will prompt us with an error message: "unknown class't '"
     * For the compiler, T is not declared in the project, so the compiler does not know how to compile this class.
     * So this is not a correct generic method declaration.
    public void showkey(T genericObj){

    }
    */

    public static void main(String[] args) {


    }
}

Generic methods in classes

Of course, this is not the whole of generic methods. Generic methods can be used anywhere and in any scenario. However, there is a very special case. When generic methods appear in generic classes, let's take another example.

public class GenericFruit {
    class Fruit{
        @Override
        public String toString() {
            return "fruit";
        }
    }

    class Apple extends Fruit{
        @Override
        public String toString() {
            return "apple";
        }
    }

    class Person{
        @Override
        public String toString() {
            return "Person";
        }
    }

    class GenerateTest<T>{
        public void show_1(T t){
            System.out.println(t.toString());
        }

        //A generic method is declared in a generic class, using generic E, which can be of any type. The type can be the same as T or different.
        //Since generic methods declare generics < E > when they are declared, the compiler can correctly recognize generics recognized in generic methods even if generics are not declared in generic classes.
        public <E> void show_3(E t){
            System.out.println(t.toString());
        }

        //A generic method is declared in the generic class and generic t is used. Note that this t is a new type and can not be the same type as the T declared in the generic class.
        public <T> void show_2(T t){
            System.out.println(t.toString());
        }
    }

    public static void main(String[] args) {
        Apple apple = new Apple();
        Person person = new Person();

        GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();
        //apple is a subclass of Fruit, so you can
        generateTest.show_1(apple);
        //The compiler will report an error because the generic type argument specifies Fruit and the passed in argument class is Person
        //generateTest.show_1(person);

        //Using both methods can be successful
        generateTest.show_2(apple);
        generateTest.show_2(person);

        //Using both methods can also be successful
        generateTest.show_3(apple);
        generateTest.show_3(person);
    }
}

Generic methods and variable parameters

public <T> void printMsg( T... args){
    for(T t : args){
        Log.d("Generic testing","t is " + t);
    }
}

printMsg("111",222,"aaaa","2323.4",55.55);

Static methods and generics

Static methods need to pay attention to one situation, that is, the static methods in the class use generics: static methods cannot access the generics defined on the class; If the reference data type of static method operation is uncertain, the generic type must be defined on the method.

That is, if static methods want to use generics, static methods must also be defined as generic methods  .

public class StaticGenerator<T> {
    ....
    ....
    /**
     * If you define a static method that uses generics in a class, you need to add additional generic declarations (define this method as a generic method)
     * Even if static methods want to use generics already declared in generic classes, they can't.
     * For example: public static void show (T) {..}, the compiler will prompt an error message:
          "StaticGenerator cannot be refrenced from static context"
     */
    public static <T> void show(T t){

    }
}

Generic upper and lower boundary

When using generics, we can also restrict the upper and lower boundaries of the passed in generic type arguments. For example, type arguments can only be passed in a certain type of parent class or a certain type of child class.

Add an upper boundary for generics, that is, the passed in type argument must be a subtype of the specified type

public void showKeyValue1(Generic<? extends Number> obj){
    Log.d("Generic testing","key value is " + obj.getKey());
}

Generic<String> generic1 = new Generic<String>("11111");
Generic<Integer> generic2 = new Generic<Integer>(2222);
Generic<Float> generic3 = new Generic<Float>(2.4f);
Generic<Double> generic4 = new Generic<Double>(2.56);

//In this line of code, the compiler will prompt an error because the String type is not a subclass of the Number type
//showKeyValue1(generic1);

showKeyValue1(generic2);
showKeyValue1(generic3);
showKeyValue1(generic4);

If we change the definition of generic classes:

public class Generic<T extends Number>{
    private T key;

    public Generic(T key) {
        this.key = key;
    }

    public T getKey(){
        return key;
    }
}

//This line of code will also report an error because String is not a subclass of Number
Generic<String> generic1 = new Generic<String>("11111");

Another example of generic methods:

//When adding upper and lower boundary restrictions in generic methods, you must add upper and lower boundaries on the < T > between the permission declaration and the return value, that is, when the generic method is declared
//Public < T > t showkeyname (generic < T extensions number > container), the compiler will report an error: "Unexpected bound"
public <T extends Number> T showKeyName(Generic<T> container){
    System.out.println("container key :" + container.getKey());
    T test = container.getKey();
    return test;
}

Blog reference

Topics: JDK