In depth analysis of java reflection (III) – generics

premise

The java reflection API is in javase 1 7, but Oracle jdk11 was used when writing this article, because jdk11 also uploaded the source code under the sun package. You can directly view the corresponding source code and debug through the IDE.

This paper mainly introduces a difficult problem in reflection - generics.

Introduction to generics

Generics is a generic programming tool added to the Java programming language in javase 5.0 (JDK1.5) in 2004. Generics are designed to be applied in the Java type system to provide "a type or method to operate on objects of various types while providing compile time type safety". However, some studies in 2016 showed that generics can not guarantee compile time type safety in all cases. For example, compile time type safety of aspect programming has not been fully implemented.

One of the biggest advantages of generics is that it provides compile time type safety. For example, before introducing generics, only one object array reference was maintained in ArrayList. This approach has two problems:

After introducing generics, we can explicitly define ArrayList through type parameters:

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

// JavaSE 7以后的版本中构造函数可以省略类型,编译器可以推导出实际类型
ArrayList<String> list = new ArrayList<>();

Here are some facts about generics in Java:

Understanding type erasure

What is the type of erasure

The specific manifestation of type erasure (or more often referred to as "generic erasure") is that whenever a generic type is defined, a corresponding original type (raw type, where the original type does not refer to basic data types such as int and Boolean) is automatically provided. The class name of the original type is the type name of the class with generic parameters after deleting the generic parameters, For example, pair < T > types with generic parameters are as follows:

public class Pair<T>{

    private T first;
    private T second;

    public Pair(T first,T second){
        this.first = first;
        this.second = second;
    }

    public T getFirst(){
        return first;
    }

    public T getSecond(){
        return second;
    }
}

The original type of pair < T > after erasing the type is:

public class Pair{

    private Object first;
    private Object second;

    public Pair(Object first,Object second){
        this.first = first;
        this.second = second;
    }

    public Object getFirst(){
        return first;
    }

    public Object getSecond(){
        return second;
    }
}

For a more complex example, if the generic parameter type has an upper limit, the variable will be erased as the upper limit type:

public class Interval<T extends Comparable & Serializable> implements Serializable {

	private T lower;
	private T upper;

	public Interval(T lower,T upper) {
		this.lower = lower;
		this.upper = upper;
	}

	//省略其他方法
}

Interval after type erasure < T extends comparable & serializable > original type:

public class Interval implements Serializable {

	private Comparable lower;
	private Comparable upper;

	public Interval(Comparable lower,Comparable upper) {
		this.lower = lower;
		this.upper = upper;
	}

	//省略其他方法
}

For types with multiple Generic upper limits like the above, try to put the identification interface upper limit type at the end of the boundary list, which can improve efficiency.

Why do I need an erase type

At jdk1 Before 5, that is, before the emergence of generics, all types, including basic data types (int, byte, etc.), wrapper types, and other user-defined types, can use Java corresponding to the bytecode of class file (. Class) Lang. class description, that is, Java A concrete instance object of lang. class class can represent the original type of any specified type. Here, all types before the emergence of generics are temporarily called "historical primitive types".

At jdk1 After 5, the data type has been expanded to the historical original type, and four generic types have been expanded: parameterized type, type variable type, wildcard type and generic array type. Historical primitive types and newly extended generic types should be unified into their own bytecode file type objects, that is, generic types should be merged into Java Lang. class. However, since JDK has iterated many versions, generics are not a basic component in current Java. If real generic types are introduced into the JVM, The modification of JVM instruction set and bytecode file must be involved (this modification is certainly not a small modification, because JDK has been iterated for many years at that time, and type is a very basic feature of programming language. From the perspective of project function iteration, the introduction of generic type may require regression test for the whole JVM project). The cost of this function is very huge, Therefore, Java does not introduce generics at the Java virtual machine level.

In order to use generics, Java uses the mechanism of type erasure to introduce the "use of generics", which does not really introduce and implement generics. Generics in Java implement type safety at compile time, that is, the type safety check of generics is implemented by the compiler (commonly javac) at compile time, which can ensure the type based security of data and avoid the trouble of forced type conversion (in fact, forced type conversion is completed by the compiler, but it does not need to be completed manually). Once the compilation is completed, all generic types will be erased. If no upper limit is specified, they will be erased as object type, otherwise they will be erased as upper limit type.

