Generic produce

Java generics, a new feature introduced in JDK1.5, are parameterized types. A parameterized type is a type that controls parameter limits through the generic type specified by the generic without creating a new type. Allows detection of illegal types at compile time.

The generic characteristics

  • Type safety. Using parameters defined by generics, you can validate a type at compile time, exposing problems more quickly
  • Eliminate casts.
  • Avoid unnecessary packing, unpacking operation, improve program performance
  • Improve code reuse

Named type parameter

  • The e-element, used primarily by the Java Collections framework.
  • The K – key is used to indicate the parameter type of the key in the mapping.
  • V-value, mainly used to represent the parameter type of the value in the mapping.
  • N – indicates a number.
  • The T – type is mainly used to represent the first type of generic parameter.
  • The S – type is mainly used to represent the second type of generic type parameter.
  • The U – type is mainly used to represent the third type of general type parameters.
  • The V – type is used primarily to represent the fourth generic type parameter.

The generic definition

A generic class

The declaration of a generic class is similar to the normal class declaration, except that the type parameter declaration is added after the class name. define

The modifierclassClass name < Declare custom generics >{... }Copy the code

The instance

public static void main(String[] args) {
    Container<String ,String> c1 = new Container<>("name"."kevin");
    Container<String, Integer> c2 = new Container<>("age".29);
    Container<Double, Integer> c3 = new Container<>(1.0.29);
}
public static class Container<K.V> { K key; V value; Container(K k, V v) { key = k; value = v; }}Copy the code

A generic interface

define

The modifierinterfaceInterface name < Declare custom generics >{}Copy the code

The instance

public interface Generator<T> {
    T init(a);
}

public class GeneratorClass1 implements Generator<String> {
    @Override
    public String init(a) {
        return "test1"; }}public class GeneratorClass2 implements Generator<Integer> {
    @Override
    public Integer init(a) {
        return 0; }}Copy the code

Generic method

define

Modifier Generic type T method name (parameter){}Copy the code

The instance

