Like attention, no more lost, your support means a lot to me!

🔥 Hi, I’m Chouchou. GitHub · Android-Notebook has been included in this article. Welcome to grow up with Chouchou Peng. (Contact information at GitHub)

preface

  • Generic types are the most difficult syntax in any language. They are painfully complex in detail and difficult to understand.
  • In this series, I will summarizeJava & KotlinGeneric knowledge points, take you fromGrammar & PrinciplesUnderstand generics thoroughly. Pursue simple and easy to understand without losing depth, if you can help, please be sure to like and follow!
  • First, try to answer these interview questions. After reading this article, you will be able to answer all of them:
Public class MyClass<T> {private T T 0; // 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. What problem do generics exist to solve? 3, please explain the principle of generics, what is the generic erase mechanism, how to implement it?Copy the code

Related articles

  • The Java | this is a comprehensive use of annotations strategy (including the Kotlin)”
  • The Java | reflection: access type information at run time (including the Kotlin)”
  • The Java | please outline the structure of the Class files”
  • The Java | deep understanding of the essence of the method call (including overloading and rewrite the distinction)”

directory


1. Generic base

  • Q: What are generics and what do they do?

A: When you define classes, interfaces, and methods, you can attach type parameters to make them generic classes, generic interfaces, and generic methods. There are three advantages to using generics over non-generic code: they are more robust (stronger type checking at compile time), more concise (strong breaks are eliminated, and strong breaks are automatically added after compile), and more generic (code can be applied to multiple types)

  • Q: What is the type erasure mechanism?

Answer: Generics are essentially a syntactic sugar in the Javac compiler because: Generics are a new feature introduced in JDK1.5. For downward compatibility, Java virtual machines and Class files do not provide support for generics. Instead, the compiler has to erase all generics information from Code properties.

  • Q: What are the specific steps for type erasure?

A: Type erasure occurs at compile time and is broken down into three steps:

  • 1: Erases all type parameter information, and replaces each parameter with its first boundary if the type parameter is bounded; If the type parameter is unbounded, replace it with Object
  • Insert type conversions (if necessary) to preserve type safety
  • 3 :(if necessary) generate bridge methods to preserve polymorphism in subclasses

Here’s an example:

Source:  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(); } -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- byte code:  public class Parent { public void func(Object t){ } } public class Child extends Parent { public Number get() { return null; Synthetic public void func(Object t){synthetic public void func(Object t){synthetic public void func(Object t){synthetic public void func((Number)t); } } void test() { Child<Integer> child = new Child(); // Insert cast Integer I = (Integer) child.get(); }Copy the code

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(); A cast was inserted

Step 3: Generate the bridge methods in Child. The bridge methods are generated by the compiler, so they carry the synthetic flag bit. Why should a bridge method be added to a subclass? Consider this question first: what if there were no bridge methods? You can see if the following code calls a subclass or superclass method:

Parent<Integer> child = new Child<>();
Parent<Integer> parent = new Parent<>();
        
child.func(1); // Parent#func(Object); 
parent.func(1); // Parent#func(Object); 
Copy the code

The two other code will be called to Parent# func (), if you have seen before I have written an article, not hard to you: “the Java | deep understanding of the essence of the method call (including overloading and rewrite the difference). Here I make a simple analysis:

1. The essence of a method call is to determine a direct reference to a method based on its symbolic reference.

2, this code calls the method symbol reference:

child.func(new Object()) => com/xurui/Child.func(Object)

parent.func(new Object()) => com/xurui/Parent.func(Object)

The bytecode instructions for these two method calls are invokevirtual

4. In the class loading and parsing stage, the class inheritance relationship is analyzed and the virtual method table of the class is generated

5, call phase (dynamic assignment) : Child does not overwrite func(Object), so Child’s virtual method table stores Parent#func(Object); Parent’s virtual method table stores Parent#func(Object);

As you can see, even if the object’s actual type is Child, the parent method is still called. So you lose polymorphism. That’s why you need to add bridge methods to generic subclasses.

  • Q: Why does a decompilation see the type parameter T after erasing?
If you decompile Parent. Class, you can see that T has been erased. public class Parent<T> { public Parent() { } public void func(T t) { } }Copy the code

A: The so-called type erasure in generics only erases the generic information in the Code attribute. The generic information is also retained in the class constant pool attribute (Signature attribute, LocalVariableTypeTable attribute), which is the fundamental basis for the reflection of the generic information at runtime. I said in verse 4.

  • Q: What are the effects of generic limitations & type erasure?

Due to type erasure, the actual type of the type argument is not known at run time. Generics are limited in their use to avoid situations where the results of a program run are inconsistent with the programmer’s semantics. The advantage is that type erasers do not create new classes for each parameterized type, so generics do not increase memory consumption.


2. Kotlin’s implementation type parameter

As mentioned earlier, the actual types of type arguments are not known at run time due to type erasure. For example, the following code is illegal because T is not really a type, but merely a symbol:

Java: <T> List<T> filter(List List) {List<T> result = new ArrayList<>(); Java: <T> List > 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: a 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 }Copy the code

In Kotlin, there is a way to overcome this limitation: inline functions with arguments of the implemented type:

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 }Copy the code

The key is inline and reified, which have the semantics:

  • Inline: The Kotlin compiler inserts the bytecode of an inline function at the place where the method is called
  • Reified: Replaces the type argument with the exact type of the type argument in the inserted bytecode

The rules are easy to understand, right? Obviously, when methods are inlined, the method body bytecode becomes:

Call:  val list = listOf("", 1, False) val strList = filter < String > (list) -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- after the inline:  val result = ArrayList<String>() for (e in list) { if (e is String) { result.add(e) } }Copy the code

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

Note that you cannot call an inline function with an argument of the implemented type from Java code

Another nice way to implement 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>() // The second way is simplerCopy the code

3. Variation: covariant & contravariant & constant

A Variant describes the relationship between different parameterized types of the same primitive type. Integer is a subtype of Number. List

is a subtype of List

?

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

  • Covariant: Subtype relationships are retained
  • Contravariant: The subtype relationship is reversed
  • Invariant: Subtype relations are destroyed

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

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

Arrays, by contrast, are covariant friendly:

Number[] nums; Integer[] ints = new Integer[10]; nums = ints; // OK covariant, subtype relationship is preservedCopy the code

So what do we do when we need to assign an object of type List

to a reference of type List

? At this point we need to qualify wildcards:

  • <? Extends > Upper bound wildcard

To support covariant type arguments, use upper bound wildcards, such as:

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

However, this introduces a compile-time restriction: you cannot call a method whose argument contains a type parameter E, nor can you set a field whose argument contains a type parameter. In short, this means that you can only access and cannot modify it (not strictly) :

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

l1.add(1); // compiler error
Copy the code
  • <? Super > lower bound wildcard

To allow inverting of type parameters, you need to use lower bound wildcards, such as:

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

Again, this introduces a compile-time constraint, but as opposed to covariance: you cannot call a method that returns a value as a type parameter, nor can you access a field of a type parameter. In short, you can only modify but cannot access (not strictly) :

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

Integer i = l1.get(0); // compiler error
Copy the code
  • <? > unbounded wildcard
List<? > l1; List<Integer> l2 = new ArrayList<>(); l1 = l2; // OKCopy the code

With that in mind, the question is easy to answer:

  • Q: List and List<? What’s the difference?

A: List is a native type that can add or access elements and has no compile-time security. List is actually an abbreviation of List, and is covariant (to introduce covariant features and limitations). Semantically, a List indicates that the consumer knows that the variable is type-safe, rather than inadvertently using the native List.

The design of generic code should follow PECS principles:

  • If you only need to get elements, use
  • If you only need to store, use

For example:

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

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

Covariant: val l0: MutableList<*> equals MutableList<out Any? > val l1: MutableList<out Number> val l2 = ArrayList<Int>() l0 = l2 // OK l1 = l2 // OK -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - inverter: val l1: MutableList<in Int> val l2 = ArrayList<Number>() l1 = l2 // OKCopy the code

In addition, Kotlin’s in & out can be used not only on type arguments, but also on type parameters of generic type declarations. This is a shorthand way of saying that the class designer knows that type parameters can only be covariant or contravariant across the class and avoids increasing them in every place they are used. For example, Kotlin’s List is designed to be an unmodifiable covariant:

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

Note: In Java, only point variants are supported, not declared point variants like Kotlin’s

To summarize:


4. Use reflection to get generic information

As mentioned earlier, type erasure occurs at compile time. The type information in the Code attribute is erased, but the generic information remains in the class constant pool attribute (Signature attribute, LocalVariableTypeTable attribute), so we can retrieve this information through reflection.

Get generic Type arguments: Use the Type system

4.1 Get generic classes & Generic interface declarations

TypeVariable ParameterizedType GenericArrayType WildcardType

Gson TypeToken

Editting….


5. To summarize

  • Takes an exam the advice
    • 1, Section 1 is very, very important, focusing on memory: the nature of generics and the design of the reason, the three steps of generic erasure, limitations and advantages, has summarized very essence, hope to help you;
    • 2. Focus on understanding the concept of Variant and the meaning of various qualifiers;
    • 3, Kotlin related parts, as knowledge accumulation and thinking expansion, not the focus of the test.

The resources

  • Kotlin In Action (Chapters 9, 10) — Dmitry Jemerov, Svetlana Isakova
  • Ideas for Java Programming (chapters 19, 20, 23). By Bruce Eckel
  • In Depth understanding the Java Virtual Machine (version 3) (Chapter 10). By Zhou Zhiming

Creation is not easy, your “three lian” is chouchou’s biggest motivation, we will see you next time!