The relationship between Java's type erasure and generics

Before discussing type erasure, we must first understand java generics. The so-called generic is a parameterized type. This means that we can pass concrete types as a parameter to methods, classes, and interfaces.

Why do we need generics? First of all, we all know that in java, Object is the parent class of the object. Object can refer to any type of object. But this will bring type safety problems. The emergence of generics brings the function of type safety to java .

  • Generic method:
class Test {
    
    
	static <T> void helloworld(T t){
    
    }
}
  • generic class
 
class Test<T> {
    
    
	T obj
	Test(T obj){
    
    
		this.obj = obj;
	}
}
  • generic interface
interface Test<T> {
    
    
	T getData();
}

Using generics can provide code reuse, using one code to apply to different types. Secondly, generics also guarantee type safety (can be checked at compile time). For example:

ArrayList<String> list = new ArrayList<>();
list.add("hello");
list.add("world");

// 编译器会阻止下面的操作,从而保证了我们的类型安全
list.add(100); // error

Another advantage of generics is that no separate type conversion is required, such as:

ArrayList<String> list = new ArrayList<>();
list.add("hello");
list.add("world");


String s0 = (String)list.get(0); // 类型转换是不需要做的
// 下面展示了,我们不需要进行单独的类型转换
String s1 = list.get(0);
String s2 = list.get(1);

The foreshadowing has been done before, and it's time for us to talk about type erasure.

What is type erasure? The so-called type erasure is a process of enforcing type constraints only at compile time and discarding type information at runtime. As we now have such a generic method:

public static <T> boolean myequal(T t1, T t2){
    
    
	return t1.equals(t2);
}

The compiler will replace the type T with Object, as follows:

public static <Object> boolean myequal(Object t1, Object t2){
    
    
	return t1.equals(t2);
}

Type erasure for generic classes

Type erasure at the class level follows this rule: first the compiler discards the type parameter on the class and replaces it with its first bound type, or Object if the type parameter is not bound.

  • parameter type is not bound
public class MyClass<T> {
    
    
	private T[] elements;
	public void doSomething(T element){
    
    }
	public T getSomething(){
    
    }
}

The type parameter T of MyClass is not bound to any type, so T will be replaced by Object, and the result of the replacement is:

public class MyClass {
    
    
	private Object[] elements;
	public void doSomething(Object element){
    
    }
	public Object getSomething(){
    
    }
}
  • The parameter type is bound
interface MyT {
    
    }
public class MyClass<T extends MyT> {
    
    
	private E[] elements;
	public void doSomething(T element){
    
    }
	public T getSomething(){
    
    }
}

MyTClass is the first type to which the type parameter T of MyClass is bound, so T will be replaced by MyTClass:

public class MyClass {
    
    
	private MyT[] elements;
	public void doSomething(MyT element){
    
    }
	public MyT getSomething(){
    
    }
}

Why is it OK to take the first binding? For example, if MyT has a parent class, and the parent class has a parent class, then our type parameters have many indirect bindings, and the first binding covers all parent classes, so use the first Just bind it.

Type erasure for generic methods

For a generic method, its type parameters will not be stored, it follows this rule: first the compiler discards the type parameter on the method, and replaces it with its first bound type, if the type parameter is not bound, Just use Object to replace.

  • parameter type is not bound
public static <T> void printSomething(T[] arr){
    
    
	for(T item: arr) {
    
    
		System.out.printf("%s", item);
	}
}

The above method, after performing type erasure results:

public static void printSomething(Object[] arr){
    
    
	for(Object item: arr) {
    
    
		System.out.printf("%s", item);
	}
}
  • The parameter type is bound
public static <T extends MyT> void printSomething(T[] arr){
    
    
	for(T item: arr) {
    
    
		System.out.printf("%s", item);
	}
}

The above method, after performing type erasure results:

public static void printSomething(MyT[] arr){
    
    
	for(MyT item: arr) {
    
    
		System.out.printf("%s", item);
	}
}

Bridge methods generated in type erasure

In addition to the above rules, for those similar methods, the compiler will create some synthetic methods to distinguish them. This synthetic method is to extend the same method signature of the first bound class . This sentence will be obtained through the following example A more intuitive understanding.
First we have such a class:

class MyQueue<T> {
    
    
    private T[] elements;

    public MyQueue(int size){
    
    
        this.elements = (T[])new Object[size];
    }

    public void add(T data){
    
    }
    public T dequeue(){
    
    
        if(elements.length > 0){
    
    
            return elements[0];
        } else {
    
    
            return null;
        }
    }
}

After the type erasure of the above class, T will be replaced by Object (please refer to the previous part for specific rules). We now write a class to inherit MyQueue:

class IntegerQueue extends MyQueue<Integer> {
    
    

    public IntegerQueue(int size){
    
    
        super(size);
    }

    @Override
    public void add(Integer data) {
    
    
        super.add(data);
    }
}

Then we write a test method to quote the principle of its synthetic method:
Please add a picture description

We can see that the parameter type of the add method of MyQueue has changed to Object, so queue.add("Helllo")it makes sense. The IDE also suggested that there might be a problem with this type: Please add a picture description
In fact, this is not what we want, because IntegerQueue only wants to receive Int type. Then when we run this test case, we see the following error: Please add a picture description
This example once again shows that generics are a function introduced for type safety. How does the compiler do it? How does it work?

In fact, the compiler generates an additional method (the synthetic method mentioned above) to do the bridge. We all know that after MyQueue is type-erased, it will become as follows:

class MyQueue {
    
    
    private Object[] elements;

    public MyQueue(int size){
    
    
        this.elements = (Object[])new Object[size];
    }
    public void add(Object data){
    
    }
    public Object dequeue(){
    
    
        if(elements.length > 0){
    
    
            return elements[0];
        } else {
    
    
            return null;
        }
    }
}

IntegerQueueThe methods of public void add(Integer data){}and MyQueue public void add(Object data){}are similar, the compiler will create an intermediate method for similar methods to bridge between them. In order to ensure the type erasure of generic polymorphism, this method is generated IntegerQueue, and the compiler can ensure that this similar method will not match wrongly, that is, the compiler will create a synthetic method between them. bridge between. As mentioned above, the bridge method created by the compiler is as follows:

 static class IntegerQueue extends MyQueue<Integer> {
    
    


        public void add(Object data){
    
    
            add((Integer) data);
        }
        
        public void add(Integer data) {
    
    
            super.add(data);
        }
    }

The synthetic method in this example is

public void add(Object data){
    
    
       add((Integer) data);
}

It is a method signature that extends the same first bound class , in other words, their method names are the same, and the type of the parameter of this synthetic method is the first bound class after type erasure (specifically You can refer to the previous section). Its function is to bridge the add method in the IntegerQueue class and the add method in its parent class to solve the type safety problem of generics in inheritance.

Guess you like

Origin blog.csdn.net/weixin_40763897/article/details/128725568
Recommended