Java | All you can ask about generics is here (including Kotlin)

Preface

  • Generic Type is the most difficult grammar in any language. The complexity of details and the difficulty of understanding are gnawing;
  • In this series, I will summarize Java & Kotlinthe generic knowledge with you from grammar & principles of a comprehensive understanding of generics. The pursuit is simple and easy to understand without losing depth. If you can help, please like and follow!
  • First of all, try to answer the questions that are easy to appear in these interviews. I believe that after reading this article, these questions will not trouble you:
1、下列代码中,编译出错的是:
public class MyClass<T> {
    private T t0; // 0
    private static T t1; // 1 
    private T func0(T t) { return t; } // 2
    private static T func1(T t) { return t; } // 3
    private static <T> T func2(T t) { return t; } // 4
}
2、泛型的存在是用来解决什么问题?
3、请说明泛型的原理,什么是泛型擦除机制,具体是怎样实现的?

 


table of Contents


1. Generic basics

  • Q: What is generic and what is it for?

Answer: When defining classes, interfaces, and methods, you can attach type parameters to make them generic classes, generic interfaces, and generic methods. Compared with non-generic code, the use of generics has three major advantages: more robust (stronger type checking at compile time), more concise (eliminate forced conversion, and automatically increase forced conversion after compilation), more general (code can be Suitable for many types)

  • Q: What is the type erasure mechanism?

A: is a Javac compiler on the generic nature of syntactic sugar , because: JDK1.5 Generics is the introduction of new features, in order to be backward compatible , Java Virtual Machine and Class document does not provide generic support , But let the compiler erase all generic information in the Code attribute. It should be noted that the generic information will remain in the attributes of the class constant pool.

  • Q: The specific steps for type erasure?

Answer: Type erasure occurs during compilation and is divided into the following 3 steps:

  • 1: Erase all type parameter information, if the type parameter is bounded, replace each parameter with its first boundary; if the type parameter is unbounded, replace it with Object
  • 2: (When necessary) insert type conversion to maintain type safety
  • 3: (If necessary) Generate bridge methods to preserve polymorphism in subclasses

for example:

源码:
public class Parent<T> {
    public void func(T t){
    }
}

public class Child<T extends Number> extends Parent<T> {
    public T get() {
        return null;
    }
    public void func(T t){
    }
}

void test(){
    Child<Integer> child = new Child<>();
    Integer i = child.get();
}
---------------------------------------------------------
字节码:
public class Parent {
    public void func(Object t){
    }
}

public class Child extends Parent {
    public Number get() {
        return null;
    }
    public void func(Number t) {
    }
    
    桥方法 - synthetic
    public void func(Object t){
        func((Number)t);
    }
}

void test() {
    Child<Integer> child = new Child();
    // 插入强制类型转换
    Integer i = (Integer) child.get();
}
  • Step 1: The type parameter T in Parent is erased to Object, and the type parameter T in Child is erased to Number;
  • Step 2: child.get(); inserts a forced type conversion
  • Step 3: Generate the bridge method in Child. The bridge method is generated by the compiler, so it will carry the synthetic flag. Why do you need to add bridge methods to subclasses? You can think about this first: What if there is no bridge method? You can see whether the following code calls a subclass or a parent method:
Parent<Integer> child = new Child<>();
Parent<Integer> parent = new Parent<>();
        
child.func(1); // Parent#func(Object); 
parent.func(1); // Parent#func(Object); 

Both lines of code will call Parent#func(). Here I briefly analyze:

1. The essence of method call is to determine the direct reference (entry address) of the method according to the symbolic reference of the method

2. The method symbol reference used by these two lines of code is:

     child.func(1) => com/xurui/Child.func(Object) parent.func(1) => com/xurui/Parent.func(Object)

3. The bytecode instruction of these two sentences method call is invokevirtual

4. Analyze the inheritance relationship of the class in the class loading analysis stage, and generate the virtual method table of the class

5. Invocation phase (dynamic dispatch): Child does not rewrite func(Object), so the virtual method table of Child stores Parent#func(Object); the virtual method table of Parent stores Parent#func(Object) ;

As you can see, even if the actual type of the object used is Child, the method of the parent class is still called here. This lost polymorphism. Therefore, it is necessary to add bridge methods to the generic subclass.

  • Question: Why does the decompilation still see the type parameter T after erasing?
