Kotlin Note 12-Comparison of Paradigms in Java and Kotlin (1)

The paradigm in Kotlin is similar to that in Java. You can refer to my article on introducing the Java paradigm:

Java paradigm those things (1)

Java paradigm those things (2)

Java paradigm those things (3)

Java Paradigm Those Things (4)

In the above blog post, it tells why Java should introduce the paradigm in version 1.5, and some basic knowledge points about the Java paradigm.


If you divide an object into a statement and use it in two parts. Generics mainly focus on code reuse of type declarations, and wildcards focus on code reuse in use. Generics are used to define the parameterization of internal data types, and wildcards are used to define the parameterization of the object types used.

Using generics and wildcards improves code reuse. At the same time, the type of the object is checked for type safety, reducing errors during the type conversion process.


 Paradigm and array type changes

 Arrays in Java are covariant

The following code can be compiled and run correctly:

        Integer[] ints = new Integer[3];
        ints[0] = 0;
        ints[1] = 1;
        ints[2] = 2;
        Number[] numbers = new Number[3];
        numbers = ints;
        for (Number n : numbers) {
            System.out.println(n);
        }

In Java, because Integer is a subtype of Number, array type Integer [] is also a subtype of Number [], so an Integer [] value can be provided wherever Number [] value is needed.

Java is not covariant to List <T> generics

In other words, List <Integer> is not a subtype of List <Number>, and trying to provide List <Integer> where List <Number> is required is a type error. The following code, the compiler will directly report an error:

Even if we use wildcards, write:

 It is still an error.

Why can the Number object be instantiated by Integer, but the ArrayList <Number> object cannot be instantiated by ArrayList <Integer>? The <? extends Number> in the list declares that its element is a derived class of Number or Number, why can't it add Integer? In order to solve these problems, you need to understand the inversion and covariance in Java and the use of wildcards in generics.

Inversion, covariance, and invariance are all used to describe the inheritance relationship after type transformation. Its definition: if A and B represent types, f (⋅) represents type conversion, and ≤ represents inheritance relationship (for example, A ≤B means A is a subclass derived from B)

  •    When A≤B, f (A) ≤f ​​(B) holds, then f (⋅) is covariant
  •    When A≤B, f (B) ≤f (A) holds, then f (⋅) is contravariant
  •    When A≤B, the above two formulas are not true, that is, f (A) and f (B) have no inheritance relationship with each other, and f (⋅) is invariant

Both covariance and inverse covariance are type-safe.

 

Kotlin's array is not covariant

abstract class Animal(val size: Int)
class Dog(val cuteness: Int): Animal(100)
class Cat(val terrorFactor: Int): Animal(1)

The following array compilation error:

val dogArr: Array<Dog> = arrayOf(Dog(1), Dog(2))
val animalArr: Array<Animal> = dogArr

Like ordinary objects in Java, the following code can be compiled:

val dog: Dog = Dog(10)
var animal: Animal = dog

In Kotlin, a paradigm class is defined in the following way, and then it will be compiled with errors when used:

class ReadableList<T>{

}

val dogReadable: ReadableList<Dog> = ReadableList()
 //提示报错,需要ReadableList<Animal>,但却传了ReadableList<Dog>
val animalReadable: ReadableList<Animal> = dogReadable

Kotlin is covariant to List <T> generics

That is, the following code can be compiled through

val dogList: List<Dog> = listOf(Dog(10), Dog(20))
playAnimal(dogList)

fun playAnimal(animalList: List<Animal>) {
    ...
}

How to make Java and Kotlin add covariant and inverter support

Generics in Java are unchanged, but sometimes you need to implement inversion and covariance, what should you do? At this time, we need to use the wildcards we talked about before ? .

In Java and Kotlin, you can add support for parameter types that do not support covariance by default. But the two languages, Java and Kotlin, are handled differently:

  •    Java: use-site variance
  •    Kotlin: declaration-site variance

It can be seen that Java uses terminal type changes, while Kotlin uses declarative terminal types. What is the difference between the two?

Personal understanding is that using end-site variance (use-site variance) is to covariate when specifically using (initializing) a Class object.

Java <? extends T>implements generic covariance

List<? extends Number> list = new ArrayList<>();  

The ? extends Numberrepresentation here is the Number class or its subclasses, which we abbreviate as C.

Here C <= Number, this relationship holds List<C> <= List< Number >true: . There are:

List<? extends Number> list1 = new ArrayList<Integer>();  
List<? extends Number> list2 = new ArrayList<Float>();  

Another example, as shown in the following code:

List<Cat> catList = new ArrayList<>();
List<? extends Animal> animalList = catList;

As you can see, when we declared animalList, we made a little modification to the generic type. After using ? Extends Animal to modify, the above code can be successfully compiled and run. Even we can define a method to accept this parameter type as follows:

List<Cat> cats = new ArrayList<>();
playAnimal(cats);

public static void playAnimal(List<? extends Animal> animal) {
    ...
}

Compilation can be passed smoothly, so our code will be more scalable!

⚠️ Note: At this time, except for null, you cannot add any objects of Animal subclass to animalList, that is, the following code will report an error:

如果可以添加的话,List<? extends Number>It will hold objects of various Number subtypes (Byte, Integer, Float, Double, etc.). In order to protect its type consistency, Java prohibits adding any objects to List <? Extends Number>, but null can be added.

Java <? super T>implements generic inversion

List<? super Number> list = new ArrayList<>();  

? super Number Wildcards indicate the lower bound of the type is Number. That is, the parent type F is here ? super Number, and the child type C is Number. That is, when F <= C, there is f (C) <= f (F), this is the inversion. Code example:

List<? super Number> list3 = new ArrayList<Number>();  
List<? super Number> list4 = new ArrayList<Object>();  
list3.add(new Integer(3));  
list4.add(new Integer(4)); 

In other words, we cannot List<? super Number >add any parent object of Number to it. But you can add Number and its subclass objects to List <? Super Number>.

 

PECS: When to use extends? When to use super?

Joshua Bloch refers to objects that you can only read from as producers , and objects that you can only write to as consumers . He suggested: " In order to maximize flexibility, use wildcard types on input parameters representing producers or consumers " and proposed the following mnemonics:

PECS stands for Producer-Extens, Consumer-Super (Producer-Extends, Consumer-Super).

Note : If you use a producer object, for example  List<? extends Foo>, calling add() or  on the object is not allowed  set(). But this does not mean that the object is immutable : for example, nothing prevents you clear()from calling  to delete all items from the list, because  clear() no parameters are needed at all. The only thing guaranteed by wildcards (or other types of type changes) is type safety . Immutability is another matter entirely.

 

Declare type change

Suppose there is a generic interface  Source<T>, there is no T method as a parameter in the interface  , but the method returns the  T type value:

// Java
interface Source<T> {
    T nextT();
}

Then, it   is extremely safe Source <Object> to store Source <String>the reference of the instance in a variable of type  -no consumer-method can be called. But Java does not know this, and still prohibits such operations:

// Java
void demo(Source<String> strs) {
    Source<Object> objects = strs; // !!!在 Java 中不允许
    // ……
}

To correct this, we must declare the type of the object as  Source<? extends Object>, which is meaningless, because we can call all the same methods on the object as before, so more complex types do not bring value. But the compiler does not know.

In Kotlin, there is a way to explain this to the compiler. This is called a declaration type change : we can mark  Source the type parameter T  to ensure that it is only returned (produced) from  Source<T> members and never consumed. For this, we provide the  out  modifier:

interface Source<out T> {
    fun nextT(): T
}

fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs // 这个没问题,因为 T 是一个 out-参数
    // ……
}

The general principle is: when C the type parameter of  a class  T is declared as  out  , it can only appear in  C the output -position of the member , but the return is a superclass that  C<Base> can be safely used  C<Derived>.

In short, they say that class  C are in the parameter  T on a covariant , or  T a covariant type parameter. You can think of  C as  T a producer , not  T a consumer .

The out modifier is called a type change annotation , and since it is provided at the type parameter declaration, we will talk about the type change declaration . This is the opposite of Java's use of type change , whose type use wildcards make type covariance.

In addition to  out , Kotlin added a type change comment: in . It makes a type parameter inversion : it can only be consumed but not produced. A good example of inverter type is  Comparable:

interface Comparable<in T> {
    operator fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
    x.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的子类型
    // 因此,我们可以将 x 赋给类型为 Comparable <Double> 的变量
    val y: Comparable<Double> = x // OK!
}

We believe that the   words in  and  out are self-explanatory (because they have been used successfully in C # for a long time), so the mnemonics mentioned above are not really needed and can be rewritten to a higher goal:

The Existential  transformation: consumer in, producer out!  :-)

 

Handle when defining a class, as follows:

// 使用out关键字
class ReadableList<out T>{

}

val dogReadable: ReadableList<Dog> = ReadableList()
val animalReadable: ReadableList<Animal> = dogReadable

The above code only difference now is that we defined before ReadableList is to add a little generic restrictions  OUT , then you can be successful assignment dogReadable to animalReadable object. We should see this before Kotlin API Why can guess the List <generic> supports covariance.

⚠️ Note: But after modified with the out keyword, there can be no methods with T as the parameter type inside the ReadableList class

 

 


reference: 

 

Published 82 original articles · Like 86 · Visit 110,000+

Guess you like

Origin blog.csdn.net/unicorn97/article/details/81843897