Since there is no generics in the Java virtual machine, why can we get generics information from some class libraries in the JDK? This is because the class file (. Class) or bytecode file itself stores generic information. When reading generic information from related class libraries (JDK class libraries or third-party class libraries), it can be extracted from bytecode files. For example, the commonly used bytecode operation class library ASM can read the information in bytecode and even transform bytecode to dynamically generate classes. For example, for the interval < T extends comparable & serializable > class mentioned earlier, use the javap - C - V command to view the bytecode information obtained from its decompilation. You can see its signature as follows:

Signature: #22                          // <T::Ljava/lang/Comparable;:Ljava/io/Serializable;>Ljava/lang/Object;Ljava/io/Serializable;

The signature information here is actually saved in the constant pool. There will be a series of articles on the parsing of bytecode files in the future.

Type system

Previously mentioned in jdk1 5 introduces four new generic types Java lang.reflect. ParameterizedType、java. lang.reflect. TypeVariable、java. lang.reflect. WildcardType、java. lang.reflect. Genericarraytype, including the existing Java Lang. class, there are five types. For the expansibility of the program, Java lang.reflect. The type class serves as the common parent interface of these five types, so that the child can use Java lang.reflect. The type parameter receives the arguments or return values of the above five subtypes, thus logically unifying the generic related types and the original Java Type described by lang.class. The type system is as follows:

be careful:

Although the type system seems beautiful, it solves the generic related types and the original Java Lang. class describes the unification of types, but introduces a new problem: if the return value of a method is Java lang.reflect. Type, or the input parameter type of a method is Java lang.reflect. Type. In both cases, you may need to lang.reflect. The subtype of an object of type type is judged because its subtype may be one of the five types mentioned above, which increases the complexity of coding.

ParameterizedType

Parameterized type, parameterized type, that is, parameterized type. In the comment, it is said that parameterized type represents a parameterized type, such as collection < string >. In fact, all parameters or attributes with parameterized (generic) label < classname > belong to parameterized type. For example, the following types are parameterizedtype:

Set<String> set;
Class<Integer> clazz;
MyClass<String> myClass;
List<String> list;

class MyClass<V>{

}

The following wrapper classes that ignore generic parameters or basic data types and basic data types are not parametrizedtype:

String name = "throwbale";
int age = 25;
Set set;
List list;

public String method(int age,String name){

}

java. lang.reflect. Parameterizedtype interface inherits from Java lang.reflect. Type interface, and the implementation class is sun reflect. generics. reflectiveObjects. Parameterizedtypeimpl. In fact, we can implement parameterizedtype by ourselves when necessary. For example, some JSON parsing tools implement parameterizedtype by themselves. The parameterizedtype interface has the following methods:

public interface ParameterizedType extends Type {

    Type[] getActualTypeArguments();

    Type getRawType();

    Type getOwnerType();
} 

Take a simple example of parameterizedtype:

public class Main13 {

    public static void main(String[] args) throws Exception {
        Class<Sub> subClass = Sub.class;
        Type genericSuperclass = subClass.getGenericSuperclass();
        if (genericSuperclass instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
            //获取父类泛型类型数组
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
            for (Type type : actualTypeArguments) {
                System.out.println(type + " is ParameterizedType -> " + (type instanceof ParameterizedType));
            }
        }
        Field field = subClass.getDeclaredField("clazz");
        Type genericType = field.getGenericType();
        System.out.println(genericType + " is ParameterizedType -> " + (genericType instanceof ParameterizedType));
    }

    public static class Person {

    }

    public static abstract class Supper<T,E> {

    }

    public static class Sub extends Supper<String,List<Person>> {

    }
}

Output results:

class java.lang.String is ParameterizedType -> false
java.util.List<org.throwable.inherited.Main13$Person> is ParameterizedType -> true
java.lang.Class<?> is ParameterizedType -> true

TypeVariable

TypeVariable, type variable, that is, type variable, is the public parent interface of various type variables. It is mainly used to represent the information of generic parameters with upper bounds. The difference between it and parametrized type is that the outermost layer of parameters represented by parametrized type must be known specific types (such as list < string >), while TypeVariable faces K, V The literal representation of these generic parameters such as E. The common representation of typevariables is < T extends knowntype-1 & knowntype-2 >. The source code of TypeVariable interface is as follows:

public interface TypeVariable<D extends GenericDeclaration> extends Type {
   //获得泛型的上限,若未明确声明上边界则默认为Object
    Type[] getBounds();
    //获取声明该类型变量实体(即获得类、方法或构造器名)
    D getGenericDeclaration();
    //获得名称,即K、V、E之类名称
    String getName();
    //获得注解类型的上限,若未明确声明上边界则默认为长度为0的数组
    AnnotatedType[] getAnnotatedBounds()
}

Take a simple example of TypeVariable:

public class Main14 {

    public static void main(String[] args) throws Exception {
        Class<Supper> subClass = Supper.class;
        TypeVariable<Class<Supper>>[] typeParameters = subClass.getTypeParameters();
        for (TypeVariable<Class<Supper>> typeVariable : typeParameters) {
            System.out.println("getBounds --> " + Arrays.toString(typeVariable.getBounds()));
            System.out.println("getGenericDeclaration  --> " + typeVariable.getGenericDeclaration());
            System.out.println("getName --> " + typeVariable.getName());
            AnnotatedType[] annotatedBounds = typeVariable.getAnnotatedBounds();
            StringBuilder stringBuilder = new StringBuilder("getAnnotatedBounds --> ");
            for (AnnotatedType annotatedType : annotatedBounds) {
                java.lang.annotation.Annotation[] annotations = annotatedType.getAnnotations();
                for (java.lang.annotation.Annotation annotation : annotations) {
                    stringBuilder.append(annotation).append(",");
                }
            }
            System.out.println(stringBuilder.toString());
            System.out.println("===================");
        }
    }

    @Target(ElementType.TYPE)
    public @interface Annotation {

    }

    interface InterFace {

    }

    public static class Person {

    }

    public static abstract class Supper<T extends Person & InterFace,E extends Annotation> {

    }
}

Output results:

getBounds --> [class org.throwable.inherited.Main14$Person,interface org.throwable.inherited.Main14$InterFace]
getGenericDeclaration  --> class org.throwable.inherited.Main14$Supper
getName --> T
getAnnotatedBounds -->
===================
getBounds --> [interface org.throwable.inherited.Main14$Annotation]
getGenericDeclaration  --> class org.throwable.inherited.Main14$Supper
getName --> E
getAnnotatedBounds -->
===================

WildcardType

Wildcardtype is used to represent wildcard (?) Generic arguments to expressions of type, such as etc. According to the comment of wildcardtype: at present, wildcard expressions only accept one upper boundary or lower boundary, which is different from specifying multiple upper boundaries when defining type variables. However, in order to maintain scalability, the return value type is written in the form of array. In fact, the size of the returned array is 1. The wildcardtype interface source code is as follows:

public interface WildcardType extends Type {

    Type[] getUpperBounds();

    Type[] getLowerBounds();
}

Take a simple example of wildcardtype:

public class Main16 {

    public static void main(String[] args) {
        Class<Main16> clazz = Main16.class;
        Method[] methods = clazz.getmethods();
        for (Method method : methods) {
            if ("print".equals(method.getName())) {
                Type[] genericParameterTypes = method.getGenericParameterTypes();
                for (Type type : genericParameterTypes) {
                    if (type instanceof ParameterizedType) {
                        ParameterizedType parameterizedType = (ParameterizedType) type;
                        Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
                        for (Type actualType : actualTypeArguments) {
                            if (actualType instanceof WildcardType) {
                                WildcardType wildcardType = (WildcardType) actualType;
                                System.out.println("WildcardType --> " + wildcardType + " getUpperBounds--> "
                                        + Arrays.toString(wildcardType.getUpperBounds()) + " getLowerBounds--> " + Arrays.toString(wildcardType.getLowerBounds()));
                            } else {
                                System.out.println("Not WildcardType --> " + actualType);
                            }
                        }

                    }
                }
            }
        }
    }

    interface Person {

    }

    public static void print(List<? extends Number> list,Set<? super Person> persons) {

    }
}

Output results:

WildcardType --> ? extends java.lang.Number getUpperBounds--> [class java.lang.Number] getLowerBounds--> []
WildcardType --> ? super org.throwable.inherited.Main16$Person getUpperBounds--> [class java.lang.Object] getLowerBounds--> [interface org.throwable.inherited.Main16$Person]

