Java generic erasure

List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();

System.out.println(l1.getClass() == l2.getClass());
The output is true

Generics are involved in the above code, and the reason for the output result is type erasure. Let's talk about generics first.

Definition and use of generics

Generics can be divided into three types according to their usage.

  • Generic class.

  • Generic methods.

  • Generic interface.

Generic class

We can define a generic class like this.

public class Test<T> {
  T field1;
}

The T in angle brackets < > is called a type parameter and is used to refer to any type. In fact, t is just a habitual way of writing, if you like. You can write that.

public class Test<Hello> {
  Hello field1;
}

However, for specification purposes, Java still recommends that we use a single uppercase letter to represent type parameters. Common problems include:

  • T stands for any class in general.

  • E stands for Element or Exception.

  • K stands for Key.

  • V stands for Value, which is usually used together with K.

  • S stands for Subtype

If a class is formally defined, it is called a generic class.

Test<String> test1 = new Test<>();
Test<Integer> test2 = new Test<>();

As long as the corresponding type is assigned in angle brackets when creating an instance of a generic class. T will be replaced with the corresponding type, such as String or Integer. You can imagine that when a generic class is created, it is automatically expanded into the following code.

public class Test<String> {
  String field1;
}

Of course, a generic class cannot accept one type parameter, it can also accept multiple type parameters in this way.

public class MultiType <E,T>{
  E value1;
  T value2;
  
  public E getValue1(){
    return value1;
  }
  
  public T getValue2(){
    return value2;
  }
}

generic method

public class Test1 {
  public <T> void testMethod(T t){
    
  }
}

The difference between generic methods and generic classes is that the part of type parameters, that is, angle brackets, is written in front of the return value. T in is called a type parameter, while t in a method is called a parameterized type, which is not a real parameter at run time.

Of course, the declared type parameter can also be used as the type of return value.

public  <T> T testMethod1(T t){
    return null;
}

Coexistence of generic classes and generic methods

public class Test1<T>{
​
  public  void testMethod(T t){
    System.out.println(t.getClass().getName());
  }
  public  <T> T testMethod1(T t){
    return t;
  }
}

In the above code, Test1 is a generic class, testMethod is a common method in a generic class, and testMethod1 is a generic method.

The type parameters in the generic class have no corresponding relationship with the type parameters in the generic method. The generic method always takes the type parameters defined by itself as the standard.

Therefore, for the above code, we can write the test code like this.

Test1<String> t = new Test1();
t.testMethod("generic");
Integer i = t.testMethod1(new Integer(1));

The actual type parameter of the generic class is String, while the type parameter passed to the generic method is Integer, which is irrelevant.

However, in order to avoid confusion, if there are generic methods in a generic class, it is better not to have the same name for the type parameters of both. For example, the Test1 code can be changed to

public class Test1<T>{
​
  public  void testMethod(T t){
    System.out.println(t.getClass().getName());
  }
  public  <E> E testMethod1(E e){
    return e;
  }
}

generic interface

Generic interfaces are similar to generic classes

public interface Iterable<T> {
}

Wildcard?

In addition to using to represent generics, there is <? > This form.? They are called wildcards.

class Base{}
​
class Sub extends Base{}
​
Sub sub = new Sub();
Base base = sub;      

The above code shows that Base is the parent class of Sub, and there is an inheritance relationship between them, so the instance of Sub can assign a value to a Base reference, then

    
List<Sub> lsub = new ArrayList<>();
List<Base> lbase = lsub;

Does the last line of code hold? Will the compilation pass?

The answer is No.

The compiler won't let it pass. Sub is a subclass of Base, which does not mean that List and List have inheritance relationship.

However, in real-world coding, there is a real demand that generic types can handle data types in a certain range, such as a class and its subclasses. For this, Java introduces the concept of wildcard.

So wildcards appear in the specified range.

There are three forms of wildcards.

<?>It is called infinite wildcard.

<? extends T>It is called a wildcard with an upper limit.

<? super T>It is called a wildcard with a lower limit.

Infinite wildcard <? >

Indefinite wildcards are often used with container classes? In fact, it represents an unknown type, so it involves? The operation must be independent of the specific type.

public void testWildCards(Collection<?> collection){
}

In the above code, the parameter in the method is a Collection object modified by infinite wildcard, which implicitly expresses an intention or qualification, that is, testWidlCards(). There is no need to pay attention to the real type in the Collection inside the method, because it is unknown.

Therefore, you can only call type independent methods in the Collection.

When <? > When it exists, the Collection object loses the function of the add() method and the compiler does not pass.

List<?> wildlist = new ArrayList<String>();
wildlist.add(123);// Compilation failed
It provides read-only function, that is, it reduces the ability to add specific type elements, and only retains functions irrelevant to specific types. No matter what type of elements are loaded in the container, it only cares about the number of elements and whether the container is empty

<? extends T>

<?>It means that the type is unknown, but we really need to describe the type more accurately. We want to determine the category within a range, such as type A And type A All subclasses are OK.

<? extends T> Representative type T and T Subclass of
public void testSub(Collection<? extends Base> para){ } 

In the above code, the para Collection accepts the types of Base and its subclasses. However, it still loses the ability to write. in other words

para.add(new Sub()); 
para.add(new Base()); 

Still failed to compile. It doesn't matter. We don't know the specific type, but we at least know the scope of the type.

<? super T> This and <? extends T>  Corresponding, representative T and T Super class.
public void testSuper(Collection<? super Sub> para){ } 
 <? super T>. The magic is that it has a certain degree of write ability.
public void testSuper(Collection<? super Sub> para){ 
      para.add(new Sub());//Compile through 
      para.add(new Base());//Compilation failed 
} 

