Generics 泛型

Variance

变种 变体 变形

One of the most tricky parts of Java's type system is wildcard types (see Java Generics FAQ). And Kotlin doesn't have any. Instead, it has two other things: declaration-site variance and type projections.

First, let's think about why Java needs those mysterious wildcards. The problem is explained in Effective Java, 3rd Edition, Item 31: Use bounded wildcards to increase API flexibility. First, generic types in Java are invariant, meaning that List is not a subtype of List. Why so? If List was not invariant, it would have been no better than Java's arrays, since the following code would have compiled and caused an exception at runtime:
使用受限制的通配符以提高api的弹性
String is a subtype of Object, but List isn't a subtype of List.

// Java
List strs = new ArrayList();
//这里type mismatch
List objs = strs; // !!! A compile-time error here saves us from a runtime exception later
//如果上一行编译时没报错,这里把数字赋值给字符串就会发生运行时错误
objs.add(1); // Here we put an Integer into a list of Strings
String s = strs.get(0); // !!! ClassCastException: Cannot cast Integer to String
 
 

So, Java prohibits such things in order to guarantee run-time safety. But this has some implications. For example, consider the addAll() method from Collection interface. What's the signature of this method? Intuitively, we'd put it this way:
所以这样的事是禁止的,但是这样会有可能的影响。比如说我们给一个集合添加某些元素,在接口中,这个方法的签名该如何定义?直觉上,可能会是这样:

// Java
interface Collection ... {
  void addAll(Collection items);
}

But then, we can't do the following simple thing (which is perfectly safe):
这样的话,我们把一个字符串集合添加到一个对象集合时,就会报错,因为它们不是父子关系。

// Java
void copyAll(Collection to, Collection from) {
  to.addAll(from);
  // !!! Would not compile with the naive declaration of addAll:
  // Collection is not a subtype of Collection
}
 
 

(In Java, we learned this lesson the hard way, see Effective Java, 3rd Edition, Item 28: Prefer lists to arrays)

That's why the actual signature of addAll() is the following:

// Java
interface Collection ... {
  void addAll(Collection items);
}

The wildcard type argument ? extends Eindicates that this method accepts a collection of objects of E or some subtype of E, not just E itself. This means that we can safely read E's from items (elements of this collection are instances of a subclass of E), but cannot write to it since we do not know what objects comply to that unknown subtype of E. In return for this limitation, we have the desired behaviour: Collection is a subtype of Collection. In "clever words", the wildcard with an extends-bound (upper bound) makes the type covariant.
Collection Collection的子类。我们可以读Collection的元素,但是不能写。

The key to understanding why this trick works is rather simple: if you can only take items from a collection, then using a collection of Strings and reading Objects from it is fine. Conversely, if you can only put items into the collection, it's OK to take a collection of Objects and put Strings into it: in Java we have List a supertype of List.
ListList父类型 。你可以把String写进去。

The latter is called contravariance, and you can only call methods that take String as an argument on List (e.g., you can call add(String) or set(int, String)), while if you call something that returns T in List, you don't get a String, but an Object.
使用List时,可以如add(String)set(int, String)。如果去取一个元素,得到的是Object而不是String

Joshua Bloch calls those objects you only read from Producers, and those you only write to Consumers. He recommends: "For maximum flexibility, use wildcard types on input parameters that represent producers or consumers", and proposes the following mnemonic:
生产者中读取,向消费者写入
厂家卖东西,消费者买东西。

PECS stands for Producer-Extends, Consumer-Super.
生产者:Collection可以读取。
消费者:List 可以写入。

NOTE: if you use a producer-object, say, List, you are not allowed to call add() or set() on this object, but this does not mean that this object is immutable: for example, nothing prevents you from calling clear() to remove all items from the list, since clear() does not take any parameters at all. The only thing guaranteed by wildcards (or other types of variance) is type safety. Immutability is a completely different story.
厂家List只卖东西--读取,不代表它就是不可变的,它也可以被清空clear(),因为这个方法并不需要参数。variance机制确保的是类型安全,不可变是另一回事儿。

