Kotlin泛型和方差(与Java相比)

本文介绍Kotlin中的泛型和方差的概念,并将其与Java进行比较。 Kotlin泛型与Java的不同之处在于,用户如何定义子类型关系中的行为。 与Java相对,Kotlin允许在声明站点上定义差异,而Java仅知道使用站点差异。

Kotlin泛型-什么是方差?

许多编程语言都支持子类型的概念,该子类型允许实现表示关系的层次结构,例如"A cat IS-an animal".  In Java, we can either use the extends keyword to change/expand behavior of an existing class (inheritance) or use implements to provide implementations for an interface.  According to 里斯科夫的替代原则, every instance of a class A can be substituted 通过 instances of its subtype B. 方差这个词也经常在数学中使用,用于描述复杂的方面如何进行子类型化,例如方法返回类型,类型声明,泛型类型或数组 与所涉及类的继承方向有关。我们需要考虑三个术语:协方差,相反方差和不变性。

实践差异(Java)

协方差

在Java中,需要使用一种覆盖方法 协变 in its return type, i.e., the return types of the overridden and the overriding method must be in line with the direction of the inheritance tree of the involved classes. A method treat(): Animal of class AnimalDoctor can be overridden with treat(): Cat in class CatDoctor, which extends AnimalDoctor. Another example of covariance would be type conversion, shown here:

public class Animal {}
public class Cat extends Animal {}
(Animal) new Cat() //works fine
(Cat) new Animal() //will not work

We can cast subclasses up the inheritance tree, while down-casting causes an error. This is also the case if we take a look at variable declarations. It isn’t a problem to assign an instance of Cat to a variable of type Animal, whereas doing the opposite will cause failure.

逆差

逆差, on the other hand, describes the exact opposite. In Java, we have to deal with it when working with generics, which we're going to look at later. To make it clear, we can imagine another programming language that allows contravariant method arguments in overriding methods (In Java, this would be an overloaded method instead). Let’s say we have a class ClassB which extends another class ClassA and overrides a method 通过 changing the original parameter type T' to its supertype T.