Note here is list list as a whole is of parametrizedtype type. After stripping the list for the first time, it is called "list"? Extensions number is the wildcardtype type.

GenericArrayType

Genericarraytype, generic array type, that is, generic arrays, that is, arrays with generic element types, implements this interface. It requires that the type of the element is parameterizedtype or TypeVariable (it is also allowed to find that the element is genericarraytype in practice). for instance:

List<String>[] listArray; //是GenericArrayType,元素是List<String>类型,也就是ParameterizedType类型
T[] tArray; //是GenericArrayType,元素是T类型,也就是TypeVariable类型

Person[] persons; //不是GenericArrayType
List<String> strings; //不是GenericArrayType

The source code of the genericarraytype interface is as follows:

public interface GenericArrayType extends Type {

    Type getGenericComponentType();
}

Take a simple example of genericarraytype:

public class Main15<T> {


    public static void main(String[] args) throws Exception {
        Method[] methods = Main15.class.getmethods();
        for (Method method : methods) {
            if ("method".equals(method.getName())) {
                Type[] genericParameterTypes = method.getGenericParameterTypes();
                for (Type type : genericParameterTypes) {
                    if (type instanceof GenericArrayType) {
                        System.out.println("GenericArrayType --> " + type + " getGenericComponentType --> "
                                + ((GenericArrayType) type).getGenericComponentType());
                    } else {
                        System.out.println("Not GenericArrayType --> " + type);
                    }
                }
            }
        }
    }

    public static <T> void method(String[] strings,List<String> ls,List<String>[] lsa,T[] ts,List<T>[] tla,T[][] tts) {

    }
}

Output results:

Not GenericArrayType --> class [Ljava.lang.String;
Not GenericArrayType --> java.util.List<java.lang.String>
GenericArrayType --> java.util.List<java.lang.String>[] getGenericComponentType --> java.util.List<java.lang.String>
GenericArrayType --> T[] getGenericComponentType --> T
GenericArrayType --> java.util.List<T>[] getGenericComponentType --> java.util.List<T>
GenericArrayType --> T[][] getGenericComponentType --> T[]

Here is an analysis:

Generic constraints

There are some limitations to consider when using java generics, most of which are caused by generic type erasure.

//下面的两种做法是错误的
if(a instanceof Pair<String>) //Error

if(a instanceof Pair<T>)  //Error

// 正确做法
if(a instanceof Pair)  //Right
// 反例
public static <T extends Throwable> void doWork(Class<T> t) {
    try{

    }catch(T t){  //Error

    }
}

// 正例
public static <T extends Throwable> void doWork(T t) throws T{
    try{

    }catch(Throwable e){  
       throw e;
    }
}
// 不加此注解会收到编译器的警告
@SuppressWarnings("unchecked")
public static <T extends Throwable> void throwAs(Throwable e){
    throw (T) e;
}

In fact, there are generic inheritance rules and wildcard rules (you can see the subtypes of type described earlier) and so on, which will not be expanded in detail here.

On the problem of generic arrays

In Java generic constraints, parameterized type arrays cannot be instantiated, such as pair < integer > [] Table = new pair < integer > [10]; It's illegal. The root cause is that erasure of generic types and arrays record the properties of element types. For example, suppose you can instantiate a parameterized type array:

Pair<String>[] table = new Pair<String>[10];

After the generic erasure of the above parameterized array, the type of the array instance table is pair [] and the type of the array element is pair, which can be forcibly converted to an object [] type array:

Object[] objArray = table;

Based on generic erasure, array objarray can arbitrarily assign generic instances of pair < anytype >, for example:

objArray[0] = new Pair<Integer>();
objArray[1] = new Pair<Long>();
objArray[2] = new Pair<String>();
....

In this way, the array storage elements can be checked, and ClassCastException will appear at any time in subsequent operations. For the above reasons, Java directly refuses to create parameterized type arrays from the compilation level.

In addition, the instantiation of type variable array is also illegal, such as t [] TT = new t [10];, This is because type variables are only literal quantities at compile time, which are not related to Java's type system.

But one thing to note: parameterized type arrays and type variable arrays can be used as method input parameter variables or class member variables. For example, the following practices are legal:

public class Pair<T> {

	private Pair<T>[] attr;
	private T[] ts;

	public static <T> void method(Pair<T> pair) {

	}

	public static <T> void method(T[] ts) {

	}
}

Finally, you can see from the previous article that you can actually use reflection to create generic arrays.

Infinite wildcard

Infinite wildcards are supported in generics , Instances that use infinite wildcard types have the following limitations:

The indefinite wildcard type can be regarded as a shadow type of the original type. It shields the set value operation except null. All methods for obtaining values can only return object type results. This feature makes it very convenient to carry out some simple operations through the indefinite wildcard type, such as:

public static boolean hasNulls(Pair<?> p){
    return p.getFirst() == null || p.getSecond() == null;
}

If you are familiar with reflection, Java Lang. class has a similar usage:

Class<?> clazz = ...;
Object instance = class.newInstance();

Bridge method

First, let's explain what bridge method is. Look at the following code:

// 父类
public interface Supper<T> {

    void method(T t);
}

// 其中一个子类
public class Sub implements Supper<Integer> {

	@Override
	public void method(Integer value) {
		System.out.println(value);
	}
}

The original type of the parent supplier < T > after the generic type is erased is:

public interface Supper{

    void method(Object t);
}

Although the subclass sub implements the parent class supplier, it only implements void method (integer value) instead of void method (object T) in the parent class. At this time, the compiler at compile time will create this method for the subclass sub, that is, the subclass sub will become like this:

public class Sub implements Supper<Integer> {

	@Override
	public void method(Integer value) {
		System.out.println(value);
	}
		
	public void method(Object value) {
		this.method((Integer) value);
	}
}

If you write a subclass like this directly, sub will compile and report an error, and the void method (object value) method generated by the compiler above is the bridge method. You can verify with reflection:

public static void main(String[] args) throws Exception {
	Method[] declaredMethods = Sub.class.getDeclaredMethods();
	List<Method> methods = new ArrayList<>();
	for (Method method : declaredMethods) {
		if (method.getName().equals("method")) {
			methods.add(method);
		}
	}
	for (Method method : methods) {
		System.out.println(String.format("name=%s,paramTypes=%s,isBridge=%s",method.getName(),Arrays.toString(method.getParameterTypes()),method.isBridge()));
	}
}

//输出结果
name=method,paramTypes=[class java.lang.Integer],isBridge=false
name=method,paramTypes=[class java.lang.Object],isBridge=true

The definition of bridge method is vague, so only its occurrence is considered here, and no blind definition is made. Not only the subclass implementing the parent class with generic parameters will generate bridge methods, but also a more common case is to specify a more "strict" return value type when the method is overridden. For example:

public Employee implements Cloneable{

    public Employee clone() throws CloneNotSupportedException{
        //...
    }
}

// 这里实际上,Employee覆盖了Object的clone()方法,因此实际上编译后Employee如下
public Employee implements Cloneable{

    public Employee clone() throws CloneNotSupportedException{
        //...
    }

    // 这个是桥方法
    public Object clone() throws CloneNotSupportedException{
        //...
    }    
}

This is because:

Look carefully. In fact, both cases are caused by inheritance.

API for manipulating generics in JDK

Here are some APIs related to operation generics known to the author in the JDK (there may be omissions). These APIs are mainly related to reflection operations:

java. Related methods in lang.class:

java. lang.reflect. Related methods in constructor:

java. lang.reflect. Related methods in method:

java. lang.reflect. Related methods in field:

If the return value obtained by using the above method is different from the expected return value, please deepen your understanding of generic type erasure.

Summary

reference material:

In my opinion, generics are actually the product of compromise and compatibility history in the JDK iteration process. It is an unrealized generics. Of course, providing compile time type safety can enable developers to avoid human errors in type conversion, that is, generics in Java can improve the readability and security of programs or codes, which is its biggest advantage.

Personal blog

(e-a-20181205)

The official account of Technology (Throwable Digest), which is not regularly pushed to the original technical article (never copied or copied):

Entertainment official account ("sand sculpture"), select interesting sand sculptures, videos and videos, push them to relieve life and work stress.

The content of this article comes from the network collection of netizens. It is used as a learning reference. The copyright belongs to the original author.
THE END
分享
二维码
< <上一篇
下一篇>>