Declaration-site variance

Suppose we have a generic interface Source that does not have any methods that take T as a parameter, only methods that return T:

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

Then, it would be perfectly safe to store a reference to an instance of Source in a variable of type Source – there are no consumer-methods to call. But Java does not know this, and still prohibits it:

// Java
void demo(Source strs) {
  Source objects = strs; // !!! Not allowed in Java
  // ...
}
 
 

To fix this, we have to declare objects of type Source, which is sort of meaningless, because we can call all the same methods on such a variable as before, so there's no value added by the more complex type. But the compiler does not know that.
Source是生产者,它卖东西,你可以读取。
Source就是Source的子类,因此可以子类赋值给父类:
Source obj = Source str

In Kotlin, there is a way to explain this sort of thing to the compiler. This is called declaration-site variance: we can annotate the type parameter T of Source to make sure that it is only returned (produced) from members of Source, and never consumed. To do this we provide the out modifier:
out 就是厂家,只卖东西,你可以读数据。
Source是Source的子类。

interface Source {
    fun nextT(): T
}

fun demo(strs: Source) {
    val objects: Source = strs // This is OK, since T is an out-parameter
    // ...
}

The general rule is: when a type parameter T of a class C is declared out, it may occur only in out-position in the members of C, but in return C can safely be a supertype of C.
C, C是父类, C是子类。

In "clever words" they say that the class C is covariant in the parameter T, or that T is a covariant type parameter. You can think of C as being a producer of T's, and NOT a consumer of T's.
C就是C生产T。

The out modifier is called a variance annotation, and since it is provided at the type parameter declaration site, we talk about declaration-site variance. This is in contrast with Java's use-site variance where wildcards in the type usages make the types covariant.

In addition to out, Kotlin provides a complementary variance annotation: in. It makes a type parameter contravariant: it can only be consumed and never produced. A good example of a contravariant type is Comparable:
in 是消费者,消费者买东西。你可以写数据。
Comparable父类, Comparable子类。
父类可以赋值给子类
子类=父类

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

fun demo(x: Comparable) {
    x.compareTo(1.0) // 1.0 has type Double, which is a subtype of Number
    // Thus, we can assign x to a variable of type Comparable
    val y: Comparable = x // OK! 父类赋值给子类
}

We believe that the words in and out are self-explaining (as they were successfully used in C# for quite some time already), thus the mnemonic mentioned above is not really needed, and one can rephrase it for a higher purpose:

The Existential Transformation: Consumer in, Producer out! :-)

Type projections

Use-site variance: Type projections

It is very convenient to declare a type parameter T as out and avoid trouble with subtyping on the use site, but some classes can't actually be restricted to only return T's! A good example of this is Array:

class Array(val size: Int) {
    fun get(index: Int): T { ... }
    fun set(index: Int, value: T) { ... }
}

This class cannot be either co- or contravariant in T. And this imposes certain inflexibilities. Consider the following function:

fun copy(from: Array, to: Array) {
    assert(from.size == to.size)
    for (i in from.indices)
        to[i] = from[i]
        **写=读**
}

This function is supposed to copy items from one array to another. Let's try to apply it in practice:

val ints: Array = arrayOf(1, 2, 3)
val any = Array(3) { "" } 
copy(ints, any)
//   ^ type is Array but Array was expected

Here we run into the same familiar problem: Array is invariant in T, thus neither of Array and Array is a subtype of the other. Why? Again, because copy might be doing bad things, i.e. it might attempt to write, say, a String to from, and if we actually passed an array of Int there, a ClassCastException would have been thrown sometime later.

Then, the only thing we want to ensure is that copy() does not do any bad things. We want to prohibit it from writing to from, and we can:
from定义为out,它只卖东西。你只可以读它。

fun copy(from: Array, to: Array) { ... }