ClassA::someMethod(t: T')
ClassB::someMethod(t: T)

You can see that the type hierarchy of method parameter t is contrary to the hierarchy of the surrounding classes. Up versus down the tree, someMethod is contravariant in its parameter type.

不变性

最后但并非最不重要的一点,最简单的是:不变性。正如我们在前面的示例中看到的那样,当我们再次想到Java中的重写方法时,可以观察到这个概念。覆盖方法必须只接受与覆盖方法相同的参数。我们谈到 不变性 如果类型,例如方法参数在父类型和子类型中没有区别。

Java中集合类型的差异

我们要考虑的另一方面是数组和其他类型的泛型集合。 Arrays in Java are 协变 in their type, which means an array of Strings can be assigned to a variable of type Object [].

Object [] arr = new String [] {"hello", "world"};

但更重要的是,数组在它们持有的类型中是协变的。这意味着您可以将整数,字符串或任何种类的对象添加到对象[]。

Object [] arr = new Object [2];
arr[0] = 1;
arr[1] = "2";

Covariant arrays might appear quite handy but can cause errors at runtime. A variable of type Object [] can possibly reference an object of String []. What happens if we pass the variable to a method expecting an array of Objects? This method might want to add an Object to the array, which seems legit because the parameter is expected to be of type Object []. The problem is that the caller has no idea what the method is possibly going to put into their array which possibly causes an ArrayStoreException at runtime, which we see in this simplified example:

Object [] arr = new String [] {"hello", "world"};
arr[1] = new Object(); //throws: java.lang.ArrayStoreException: java.lang.Object 

通用集合

As of Java 1.5, generics can be used to inform the compiler which elements are supposed to be stored in a particular collections instance (i.e. List, Map, Queue, Set, etc.). Unlike arrays, 通用集合是不变的 in their parameterized type 通过 default. This means you can’t substitute a List<Animal> with a List<Cat>. It won’t even compile. As a result, it is not possible to run into unexpected runtime errors which makes generic collections safer than 协变 arrays (有效的Java)。缺点是,起初我们在集合子类型方面不那么灵活。幸运的是,使用泛型时,用户可以显式指定类型参数的差异。我们称之为 使用地点差异.

协变集合

The following code example shows how we declare a 协变 list of Animal and assign a list of Cat to it.

List<Cat> cats = new ArrayList<>();
List<? extends Animal> animals = cats;

像这样的协变列表仍然不同于数组,因为协方差被编码在其类型参数中,这对读者来说更加明显。我们只能从列表中读取内容,而编译器禁止添加元素。据说这个清单是 制片人 of Animals. The generic type ? extends Animal (? is the "wildcard" character) only indicates that the list contains any type with an upper bound of Animal, which could mean a list of Cat, Dog or any other animal. This approach turns the runtime error encountered with 协变 arrays into the preferable compile error:

 public void processAnimals(List<? extends Animal> collection){
    Animal a = collection.get(0);
    Cat c = collection.get(0); //will not compile
    collection.add(new Dog()) //will not compile
}

Now, if your invoke processAnimals with a list of Cat, as a caller, you can be sure that the function won't add anything to it. The function itself can only retrieve Animals from the collection since that's what the 通配符 syntax indicates.

逆向集合

It is also possible to work with contravariant collections, which we declare with a generic type parameter ? super Animal (lower bound of type Animal). A list like that may be of type List<Animal> itself or a list of any supertype of Animal, even Object. Like with 协变 lists, we can't know which type the list really represents (again indicated 通过 the 通配符). The difference is, we can not read from a contravariant list since it is unclear whether we will get Animals or just plain Objects. Writing to the list is permitted though since we know that the original caller expects Animals or its supertypes which makes it possible to add any subtype of Animal. The list is acting as a 消费者 of Animals:

public void addAnimals(List<? super Animal> collection) {
    collection.add(new Cat());
    collection.add(new Dog());
}

The function shown above accepts a contravariant list of Animals. As a caller of this function you could pass a List<Animal> and also List<Object> or lists of other possible supertypes of Animal. You can be sure that only animals or its subtypes will be added to your collection which means that, even after the method call, you are safe to retrieve animals from that list:

List<Animal> animals = new ArrayList<>();
addAnimals(animals);
//here we can still safely get Animals from the list, we don't care which subtype it actually has
Animal someAnimal = animals.get(0);

addAnimals(new ArrayList<Object>());

约书亚·布洛赫(Joshua Bloch)在他的绝妙书中创造了一条经验法则 有效的Java: "生产者扩展,超级消费者(PECS)" which helps memorizing the relation between 协变 制片人s and the extends keyword as well as between contravariant 消费者s and the super keyword.

与Kotlin泛型的差异

在研究了一般意义上的差异以及Java如何利用这些概念之后,我们现在来看一下Kotlin的处理方式。 Kotlin在泛型方面与Java有所不同,在某些方面还与数组相结合,对于有经验的Java开发人员乍一看可能有些奇怪。第一个,也许是最舒适的区别是: Kotlin中的数组是不变的. As a result, as opposed to Java, it is not possible to assign an Array<String> to a reference variable of type Array<Object>. That's great because it ensures compile-time safety and prevents runtime errors you may encounter in Java with its 协变 arrays. 但是仍然需要使用其他方法来处理子类型的数组或过程,我们将在下面讨论。

申报地点差异

As shown earlier, Java uses so-called "wildcard types" to make generics variant, which is said to be [the most tricky part[s] of Java’s type system] (http://kotlinlang.org/docs/reference/generics.html#type-projections). Since the user of particular generic types has to handle the variance every time a specific type is used, we refer to it as 使用地点差异. Kotlin does not use 通配符s at all. Instead, in Kotlin we use 申报地点差异. Let’s recall the initial problem again: Imagine, we have a class ReadableList<E> with one simple 制片人 method get(): T. Java prohibits the assignment of an instance of ReadableList<String> to a variable of type ReadableList<Object> because generic types are invariant 通过 default. To fix this, the user can change the variable type to ReadableList<? extends Object> and everything works fine. Kotlin approaches that issue differently. We can mark the type T as only being produced with the out keyword so that the compiler immediately understands: ReadableList is never going to consume any T, which makes it 协变 in T.

abstract class ReadableList<out T> {
    abstract fun get(): T
}

fun workWithReadableList(strings: ReadableList<String>) {
    val objects: ReadableList<Any> = strings // This is OK, since T is an out-parameter
    // ...
}

As you can see, the type T is annotated as an out type via 申报地点差异 - also called variance annotation. The compiler does not prohibit the use of T as a 协变 type. A great example of a 协变 collection in the standard library is List<T>:

val ints: List<Int> = listOf(1, 2, 3, 4)

fun takeNumbers(nums: List<Number>) {
    val number: Number = nums[0]
     nums.add(1) // add is not visible
}

takeNumbers(ints)

You can easily pass a List<Int> to a function accepting a List<Number> since List is only a 制片人 of T and takeNumbers will never add something to the class. On the other hand, if the parameter nums was defined with type MutableList<Number>, the add would work just fine, but the caller could not pass in a List<Int> anymore and it would be evident that the method can add stuff to the list passed into it.

Of course, there is also a corresponding annotation to mark generic types as 消费者s, i.e., make them contravariant: in. Folks have been using the presented approach in C# successfully for some years already.

Kotlin的规则要记住:生产者退出,消费者进入

使用地点差异,类型投影

Unfortunately, it's not always sufficient to have the opportunity of declaring a type parameter T as either out or in. Just think of arrays for example. An array offers methods for adding and receiving objects, so it cannot be either 协变 or contravariant in its type parameter. As an alternative, Kotlin also allows 使用地点差异 which we can apply using the already defined keywords in and out:

Array<in String> corresponds to Java’s Array<? super String> and Array<out String> corresponds to Java’s Array<? extends Object>

fun copy(from: Array<out Any>, to: Array<Any>) {
 // ...
}

The example shows how from is declared as a 制片人 of its type and thus the copy method cannot do bad things like adding to the Array. This concept is called 类型投影 since the array is restricted in its methods: only those methods that return type parameter T may be called.

修饰类型

与Java中相同,泛型类型仅在编译时可用,并且 在运行时擦除。这对于大多数用例已经足够了,因为我们使用泛型来确保类型安全。不过有时候,能够在运行时检索实际的类型信息也很棒。幸运的是,Kotlin带有一项称为 修饰类型,这样就可以在运行时处理通用类型(例如,执行类型检查)。我在此深入介绍了此功能 堆栈溢出 文章并发表了 博客文章 关于它。

底线

In this 文章, we looked at the quite complex aspects of variance in the context of generics. We've used Java to demonstrate the concepts of covariance, contravariance, and 不变性 and compared it to Kotlin. Kotlin tries to simplify generics using different approaches such as 申报地点差异 and also introduces more obvious keywords (in, out). In my opinion, Kotlin really improves and simplifies the usage of generics and additionally eliminates the problem of 协变 arrays. Declaration-site variance simplifies client code a lot 通过 liberating it from using complex declarations as known from Java's 通配符 syntax. Also, even if we have to fall back to 使用地点差异, the syntax appears clearer and easier to understand. I know this topic is not the simplest one, but hopefully, some aspects were made a bit clearer in this 文章. If you still struggle with variance and generics in Kotlin, the book 行动中的科特林 是我应该阅读的推荐资源。

5个想法“Kotlin泛型和方差(与Java相比)

发表评论

您的电子邮件地址不会被公开。 必需的地方已做标记 *