This article is simultaneously published on my wechat official account. You can follow it by scanning the QR code at the bottom of the article or searching Nanchen on wechat
primers
Nan Chen has not written an article for a long time. In fact, he has been thinking about many problems, how to write valuable words for everyone. After reviewing the styles of several new influencer bloggers, I think it’s time for me to start a new chapter. Yes, that’s literacy.
Today, we bring you our first literacy article: Kotlin’s Generics.
I believe there are always a lot of students, always complaining about generic no matter how to learn, just stay in a simple use of the level, so has been suffering for this.
Kotlin, as a language that can interact with Java, naturally supports generics, but Kotlin’s new keywords in and out have always confused some people, because Java’s generics fundamentals are not solid enough.
Many students always have these questions:
- What is the difference between Kotlin generics and Java generics?
- What exactly is the point of Java generics?
- What exactly does Java type erasure mean?
- What is the difference between upper bound, lower bound, and wildcard characters of Java generics? Can they implement multiple restrictions?
- The Java
<? extends T>
,<? super T>
,<? >
What does that correspond to? What are the usage scenarios? - The Kotlin
in
,out
,*
,where
What’s the magic? - What is a generic approach?
Today, with an article for you to remove the above doubts.
Generics: The edge of type safety
As you know, generics were not a concept in Java prior to 1.5. Back then, a List was just a collection that could hold everything. So it’s inevitable to write code like this:
List list = new ArrayList();
list.add(1);
list.add("nanchen2251");
String str = (String) list.get(0);
Copy the code
The above code compiles without any problems, but it does run with the usual ClassCastException:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
Copy the code
The experience is terrible, and what we really need is to catch bugs as they compile, not release them into production.
If we added generics to the code above, we would see obvious errors at compile time.
List<String> list = new ArrayList<>();
list.add(1);
ð error Required type:String but Provided:int
list.add("nanchen2251");
String str = list.get(0);
Copy the code
Obviously, with the advent of generics, we can make the type safer. We don’t need to write StringList, StringMap when we use List, Map, etc. We just need to specify the parameter type when we declare the List.
In general, generics have the following advantages:
- Type checking, which helps developers detect errors at compile time;
- More semantic, let’s say we declare one
LIst<String>
We can tell directly what is stored in itString
Object; - Automatic type conversion, no need to do strong conversion operation when obtaining data;
- Be able to write more generic code.
Type erasure
Some of you might have thought, since generics are related to types, can you also use polymorphism of types?
We know that a subtype can be assigned to a parent type, as in:
Object obj = "nanchen2251";
// ð this is polymorphic
Copy the code
Object, as the parent of String, can naturally accept the assignment of String objects.
But when we write down this code:
List<String> list = new ArrayList<String>();
List<Object> objects = list;
// ð polymorphism Provided: List
Copy the code
This assignment error occurs because Java’s generics have an inherently “Invariance” Invariance, which states that List
and List
Since Java generics are themselves “pseudo-generics”, Java cannot use Object references on the underlying implementation of generics in order to be compatible with pre-1.5 versions, so the generics we declare will be “type erased” at compile time, with generic types being replaced by Object types. Such as:
class Demo<T> {
void func(T t){
// ...}}Copy the code
Will compile to:
class Demo {
void func(Object t){
// ... }}Copy the code
You may be wondering why our generics were changed to Object after type erasure at compile time, so we don’t need to strong-cast when we use them. Such as:
List<String> list = new ArrayList<>();
list.add("nanchen2251");
String str = list.get(0);
// ð there is no requirement to force list.get(0) to String
Copy the code
This is because the compiler will perform a type check in advance according to the generic type we declare, and then erase it to Object. However, the bytecode also stores the type information of our generic type, and when we use the generic type, the erased Object will be automatically cast. So list.get(0) is itself a strong String.
This technique looks good, but there is a downside. At runtime, you are not sure what type the Object is, although you can get the type of the Object by checking the checkcast inserted by the compiler. But you can’t really use T as a type: for example, this statement is illegal in Java.
T a = new T();
// ð error: Type parameter 'T' cannot be instantiated directly
Copy the code
Similarly, since they’re all erased as objects, you can’t make some kind of distinction based on type.
Such as instanceof:
if("nanchen2251" instanceof T.class){
// ð error: Identifier Expected Unexpected token
}
Copy the code
Such as overloading:
void func(T t){
// ð é : 'func(T)' clashes with 'func(E)'; both methods have same erasure
}
void func(E e){}Copy the code
Also, since primitive data types are not OOP and therefore cannot be erased as Object, Java generics cannot be used for primitive types:
List<int> list;
ð é čŊŊ : Type argument cannot be of primitive Type
Copy the code
Oop: Object Oriented Programming
This brings us to the third question above: What exactly does Java type erasure mean?
The first thing you need to understand is that the type of an Object is never erased. If you use an Object to refer to an Apple Object, for example, you can still get its type. For example, RTTI.
RTTI: Run Time Type Identification
Object object = new Apple();
System.out.println(object.getClass().getName());
// ð will print Apple
Copy the code
Even if it’s inside a generic.
class FruitShop<T>{
private T t;
public void set(T t){
this.t = t;
}
public void showFruitName(a){
System.out.println(t.getClass().getName());
}
}
FruitShop<Apple> appleShop = new FruitShop<Apple>();
appleShop.set(new Apple());
appleShop.showFruitName();
// ð will print Apple too
Copy the code
Why? Because a reference is just a label that accesses an object, and the object is always on the heap.
So don’t take it out of context that type erasure means erasing the types of the objects in the container, and by type erasure, I mean the container classesFruitShop<Apple>
forApple
The type declaration is erased after type checking at compile time and becomes andFruitShop<Object>
Equivalent effect, you could sayFruitShop<Apple>
å FruitShop<Banana>
Be clean for andFruitShop<Object>
Equivalent, not refers to inside the object itself of the type is erased!
So, is there type erasure in Kotlin?
Both C# and Java did not support generics at first. Java only added generics in 1.5. In order for a language that does not support generics to support generics, there are only two ways to go:
- The previous non-generic container remains the same, and then a set of generic types is added in parallel.
- Extend existing non-generic containers directly to generics without adding any new generic versions.
Java had to choose the second option because of the amount of code available in one sentence prior to 1.5, while C# wisely chose the first.
Kotlin was originally written in Java 1.6, so there were no problems with generics. Does Kotlin’s implementation of generics have type erasures?
Of course. As is already clear, Kotlin is written in Java 1.6, and Kotlin and Java have strong interoperability, as well as type erasers.
But…
You’ll still find something interesting:
Val list = ArrayList() // ð æ é : Not enough information to infer type variable ECopy the code
In Java, it’s fine not to specify generic types, but Kotlin doesn’t work that way. This is easy to think about, since there was certainly no such code before Java 1.5, and generics were not designed to accommodate the default Kotlin Any.
The upper bound wildcard of the generic
As mentioned earlier: Because Java’s generics are inherently “invariable Invariance”, even though the Fruit class is the parent of the Apple class, Java argues that List
is not the same as List
, which is to say, The subclass’s generic List
is not a subclass of the generic List
.
So such code is not run.
List<Apple> apples = new ArrayList<Apple>();
List<Fruit> fruits = apples;
Required type:List
Provided: List
Copy the code
What if we want to break through that barrier? Use upper bound wildcards? Extends.
List<Apple> apples = new ArrayList<Apple>();
List<? extends Fruit> fruits = apples;
// ð use the upper bound wildcard, compile no longer error
Copy the code
“Upper bound wildcards” allow Java generics to be “Covariance”, where Covariance allows the above assignments to be legal.
In an inheritance tree, a subclass inherits from a parent class, which can be considered above and below. Extends limits the parent type of a generic type, so it is called an upper bound.
It has two meanings:
- Among them
?
It’s a wildcard, which means thisList
The generic type of is an unknown type. extends
It limits the upper bound of the unknown type, that is, the generic type must satisfy thisextends
The restrictions are here and definedclass
įextends
The keywords are a little different:- Its scope extends not only to all direct and indirect subclasses, but also to the superclass itself defined by the upper bound, i.e
Fruit
. - It also has
implements
The upper bound here could also beinterface
.
- Its scope extends not only to all direct and indirect subclasses, but also to the superclass itself defined by the upper bound, i.e
Does this breach make sense?
Some do.
So let’s say we have an interface Fruit:
interface Fruit {
float getWeight(a);
}
Copy the code
There are two Fruit classes that implement the Fruit interface:
class Banana implements Fruit {
@Override
public float getWeight(a) {
return 0.5 f; }}class Apple implements Fruit {
@Override
public float getWeight(a) {
return 1f; }}Copy the code
Suppose we have a need to weigh fruit:
List<Apple> apples = new ArrayList<>();
apples.add(new Apple());
float totalWeight = getTotalWeight(apples);
// ð æ įĪš é : Required type: List
Provided: List
private float getTotalWeight(List<Fruit> fruitList) {
float totalWeight = 0;
for (Fruit fruit : fruitList) {
totalWeight += fruit.getWeight();
}
return totalWeight;
}
Copy the code
This is a very normal requirement. The scale can weigh all kinds of fruit, but it can also just weigh apples. You can’t stop weighing me just because I buy apples. So add the above code to the upper bound wildcard.
List<Apple> apples = new ArrayList<>();
apples.add(new Apple());
float totalWeight = getTotalWeight(apples);
// ð no longer returns an error
// ð added upper bound wildcards? extends
private float getTotalWeight(List<? extends Fruit> fruitList) {
float totalWeight = 0;
for (Fruit fruit : fruitList) {
totalWeight += fruit.getWeight();
}
return totalWeight;
}
Copy the code
However, the above use? Extends extends wildcards break one limit but impose another: they can be output but not input.
What does that mean?
Such as:
List<Apple> apples = new ArrayList<Apple>();
List<? extends Fruit> fruits = apples;
Fruit fruit = fruits.get(0);
fruits.add(new Apple());
// ð : Required type: capture of? extends Fruit Provided: Apple
Copy the code
Declares the upper wildcard generic collections, is no longer allowed to add new objects, not Apple, Fruit. More broadly: not just collections, but also writing a generic as input.
interface Shop<T> {
void showFruitName(T t);
T getFruit(a);
}
Shop<? extends Fruit> apples = new Shop<Apple>(){
@Override
public void showFruitName(Apple apple) {}@Override
public Apple getFruit(a) {
return null; }}; apples.getFruit(); apples.showFruitName(new Apple());
// ð : Required type: capture of? extends Fruit Provided: Apple
Copy the code
The lower bound wildcard of the generic
Generics have upper bounded wildcards, but do they have lower bounded wildcards?
Some do.
And the upper bound wildcard? Extends corresponds to a lower bound wildcard, right? super
Lower bound wildcard? Super all cases and? Extends upper wildcard is just the opposite:
- The wildcard
?
The generic type representing List is oneUnknown type. super
It limits the lower bound of the unknown type, that is, the generic type must satisfy the super constraint- It covers not only all direct and indirect child classes, but also the subclasses themselves defined by the lower bounds.
super
Also supportsinterface
.
The new restriction imposed on it is that it can only be input but not output.
Shop<? super Apple> apples = new Shop<Fruit>(){
@Override
public void showFruitName(Fruit apple) {}@Override
public Fruit getFruit(a) {
return null; }}; apples.showFruitName(new Apple());
Apple apple = apples.getFruit();
ð error: Required type: Apple Provided: Capture of? super Apple
Copy the code
Explain. First of all? Represents an unknown type. The compiler is not sure of its type.
We don’t know its type, but in Java any Object is a subclass of Object, so we can only assign objects from apples. GetFruit () to Object. Since the type is unknown, assigning directly to an Apple object is definitely irresponsible, requiring a layer of casts that can go wrong.
The Apple object must be a subtype of this unknown type. Due to the nature of polymorphism, it is legal to enter the Button object through the showFruitName.
In summary, Java generics themselves do not support covariant and contravariant generics:
- You can use generic wildcards
? extends
Make generics covariant, but “read but not modify”, where modification only means adding elements to the generic collection, ifremove(int index)
As well asclear
Of course you can. - You can use generic wildcards
? super
Make generics contravariant, but “modify but not read”, by “unread” I mean not read from generic types, if you followObject
Read out again strong turn of course is also ok.
Once you understand generics in Java, it’s easier to understand generics in Kotlin.
Kotlin’s out and in
Like Java generics, generics in Kolin themselves are immutable.
But in a different way:
- Use keywords
out
To support covariant, equivalent to the upper bound wildcard in Java? extends
. - Use keywords
in
To support inverse, equivalent to the lower bound wildcard in Java? super
.
val appleShop: Shop<out Fruit>
val fruitShop: Shop<in Apple>
Copy the code
They are exactly equivalent to:
Shop<? extends Fruit> appleShop;
Shop<? super Apple> fruitShop;
Copy the code
I wrote it a different way, but it does exactly the same thing. “Out” means, “I”, this variable or parameter is only used for output, not input, you can only read me, you can’t write me; In, on the other hand, means it’s only used for input, not output, you can only write me, you can’t read me.
Upper and lower bound constraints on generics
This is all about limiting generics as they are used. We call these “upper bound wildcards” and “lower bound wildcards”. Can we set this limit when we design the function?
Yes, yes, yes.
Such as:
open class Animal
class PetShop<T : Animal?>(val t: T)
Copy the code
Equivalent to Java:
class PetShop<T extends Animal> {
private T t;
PetShop(T t) {
this.t = t; }}Copy the code
Thus, when designing the PetShop class PetShop, we set an upper bound on the supported generics, which must be a subclass of Animal. So we use words like:
class Cat : Animal(a)val catShop = PetShop(Cat())
val appleShop = PetShop(Apple())
// ð error: Type mismatch. Required: Animal? Found: Apple
Copy the code
Obviously, Apple is not a subclass of Animal and certainly does not satisfy the upper bound of the PetShop generic type.
That… Can I set more than one upper bound constraint?
Sure. In Java, the way to declare multiple constraints on a generic parameter is to use & :
class PetShop<T extends Animal & Serializable> {
// ð implements two upper bounds via &, which must be a subclass or implementation of Animal and Serializable
private T t;
PetShop(T t) {
this.t = t; }}Copy the code
Instead of &, Kotlin added the where keyword:
open class Animal
class PetShop<T>(val t: T) whereT : Animal? , T : SerializableCopy the code
In this way, multiple upper bound constraints are realized.
Kotlin’s wildcard *
What we’ve said about generic types is that we need to know what type a parameter is. If we’re not interested in the type of a generic parameter, is there a way to handle this?
Some do.
In Kotlin, you can substitute the wildcard * for generic arguments. Such as:
val list: MutableList<*> = mutableListOf(1."nanchen2251")
list.add("nanchen2251")
// ð error: Type mismatch. Required: Nothing Found: String
Copy the code
This error is truly bizarre. The wildcard represents the generic parameter type of MutableList. A String was added to the initialization, but a compilation error occurred when adding a new String.
If the code looks like this:
val list: MutableList<Any> = mutableListOf(1."nanchen2251")
list.add("nanchen2251")
// ð no longer returns an error
Copy the code
It seems that the so-called wildcard as a generic parameter is not equivalent to Any as a generic parameter. MutableList<*> and MutableList
are not the same list. The type of the MutableList<*> is definite, while the type of the MutableList
is not. The compiler does not know what type it is. So it is not allowed to add elements because it would make the type unsafe.
But if you’re careful, you’ll notice that this is very similar to the generic covariant. The wildcard * is just a syntactic sugar that is covariant behind it. So: MutableList<*> is equivalent to MutableList
, using wildcards has the same properties as covariants.
In Java, wildcards have the same meaning, but with? As a wildmatch.
List<? > list =new ArrayList<Apple>();
Copy the code
Wildcards in Java? Which is the same thing as? Extends the Object.
Multiple generic parameter declarations
Can you declare multiple generics?
Yes, yes, yes.
Isn’t HashMap a classic example?
class HashMap<K,V>
Copy the code
Multiple generics can be split by multiple declarations, two above, actually multiple can be done.
class HashMap<K: Animal, V, T, M, Z : Serializable>
Copy the code
Generic method
If you declare generic types on a class, can you declare them on a method?
Yes, yes, yes.
If you’re an Android developer, isn’t findViewById of a View the best example?
public final <T extends View> T findViewById(@IdRes int id) {
if (id == NO_ID) {
return null;
}
return findViewTraversal(id);
}
Copy the code
Obviously, View has no generic parameter types, but findViewById is a typical generic method, and the generic declaration is on the method.
It’s also very easy to rewrite this as Kotlin:
fun
findViewById(@IdRes id: Int): T? {
return if (id == View.NO_ID) {
null
} else findViewTraversal(id)
}
Copy the code
The Kotlin reified
As mentioned earlier, any operation that needs to know the exact type of a generic at run time is useless because of type erasings in Java generics. For example, you can’t check whether an object is an instance of the generic type T:
<T> void printIfTypeMatch(Object item) {
if (item instanceof T) { // ð IDE will raise error, illegal generic type for instanceof}}Copy the code
The same goes for Kotlin:
fun <T> printIfTypeMatch(item: Any) {
if (item is T) { ð IDE prompts an error, Cannot check for instance of Erased Type: T
println(item)
}
}
Copy the code
The usual solution to this problem in Java is to pass an additional argument of type Class
and then check with the Class#isInstance method:
ð < T >void check(Object item, Class<T> type) {
if(type.isinstance (item)) {ð}}Copy the code
Kotlin can also do this, but there is a more convenient way to do this: use the keyword reified with inline:
ð ð
inline fun <reified T> printIfTypeMatch(item: Any) {
if (item is T) { // ð there will be no errors}}Copy the code
The above Gson parsing is widely used, such as this extension method in our project:
inline fun <reified T>String? .toObject(type: Type? = null): T? {
return if(type ! =null) {
GsonFactory.GSON.fromJson(this, type)
} else {
GsonFactory.GSON.fromJson(this, T::class.java)
}
}
Copy the code
conclusion
Kotlin generics and Java generics have spent a lot of time in this article. Now let’s go back to the first few questions. Do you have the score? If you still feel half-assed, you might as well read it several times.
There is a lot of reference to Kotlin’s generics article on code
Some of them were even taken directly, mainly because they didn’t want to duplicate the wheel. If there are omissions in this article, feel free to leave a comment in the comments section.
Do not finish the open source, write not finish hypocritical. Please scan the qr code below or search “Nanchen” on the official account to follow my wechat official account. At present, I mainly operate Android and try my best to improve for you. If you like, give me a “like” and share