What has happened here is called type projection: we said that from is not simply an array, but a restricted (projected) one: we can only call those methods that return the type parameter T, in this case it means that we can only call get(). This is our approach to use-site variance, and corresponds to Java's Array, but in a slightly simpler way.

You can project a type with in as well:

fun fill(dest: Array, value: String) { ... }

Array corresponds to Java's Array, i.e. you can pass an array of CharSequence or an array of Object to the fill() function.
可以Array = Array, 子=父

Star-projections

Sometimes you want to say that you know nothing about the type argument, but still want to use it in a safe way. The safe way here is to define such a projection of the generic type, that every concrete instantiation of that generic type would be a subtype of that projection.

Kotlin provides so called star-projection syntax for this:

  • For Foo, where T is a covariant type parameter with the upper bound TUpper, Foo<*> is equivalent to Foo. It means that when the T is unknown you can safely read values of TUpper from Foo<*>.
  • For Foo, where T is a contravariant type parameter, Foo<*> is equivalent to Foo. It means there is nothing you can write to Foo<*> in a safe way when T is unknown.
  • For Foo, where T is an invariant type parameter with the upper bound TUpper, Foo<*> is equivalent to Foo for reading values and to Foo for writing values.

If a generic type has several type parameters each of them can be projected independently. For example, if the type is declared as interface Function we can imagine the following star-projections:

  • Function<*, String> means Function;
  • Function means Function;
  • Function<*, *> means Function.

Note: star-projections are very much like Java's raw types, but safe.

Generic functions

Not only classes can have type parameters. Functions can, too. Type parameters are placed before the name of the function:

fun  singletonList(item: T): List {
    // ...
}

fun  T.basicToString(): String {  // extension function
    // ...
}

To call a generic function, specify the type arguments at the call site after the name of the function:

val l = singletonList(1)

Type arguments can be omitted if they can be inferred from the context, so the following example works as well:

val l = singletonList(1)

Generic constraints

The set of all possible types that can be substituted for a given type parameter may be restricted by generic constraints.

Upper bounds

The most common type of constraint is an upper bound that corresponds to Java's extends keyword:

fun > sort(list: List) {  ... }

The type specified after a colon is the upper bound: only a subtype of Comparable may be substituted for T. For example:
T只能是定义它时的泛型约束的子类。

sort(listOf(1, 2, 3)) // OK. Int is a subtype of Comparable
sort(listOf(HashMap())) // Error: HashMap is not a subtype of Comparable>

The default upper bound (if none specified) is Any?. Only one upper bound can be specified inside the angle brackets. If the same type parameter needs more than one upper bound, we need a separate where-clause:
T有多个上限约束时

fun  copyWhenGreater(list: List, threshold: T): List
    where T : CharSequence,
          T : Comparable {
    return list.filter { it > threshold }.map { it.toString() }
}

The passed type must satisfy all conditions of the where clause simultaneously. In the above example, the T type must implement both CharSequence and Comparable.
必须同时满足。

Type erasure

The type safety checks that Kotlin performs for generic declaration usages are only done at compile time. At runtime, the instances of generic types do not hold any information about their actual type arguments. The type information is said to be erased. For example, the instances of Foo and Foo are erased to just Foo<*>.

Therefore, there is no general way to check whether an instance of a generic type was created with certain type arguments at runtime, and the compiler prohibits such is-checks.

Type casts to generic types with concrete type arguments, e.g. foo as List, cannot be checked at runtime.
These unchecked casts can be used when type safety is implied by the high-level program logic but cannot be inferred directly by the compiler. The compiler issues a warning on unchecked casts, and at runtime, only the non-generic part is checked (equivalent to foo as List<*>).

The type arguments of generic function calls are also only checked at compile time. Inside the function bodies, the type parameters cannot be used for type checks, and type casts to type parameters (foo as T) are unchecked. However, reified type parameters of inline functions are substituted by the actual type arguments in the inlined function body at the call sites and thus can be used for type checks and casts, with the same restrictions for instances of generic types as described above.

你可能感兴趣的:(Generics 泛型)