反编译Parent.class,可以看到 T ,不是已经擦除了吗?

public class Parent<T> {
    public Parent() {
    }

    public void func(T t) {
    }
}

Answer: The so-called type erasure in generics is actually just Code 属性the generic information in erasure . The generic information is actually retained in the class constant pool properties (Signature property, LocalVariableTypeTable property), which can also be obtained by reflection at runtime The fundamental basis of generic information.

  • Q: What impact will the restriction of generics & type erasure have?

Due to the effect of type erasure, the actual type of the type argument is not clear at runtime. In order to avoid the situation where the program's running result is inconsistent with the programmer's semantics, there are some restrictions on the use of generics. The advantage is that type erasure will not create a new class for each parameterized type, so generics will not increase memory consumption.


2. Kotlin's actualized type parameters

As we mentioned earlier, due to the effect of type erasure, the actual type of the type argument is not clear at runtime. For example, the following code is illegal, because T is  not a real type, but just a symbol:

在这个函数里,我们传入一个List,企图从中过滤出 T 类型的元素:

Java:
<T> List<T> filter(List list) {
    List<T> result = new ArrayList<>();
    for (Object e : list) {
        if (e instanceof T) { // compiler error
            result.add(e);
        }
    }
    return result;
}
---------------------------------------------------
Kotlin:
fun <T> filter(list: List<*>): List<T> {
    val result = ArrayList<T>()
    for (e in list) {
        if (e is T) { // cannot check for instance of erased type: T
            result.add(e)
        }
    }
    return result
}

In Kotlin, there is a way to break through this limitation, namely: inline functions with actualized type parameters :

Kotlin:
inline fun <reified T> filter(list: List<*>): List<T> {
    val result = ArrayList<T>()
    for (e in list) {
        if (e is T) {
            result.add(e)
        }
    }
    return result
}

The key is inlineand reified, the semantics of the two are:

  • inline(Inline function): The Kotlin compiler inserts the bytecode of the inline function into every call to the method
  • reified(Realized type parameter): In the inserted bytecode, use the exact type of the type argument instead of the type argument

The rules are easy to understand, right. Obviously, when method inlining occurs, the method body bytecode becomes:

调用:
val list = listOf("", 1, false)
val strList = filter<String>(list)
---------------------------------------------------
内联后:
val result = ArrayList<String>()
for (e in list) {
    if (e is String) {
        result.add(e)
    }
}

It should be noted that the bytecode of the entire method body of the inline function will be inserted at the call location, thus controlling the size of the inline function body. If the function body is too large, Tthe code that does not depend on should be extracted into a separate non-inline function.

Note that you cannot call inline functions with actualized type parameters from Java code

Another magical effect of materialized type parameters is to replace Class object references , for example:

fun Context.startActivity(clazz: Class<*>) {
    Intent(this, clazz).apply {
        startActivity(this)
    }
}

inline fun <reified T> Context.startActivity() {
    Intent(this, T::class.java).apply {
        startActivity(this)
    }
}

调用方:
context.startActivity(MainActivity::class.java)
context.startActivity<MainActivity>() // 第二种方式会简化一些

3. Variation: Covariance & Contrary & Unchanged

Variant describes the relationship between different parameterized types of the same primitive type . It's a bit convoluted to say, but in fact it means: Integeryes Numbersubtype, ask you if it List<Integer>is List<Number>a subtype?

There are three types of variants: covariant & contravariant & invariant

  • Covariant (covariant): subtype relationship is preserved
  • Contravariant: The subtype relationship is reversed
  • Invariant (invariant): the subtype relationship is eliminated

In Java, type parameters are invariant by default , for example:

List<Number> l1;
List<Integer> l2 = new ArrayList<>();
l1 = l2; // compiler error

In contrast, arrays support covariance:

Number[] nums;
Integer[] ints = new Integer[10]; 
nums = ints; // OK 协变,子类型关系被保留

So, what should we do when we need to assign List<Integer>an object of a List<Number>type to a reference of a type? At this time we need to qualify wildcards :

  • <? extends> upper bound wildcard

To support covariance for type parameters, you need to use upper bound wildcards, for example:

List<? extends Number> l1;
List<Integer> l2 = new ArrayList<>();
l1 = l2; // OK

