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)