The difference between wildcard and type parameter

Generally speaking, anything that wildcards can do can be replaced with type parameters. such as

public void testWildCards(Collection<?> collection){} 

Can be

public <T> void test(Collection<T> collection){}

replace

It is worth noting that if the wildcard is replaced by the generic method, the collection in the above code can be written. It's just a cast.

public <T> void test(Collection<T> collection){
  collection.add((T)new Integer(12));
  collection.add((T)"123");
}

It should be noted that type parameters are applicable to category dependencies between parameters, for example.

public class Test2 <T,E extends T>{
  T value1;
  E value2;
}
public <D,S extends D> void test(D d,S s){
​
  }

Type E is a subclass of type T. obviously, the type parameter is more suitable in this case.
In one case, wildcards are used with type parameters.

public <T> void test(T t,Collection<? extends T> collection){
  
}

If the return type of a method depends on the type of the parameter, there is nothing wildcards can do.

public <T> T test1(T t){
  return value1;
}

Type Erasure

Genericity is a concept introduced in Java version 1.5. There was no concept of genericity before, but obviously, the generic code can be well compatible with the code of previous versions.

This is because generic information only exists in the code compilation stage. Before entering the JVM, the information related to generics will be erased, which is called type erasure in professional terms.

Generally speaking, generic classes and ordinary classes are nothing special in the java virtual machine. Review the code at the beginning of the article

List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
    
System.out.println(l1.getClass() == l2.getClass());

The printed result is true because the Class of List and List in the jvm is List Class.

Generic information was erased.

Students may ask, what about the types of String and Integer?

The answer is generic translation.

public class Erasure <T>{
  T object;
​
  public Erasure(T object) {
    this.object = object;
  }
}

Erase is a generic class. We can view its state information at runtime through reflection.

Erasure<String> erasure = new Erasure<String>("hello");
Class eclz = erasure.getClass();
System.out.println("erasure class is:"+eclz.getName());

result:

erasure class is:FinalizeEscapeGC$Erasure

The type of Class is still Erasure, not Erasure. Let's look at the specific type of T in the generic Class in the jvm.

        Erasure<String> erasure = new Erasure<String>("hello");
        Class eclz = erasure.getClass();
        Field[] fs = eclz.getDeclaredFields();
        for ( Field f:fs) {
            System.out.println("Field name "+f.getName()+" type:"+f.getType().getName());
        }

Result: field name object type: Java lang.Object

Can we say that after the generic class is erased by the type, the corresponding type is replaced by the Object type?

This statement is not entirely correct.

Let's change the code.

public class Erasure <T extends String>{
//  public class Erasure <T>{
  T object;
​
  public Erasure(T object) {
    this.object = object;
  }
}

Result: field name object type: Java lang.String

We can now conclude that when the generic class is erased by type, if the upper limit is not specified in the type parameter part of the previous generic class,

If the upper limit is specified, the type parameter will be replaced with the upper limit of the type.

So, in reflection

public class Erasure <T>{
  T object;
​
  public Erasure(T object) {
    this.object = object;
  }
  
  public void add(T object){
    
  }
}

The signature of the Method corresponding to the add() Method should be object class.

Erasure<String> erasure = new Erasure<String>("hello");
Class eclz = erasure.getClass();
System.out.println("erasure class is:"+eclz.getName());
​
Method[] methods = eclz.getDeclaredMethods();
for ( Method m:methods ){
  System.out.println(" method:"+m.toString());
}

The print result is

method:public void FinalizeEscapeGC$Erasure.add(java.lang.Object)

In other words, if you want to find the Method corresponding to add in the reflection, you should call getdeclaraedmethod ("add", Object. Class), otherwise the program will report an error and prompt that there is no such Method. The reason is that when the type is erased, T is replaced with Object type. Limitations of type erasure

Limitations of type erasure

Type erasure is the reason why generics can coexist with the code of previous java versions. But also because of type erasure, it will erase many inheritance related features, which is its limitation.

Understanding type erasure can help us bypass the minefields we may encounter in development. Similarly, understanding type erasure can also help us bypass some limitations of generics itself. such as

Normally, due to the limitation of generics, the compiler will not let the last line of code compile because of similar mismatch. However, based on the understanding of type erasure, we can bypass this limitation by using reflection

public interface List<E> extends Collection<E>{
  
   boolean add(E e);
}

The above is the source code definition of List and its add() method.

Because E represents any type, when erasing a type, the add method is actually equivalent to

boolean add(Object obj);

Then, using reflection, we bypass the compiler to call the add method.

public class ToolTest {
​
​
  public static void main(String[] args) {
    List<Integer> ls = new ArrayList<>();
    ls.add(23);
//    ls.add("text");
    try {
      Method method = ls.getClass().getDeclaredMethod("add",Object.class);
      
      
      method.invoke(ls,"test");
      method.invoke(ls,42.9f);
    } catch (NoSuchMethodException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } catch (SecurityException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } catch (IllegalAccessException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } catch (IllegalArgumentException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } catch (InvocationTargetException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
    
    for ( Object o: ls){
      System.out.println(o);
    }
  }
}

result:

23 
test
42.9

It can be seen that using the principle of type erasure and reflection can bypass the operation restrictions not allowed by the compiler in normal development.

Notable points in generics

Eight basic data types are not accepted in generic classes or generic methods.

You need to use their corresponding wrapper classes.

List<Integer> li = new ArrayList<>();
List<Boolean> li1 = new ArrayList<>();

Keywords: Java

Added by kvishnu_13 on Thu, 03 Feb 2022 14:25:55 +0200