But this will introduce a compile-time restriction: methods whose parameters contain type parameter E cannot be called, and the field of type parameter cannot be set. Simply put, it can only be accessed and cannot be modified (non-strict):

// ArrayList.java
public boolean add(E e) {
    ...
}

l1.add(1); // compiler error
  • <? super> Lower bound wildcard

To support contravariance for type parameters, lower bound wildcards are required, for example:

List<? super Integer> l1;
List<Number> l2 = new ArrayList<>();
l1 = l2; // OK

Similarly, this also introduces a compile-time restriction, but it is the opposite of covariance: you cannot call methods that return type parameters, and you cannot access the fields of type parameters. Simply put, you can only modify and not access (non-strict):

// ArrayList.java
public E get(int index) {
    ...
}

Integer i = l1.get(0); // compiler error
  • <?> Unbounded wildcard

In fact, it is very simple, and many materials are actually too complicated to explain. <?> is actually an abbreviation. E.g:

List<?> l1;
List<Integer> l2 = new ArrayList<>();
l1 = l2; // OK

Understand this point, this question is easy to answer:

  • Q: What is the difference between List and List<?>?

Answer: List is a primitive type, you can add or access elements, it does not have compile-time safety, and List is actually the abbreviation of List, which is covariant (it can lead to the characteristics and limitations of covariance); semantically, List indicates use It is clear that variables are type-safe, and not because of negligence and use the primitive type List.

The design of generic code should follow the PECS principle (Producer extends Consumer super):

  • If you only need to get the element, use <? extends T>
  • If you only need storage, use <? super T>

For example:

// Collections.java public static void copy(List<? super T> dest, List<? extends T> src) { }

In Kotlin, variants are written differently, but the semantics are exactly the same:

协变:
val l0: MutableList<*> 相当于MutableList<out Any?>
val l1: MutableList<out Number>
val l2 = ArrayList<Int>()
l0 = l2 // OK
l1 = l2 // OK
---------------------------------------------------
逆变:
val l1: MutableList<in Int>
val l2 = ArrayList<Number>()
l1 = l2 // OK

In addition, Kotlin in & outcan be used not only on type arguments, but also on type parameters declared by generic types. In fact, this is a simple way of writing, which means that the class designer knows that the type parameters can only be covariant or contravariant in the entire class, and avoid adding them in each place of use. For example, Kotlin's Listis designed as an unmodifiable covariant:

public interface List<out E> : Collection<E> {
    ...
}

Note: In Java, only use point variants are supported, and Kotlin-like declaration point variants are not supported

To summarize:


4. Use reflection to get generic information

As mentioned earlier, type erasure will be carried out during compile time, and the type information in the Code attribute will be erased, but generic information is still retained in the class constant pool attributes (Signature attribute, LocalVariableTypeTable attribute), so we can use reflection to Get this information.

Obtain generic type arguments: need to use the Type system

4.1 Obtaining generic class & generic interface declaration

TypeVariable ParameterizedType GenericArrayType WildcardType

Gson TypeToken

Editing ....


5. Summary

  • Suggestions
    • 1. The first section is very very important, focusing on memory: the essence and design reason of generics, the three steps, limitations and advantages of generic erasure have been summarized very well, and I hope to help you;
    • 2. Focus on understanding the concept of Variant and the meaning of various qualifiers;
    • 3. Kotlin-related parts are mainly for knowledge accumulation and thinking expansion, not the focus of examination.

Reference materials:

    Get information for free: Click here to get it for free, password: CSDN

The readers don’t pay attention, eh, I’m not panicking at all, he just let him go, talk to them first, oh, got sprayed, spray him? If it doesn't exist, I backhanded it and apologized to him sincerely, depending on what the reader said, eh, this is very comfortable. The comment area is going to blow up It’s not a big problem. I’ll like every one of them first, and I’ll get a wave from the edge. What do you think in the comment area. Oh, this man, iron-headed baby, is very spiritual and seems to be really going to explode. My scalp is numb, but I am not afraid. At this time, I have to think backwards and give him a copy of learning materials for free . This wave is not bad, what do you think? Dignified and nice bloggers are afraid of your comment area? I'm in a good mood, I don't know anything

Free receipt of information: Click here to receive it for free, password: CSDN

 

Guess you like

Origin blog.csdn.net/qq_43080036/article/details/109359754