/** * A basic introduction to generic methods *@paramThe generic argument * passed in by tClass@return<T> <T> <T> <T> <T> * 2) Only methods that declare <T> are generic, and member methods that use generics in a generic class are not generic. * 3) <T> indicates that the method will use the generic type T before you can use the generic type T in the method. * 4) Like the definition of generic classes, here T can be written as any identifier, common parameters such as T, E, K, V are often used to represent generics. * /
public <T> T genericMethod(Class<T> tClass){
        T instance = tClass.newInstance();
        return instance;
}
Copy the code

The wildcard

Wildcard generation

Any place where a parent class is used can be replaced by a subclass of that parent class. We often encounter the interior substitution principle when using classes and objects, and the same principle applies to arrays:

  class Fruit {}
    class Apple extends Fruit {}
    class Jonathan extends Apple {}
    class Orange extends Fruit {}

    public class CovariantArrays {
        public  void main(String[] args) {
            Fruit[] fruit = new Apple[10]; // OK
            List<Fruit> fruits=new ArrayList<Apple>();//error
            fruit[0] = new Apple(); // OK
            fruit[1] = new Jonathan(); // OK
            // Runtime type is Apple[], not Fruit[] or Orange[]:
            try {
                // Compiler allows you to add Fruit:
                fruit[0] = new Fruit(); // ArrayStoreException
            } catch(Exception e) { System.out.println(e); }
            try {
                // Compiler allows you to add Oranges:
                fruit[0] = new Orange(); // ArrayStoreException
            } catch(Exception e) { System.out.println(e); }}}Copy the code

This upward transformation in an array is called array covariance and is not supported by generics, as shown in the following code

 List<Fruit> fruits=new ArrayList<Apple>();//error
Copy the code

The code above produces a compile-time error, which is designed because arrays support runtime checking and collections do not.

This feature of Java generics is not useful when there is a need for upward transformation, so Java has designed wildcards to meet this need.

Upper bound bound wildcard [Java]/ covariant [Kotln]

Java

The Java language leverages
make an upward transition to generics:

 static void ccc(a) {
    Apple apple = new Apple();
    apple.name = "Apple";
    ArrayList<? extends Fruit> fruits = new ArrayList<>();
    fruits.add(apple); //Error

    for (int i = 0; i < fruits.size(); i++) {
        Fruit fruit = (Fruit) fruits.get(i);
        System.out.println("println---"+ fruit.name); }}Copy the code

Kotlin

The Kotlin language uses wildcards of the form

to make an upward transition to generics:

 fun ccc(a) {
    val apple = Apple()
    apple.name = "Apple"
    val fruits: ArrayList<out Fruit> = ArrayList()
    fruits.add(apple) //Error
    for (i in fruits.indices) {
        println("println---" + fruits[i].name)
    }
}
Copy the code

In order to ensure the security of runtime, the compiler will limit the operation of writing and open reading to it. Because the compiler can only guarantee that fruits and its subclasses exist in the FRUITS collection, it does not know the specific type, so the above code fruits.add(apple) will report an error

Lower bound bound wildcard [Java]/ contravariant [Kotln]

Java

The Java language leverages
can be used to make an upward transition to generics:

static void ccc(a) {
    Apple apple = new Apple();
    apple.name = "Apple";
    ArrayList<? super Apple> fruits = new ArrayList<>();
    fruits.add(apple);//OK
    fruits.add(new Fruits()); //Error
    for (int i = 0; i < fruits.size(); i++) {
        Apple fruit = fruits.get(i);//Error
        System.out.println("println---"+ fruit.name); }}Copy the code

Kotlin

The Kotlin language uses wildcards of the form

to make an upward transition to generics:

fun ccc(a) {
    val apple = Apple()
    apple.name = "Apple"
    val fruits: ArrayList<in Apple> = ArrayList()
    fruits.add(apple) //OK
    fruits.add(new Fruits()) //Error
    for (i in fruits.indices) {
        println("println---" + fruits[i].name) //error}}Copy the code

In contrast to the top wildcard, the bottom wildcard usually qualifies read operations, opens write operations, and, for code like this, identifies a List of some type, which is Apple’s base type. That is, we don’t actually know what the type is, but it must be Apple’s parent type. Therefore, we know that it is safe to add an Apple object or its subtype to the List, which can all be cast up to Apple. But we don’t know if it’s safe to add Fruit objects,

Borderless wildcard [Java]/ star projection [Kotln]

Another wildcard is the borderless wildcard, which takes the form of a single question mark: List<? >, that is, there is no qualification

Java

<? >Copy the code

kotlin

< * >Copy the code

Unbounded wildcards or star projections are not qualified, and because they are not qualified, we cannot be sure what type the parameter is, and we cannot add objects to them at this point.

MutableList
and MutableList?

The following code

Java

List<? > list1 =new ArrayList<>();
  aaa.add(""); //Error

  List list2 = new ArrayList();
  aaa1.add(""); //Ok
Copy the code

Kotlin


  val list1: MutableList<*> = mutableListOf<Any>()
  fruits.add(Fruit()) //Error

  val list2: MutableList<Any> = mutableListOf<Any>()
  fruits.add(Fruit()) //OK

Copy the code

MutableList<*> list indicates that the list is a MutableList of a specific type, but it is not known what type. So can we add objects to it? Of course not, because it’s not safe to add any type because you don’t know what the actual type is. The MutableList list, which has no generic arguments, means that the list holds elements of type Any, Object, so you can add objects of Any type, but the compiler will warn you.

Generic parameter constraints

Single generic parameter constraint [Java]/[Kotln]

A single generic constraint is simple. In Java we use extends, and in Kotlin we use:

We want to implement a comparison of two generic parameters using Comparable as follows:

Java

public <T extends Comparable<T>> T maxOf(T params1, T params2) {
    if (params1.compareTo(params2) > 0) {
        return params1;
    } else {
        returnparams2; }}Copy the code

Kotlin

fun <T: Comparable<T>> maxOf(params1: T, params2: T): T {
    return if (params1 > params2) {
        params1
    } else{
        params2
    }
}
Copy the code

We’ve looked at single constraints before, but what if we want to implement multiple constraints?

Multiple generic parameter constraints [Java]/[Kotlin]

Java implements multiple generic parameters using & Supplier, whereas Kotlin uses WHERE

Or the code above, let’s change:

Java

<T extends Comparable<T> & Supplier<R>, R extends String> R maxOf(T params1, T params2) {
    if (params1.compareTo(params2) > 0) {
        return params1.get();
    } else {
        returnparams2.get(); }}Copy the code

Kotlin

fun <T, R> maxOf(params1: T, params2: T): R where T: Comparable<T>, T:() ->R {
    if (params1 > params2) {
        return params1.invoke()
    } else {
        return params2.invoke()
    }
}
Copy the code

Type erasure

As we all know, Java generics are pseudo-generics, because all generic information is erased during compilation in Java. The first prerequisite for understanding the concept of generics correctly is to understand type erasure. Java generics are basically implemented at the compiler level. The generated bytecode does not contain the type information in the generics. When the generics are used, the type parameters will be removed when the compiler compiles, and this process is called type erasing.

Defining types such as List and List, for example, becomes a List when compiled, and the JVM sees only the List, while the additional type information provided by generics is invisible to the JVM

  • 1: The original type is equal
public class Test {
    public static void main(String[] args) {
        ArrayList<String> list1 = new ArrayList<String>();
        list1.add("abc");

        ArrayList<Integer> list2 = new ArrayList<Integer>();
        list2.add(123); System.out.println(list1.getClass() == list2.getClass()); }}Copy the code

In this example, we define two ArrayList arrays, but one is of the ArrayList generic type and can only store strings. One is the ArrayList generic type, which can only store integers. Finally, we get information about list1 objects and List2 objects using the getClass() method, and find that the result is true. Note The generic String and Integer types have been erased, leaving only the primitive type.

  • 2: Add elements of other types through reflection
public class Test {
    public static void main(String[] args) throws Exception {
        ArrayList<Integer> list = new ArrayList<Integer>();
        list.add(1);  // This call to add can only store integers, since instances of generic types are Integer
        list.getClass().getMethod("add", Object.class).invoke(list, "asd");
        for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); }}}Copy the code

We define an ArrayList generic type instantiated as an Integer object. If we call add() directly, we can only store Integer data, but when we call add() with reflection, we can store strings. This shows that the Integer generic instances are erased after compilation. Only the original type is preserved.

  • 3: indicates the original type retained after type erasure

A primitive type is the true type of the type variable in the bytecode after the generic information is erased. Whenever a generic type is defined, the corresponding primitive type is automatically supplied, the type variable is erased, and replaced with its qualified type (unqualified variables are replaced with Object).

class Pair<T> {  
    private T value;  
    public T getValue(a) {  
        return value;  
    }  
    public void setValue(T value) {  duyou
    private Object value;  
    public Object getValue(a) {  
        return value;  
    }  
    public void setValue(Object value) {  
        this.value = value; }}Copy the code

Because T is an unqualified type variable in Pair, replacing it with Object results in a normal class, just as generics were implemented before they were added to the Java language. You can include different types of pairs in your program, such as Pair or Pair, but when you erase the types they become the original Pair type, which is Object.

For questions about type erasure, see the Great God article

Type erasure problems and solutions

For a variety of reasons [partly because Java was all-user compatible, there were no generics before Jdk1.5, but the magnitude was so large that Sun had to use pseudo-generics], Java cannot implement true generics, only pseudo-generics with type erasures, but this raises some new problems.

1: Check, recompile, and check compiled objects and reference passing issues

ArrayList<String> list1 = new ArrayList<>();
list.add(1)  // Error compiling
list.add("1") // The compiler passes
Copy the code

In the above code, we define a list that can be read and written. When we pass in an Integer object with add, the program immediately reports an error. This means that the program does type checking before compiling.

So who exactly is this type checking for? Look at the code

ArrayList<String> list1 = new ArrayList<>();
list1.add(1); // Error compiling
list1.add(""); // The compiler passes


ArrayList list2 = new ArrayList<String>();
list2.add(1); // The compiler passes
list2.add(""); // The compiler passes
Copy the code

As you can see, when we create objects using list2, the program is error-free, meaning we can pass in any object. Why?

The main reason for this is that new ArrayList() just opens up a storage control in memory that can store objects of any type. What really matters is its reference. Because our list1 reference is ArrayList

, we can do generic type detection. List2, which references an ArrayList, doesn’t use generics, so it won’t work.

To give a more comprehensive example:

public static void main(String[] args) {  
    ArrayList<String> arrayList1=new ArrayList();  
    arrayList1.add("1");// The compiler passes
    arrayList1.add(1);// Error compiling
    String str1=arrayList1.get(0);// The return type is String
        
    ArrayList arrayList2=new ArrayList<String>();  
    arrayList2.add("1");// The compiler passes
    arrayList2.add(1);// The compiler passes
    Object object=arrayList2.get(0);// Return type is Object
        
    new ArrayList<String>().add("11");// The compiler passes
    new ArrayList<String>().add(22);// Error compiling
    String string=new ArrayList<String>().get(0);// The return type is String
}  
Copy the code

2: automatic type conversion

Because of type erasure, all generic type variables end up being replaced with the original type, so there is a problem. Since they are all replaced with primitive types, why don’t we cast them when we fetch? Take a look at the get method of ArrayList:

public E get(int index) {  
    RangeCheck(index);  
    return (E) elementData[index];  
}  
Copy the code

As you can see, there is a strong cast based on the generic variable before return. If the generic type variable is Date, the generic information will be erased, but (E) elementData[index] will be compiled to (Date)elementData[index]. So we don’t have to do it ourselves.

3: Type erasure causes conflicts with polymorphism

There is now a generic class like this:

class Pair<T> {  
    private T value;  
    public T getValue(a) {  
        return value;  
    }  
    public void setValue(T value) {  
        this.value = value; }}Copy the code

And then we want a subclass to inherit it

class DateInter extends Pair<Date> {  
    @Override  
    public void setValue(Date value) {  
        super.setValue(value);  
    }  
    @Override  
    public Date getValue(a) {  
        return super.getValue(); }}Copy the code

In this subclass, we set the generic type of the parent class to Pair. In this subclass, we override the two methods of the parent class. We want to limit the generic type of the parent class to Date, so that the two methods of the parent class will have the parameters of Date:

public Date getValue(a) {  
    return value;  
}  
public void setValue(Date value) {  
    this.value = value;  
}  
Copy the code

So, we have no problem overriding these two methods in our subclasses. In fact, as you can see from their @override tag, we have no problem at all. Is that really the case?

Analysis:

In fact, when the type is erased, all the generic types of the parent class become the original type Object, so the parent class will compile like this:

class Pair {  
    private Object value;  
    public Object getValue(a) {  
        return value;  
    }  
    public void setValue(Object value) {  
        this.value = value; }}Copy the code

Look again at the types of the subclasses’ two overridden methods:

@Override  
public void setValue(Date value) {  
    super.setValue(value);  
}  
@Override  
public Date getValue(a) {  
    return super.getValue();  
}
Copy the code

The setValue method has the same type of Object as the parent class and Date as the subclass. If it is an ordinary inheritance relationship, it will not be overridden at all, but overridden. Let’s test it in a main method:

public static void main(String[] args) throws ClassNotFoundException {  
    DateInter dateInter=new DateInter();  
    dateInter.setValue(new Date());                  
    dateInter.setValue(new Object());// Error compiling
}  
Copy the code

If it is overloading, then there are two setValue methods in the subclass, one of Object type and one of Date type. However, we find that there is no such subclass that inherits the method of Object type parameter from the parent class. So it’s rewritten, not reloaded.

Why is that?

The reason is that the generic type we pass in to the parent class is Date, Pair, and our intention is to change the generic class to the following:

class Pair {  
    private Date value;  
    public Date getValue(a) {  
        return value;  
    }  
    public void setValue(Date value) {  
        this.value = value; }}Copy the code

Then subclass to override the two methods whose parameter type is Date to achieve polymorphism in inheritance. However, for some reason, the virtual machine cannot change the generic type to Date. Instead, the virtual machine can erase the generic type and change it to Object. So, our intention is to rewrite and implement polymorphism. However, after type erasure, can only become overloaded. Thus, type erasure conflicts with polymorphism. Does the JVM know what you mean? Yes!! But can it be implemented directly? No!! If not, how can we override the method we want for the Date type parameter?

The JVM uses a special method to accomplish this, called the bridge method.

First, we decompile the bytecode of the subclass DateInter using javap -c className. The result is as follows:

class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {  
  com.tao.test.DateInter();  
    Code:  
       0: aload_0  
       1: invokespecial #8                  // Method com/tao/test/Pair."<init>"  
:()V  
       4: return  
  
  public void setValue(java.util.Date);  // We override the setValue method
    Code:  
       0: aload_0  
       1: aload_1  
       2: invokespecial #16                 // Method com/tao/test/Pair.setValue  :(Ljava/lang/Object;) V5: return  
  
  public java.util.Date getValue(a);    // We override the getValue method
    Code:  
       0: aload_0  
       1: invokespecial #23                 // Method com/tao/test/Pair.getValue  
:()Ljava/lang/Object;  
       4: checkcast     #26                 // class java/util/Date  
       7: areturn  
  
  public java.lang.Object getValue(a);     // A clever method generated by the compiler at compile time
    Code:  
       0: aload_0  
       1: invokevirtual #28                 // Method getValue:()Ljava/util/Date to call our overridden getValue Method
;  
       4: areturn  
  
  public void setValue(java.lang.Object);   // A clever method generated by the compiler at compile time
    Code:  
       0: aload_0  
       1: aload_1  
       2: checkcast     #26                 // class java/util/Date  
       5: invokevirtual #30                 // Method setValue:(Ljava/util/Date; To call the setValue method that we overwrote
)V  
       8: return  
}  
Copy the code

It turns out that the subclasses we intended to override setValue and getValue have four methods, the last two of which, not surprisingly, are the bridge methods generated by the compiler itself. As you can see, the bridge methods are of type Object, which means that the bridge methods in the subclass really override the two methods in the parent class. The @oveerride on top of our own setValue and getValue methods is just an illusion. The internal implementation of the bridge method is just to call the two methods that we overwrote ourselves. Therefore, the virtual machine cleverly uses the bridge method to resolve the conflict between type erasing and polymorphism.

Generic inline specialization Reified [unique to Kotlin]

We’ve looked at type erasure of generics before, and type erasure generally causes problems, such as the conversion operator as commonly used in Kotlin

fun <T> Any.asAny(a): T? {
    return this as? T
}
Copy the code

The above code did not check when casting, and could have run time crashes due to inconsistent types

For example, the following code

fun <T> Any.asAny(a): T? {
    return this as? T
}

fun main(a) {
    val res = 1.asAny<String>()? .substring(1)
    println(res)
}
Copy the code

The output

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
	at com.eegets.javademo.generic.ExtKt.main(Ext.kt:24)
	at com.eegets.javademo.generic.ExtKt.main(Ext.kt)
Copy the code

As you can see, the ClassCastException occurs because we did not do type checking, so in order to safely retrieve the data, we generally need to display the class information that passes the result of the conversion

fun <T> Any.asAny(clazz: Class<T>): T? {
    return if (clazz.isInstance(this)) {
        this as? T
    } else {
        null}}fun main(a) {
    val res = 1.asAny<String>(String::class.java)? .substring(1)
    println(res)
}
Copy the code

The output

null
Copy the code

This can solve the problem, but requires passing the class method, which can be cumbersome, especially if there are too many parameters.

Is there a better way to do this without passing parameters?

Reified inline specialization keyword

Fortunately, Kotlin has a better solution than Java does, which is also called Reified

Reified is very simple to use and has two main steps (which are mandatory) :

  • 1: Add the reified modifier before the generic type
  • 2: Add inline before method

We can improve the above code

inline fun <reified T> Any.asAny(clazz: Class<T>): T? {
    return if (this is T) {
        this
    } else {
        null}}fun main(a) {
    val res = 1.asAny<String>(String::class.java)? .substring(1)
    println(res)
}
Copy the code

At this point, the output is normal

public static final void main(a) {
      Integer $this$asAny$iv = 1;  // The value to be converted
      Class clazz$iv = String.class;
      int $i$f$asAny = false;
      String var10000 = (String)($this$asAny$iv instanceof String ? $this$asAny$iv : null);$this$asAny$iv 'constant is of type' String 'by instanceof
      if ((String)($this$asAny$iv instanceof String ? $this$asAny$iv : null) != null) { // Use inline code replacement and constant validation via instanceof
         String var4 = var10000;
         byte var6 = 1;
         $i$f$asAny = false;
         if (var4 == null) {
            throw new TypeCastException("null cannot be cast to non-null type java.lang.String");
         }

         var10000 = var4.substring(var6);
         Intrinsics.checkExpressionValueIsNotNull(var10000, "(this as java.lang.String).substring(startIndex)");
      } else {
         var10000 = null;
      }

      String res = var10000;
      boolean var5 = false;
      System.out.println(res);
   }
Copy the code

The Java keyword instanceof validates the constant $this$asAny$iv as a String with the Java keyword reanceof, and then inline replaces it with the Java keyword instanceof. The specified type is not erased because the inline function copies the bytecode into the called method at compile time, so the compiler knows the exact type when executing the code and replaces the generic type with the exact type to avoid erasure.

[See article about self-generics classes]

  • Overview of Generics in Java – this is definitely the most detailed overview of generic methods out there

  • Deep understanding of generics in Java and Kotlin

  • Understand Java generics in depth

  • Java generic type erasure and the problems associated with type erasure

  • Java Generics The internals of generics: type erasure and the problems associated with type erasure

  • Reified Types in Kotlin: how to use the type within a function (KAD 14)