变型:泛型和子类型化
变型概念描述了拥有相同基础类型和不同类型实参的(泛型)类型之间是如何关联的
为什么存在变型:给函数传递实参
一个接收List作为实参的参数,可以把List<String> 传入,这样是安全的,因为String继承了Any。但当String和Any变成List接口的类型实参后就不一样了
当期望的是MutableList<Any> 的时候把一个MutableList<String> 当作实参传递是不安全的。如果函数添加或替换了列表中的元素就是不安全的,因为这样会产生类型不一致的可能性。在kotlin中可以通过列表是否可变选择合适的接口来轻易控制,当它是可读列表可以传递更具体的元素类型的列表,如果是可变的就不行
类、类型和子类型
有时候我们会把类型和类当成同样的概念来使用。在非泛型类中,类的名称可以直接当作类型使用。一个kotlin类都可以用于构造可空和非空类型。在泛型类中,要得到一个合法的类型,需要用一个作为类型实参的具体类型替换(泛型)类的类型实参。List不是一个类型,它是一个类,但List<Int> 这些都是合法的类型。每一个泛型类都可能生成潜在的无限数量的类型
子类型:任何时候如果需要的是类型A的值,你都能够使用类型B的值当作A的值,类型B就称为类型A的子类型,所有类型都是它自己的子类型。
超类型是子类型的反义词
简单的情况下,子类型和子类本质上意味着一样的事物,比如Int类是Number的子类,因此Int类型也是Number类型的子类型。如果一个类实现了一个接口,它的类型就是该接口类型的子类型,比如String是CharSequence的子类型
不一样的情况:一个非空类型是它的可空版本的子类型,但它们都对应着同一个类。你始终能在可空类型的变量中存储非空类型的值,但反过来不行。由于安全性问题,所以我们不能把MutableList<String> 看作是MutableList<Any> 的子类型。
不变型
一个泛型类如MutableList,如果对于任意两种类型A和B,MutableList<A> 既不是MuableList<B> 的子类型也不是它的超类型,它就被称为在该类型参数上是不变型的。java中所有的类都是不变型的
协变:保留子类型化关系
一个协变类是一个泛型类,以Producer为例,对这种类来说,如果A是B的子类型,那么Producer<A> 是Producer<B> 的子类型,我们说子类型化被保留了。比如说Producer<Cat> 是Producer<Animal> 的子类型,因为Cat是Animal的子类型
要声明类在某个类型参数上是可以协变的,在该类型参数的名称前加上out 关键字
将一个类的类型标记为协变的,在该类型实参没有精确匹配到函数中定义的类型实参时,可以让该类的值作为这些函数的实参传递,也可以作为这些函数的返回值。
如果尝试把猫群传递给feedall函数会得到类型不匹配的错误。因为Herd类中的类型参数T没有用任何变型修饰符,此时猫群不是畜群的子类。我们可以使用显式类型转换解决,但这并不是解决类型不匹配问题的正确方式
使用协变解决类型不匹配问题
你不能把任何类都变成协变的,这样不安全。让类在某个类型参数变为协变,限制了该类中对该类型参数使用的可能性。要保证类型安全,它只能用在所谓的out 位置,意味着这个类只能生产类型T的值而不能消费它们
一个约定:一个泛型类或者泛型接口,它的参数列表是接受数据的地方,称它为in 位置,它的返回值是输出数据的地方,称它为out 位置。
类的类型参数前的out 关键字要求所有使用T的方法只能把T放在out位置而不能放在in位置,这个关键字约束了使用T的可能性,但保证了对应子类型关系的安全性
以Herd类为例
这是一个out 位置,可以安全地把类声明成协变的,如果Herd<Animal> 类的get方法返回Cat是可以正常工作的,因为此时Cat是Animal的子类型
out 的两层含义 1.子类型化会被保留(Producer<Cat> 是Producer<Animal> 的子类型) 2.T只能用在out位置
查看List接口
List为只读接口所以只有一个返回类型为E 的get方法,所以可以是协变的
注意!类型形参不仅可以直接当作类型参数或者返回类型使用,还可以当作另一个类型的类型实参
但不能把MutableList<T> 声明成协变的,因为它的T会出现在in和out两个位置上
构造方法的参数既不在in也不在out位置,即使类型参数声明成了out,仍然可以在构造方法参数的声明中使用它。如果把类的实例当成一个更泛化的实例使用,变型会防止该实例被误用,不能调用存在潜在危险的方法,构造方法不是那种在实例创建后还能调用的方法,因此不会有潜在危险
若你在构造方法中使用了val和var关键字,同时就会声明一个getter和settet(若属性可变),此时T不能用out标记,因为类包含属性的setter,它在in位置用到了T
位置规则只覆盖了类外部可见的(public protected internal),私有方法的参数既不在in位置也不在out位置,变型规则只会防止外部使用者对类的误用但不会对类自己的实现起作用
逆变:反转子类型化关系
逆变可看作是协变的镜像:对逆变类来说,它的子类型化关系与用作类型实参的类的子类型化关系是相反的
sortedWith函数期望一个Comparator<String> (一个可以比较字符串的比较器),传给它一个能比较更一般的类型的比较器是安全的。如果你要在特定类型的对象上执行比较,可以使用能处理该类型或者它的超类型的比较器。这说明Comparator<Any> 是Comparator<String> 的子类型,其中Any是String的超类型。不同类型之间的子类型关系和这些类型的比较器之间的子类型化关系截然相反
如果B是A的子类型,那么Consumer<A> 就是Consumer<B> 的子类型。类型参数A和B交换了位置,所以我们说子类型化被反转了,例如Consumer<Animal> 就是Consumer<Cat> 的子类型
in 关键字的意思是,对应类型的值是传递进来给这个类的方法的,并且被这些方法消费。约束类型参数的使用将导致特定的子类型化关系,在类型参数T上的in关键字意味着子类型化被反转了
一个类可以在一个类型参数上协变,同时在另一个类型参数上逆变
kotlin的表示法(P)->R 是表达Function<P,R> 的另一种更具可读性的形式
上图是一高阶函数尝试对所有的猫进行迭代,你可以把一个接收任意动物的lambda传给它,其中Animal是Cat的超类型,Int是Number的子类型
使用点变型:在类型出现的地方指定变型
在类声明的时候就能够指定变型修饰符是很方便的,因为这些修饰符会应用到所有类被使用的地方,这被称作声明点变型。在java中每一次使用带类型参数的类型的时候,还可以指定这个类型参数是否可以用他的子类型或者超类型替换,这叫作使用点变型
声明点变型比java通配符带来了更简洁的代码,因为只用指定一次变型修饰符
kotlin也支持点变型,允许在类型参数出现的具体位置指定变型,即使在类型声明时它不能被声明成协变或者逆变的
对于MutableList这样的接口来说,通常情况下既不是协变也不是逆变的。但是在某个特定函数中制备当成其中一种角色使用的情况却比较常见:要么是生产者,要么是消费者
这个函数从一个集合中把元素拷贝到另一个集合中,尽管两个集合都拥有不变型的类型,来源集合只用于读取,目标集合只用于写入,这种情况下,集合的元素类型不需要精确匹配,比如可以把一个字符串的集合拷贝到可包含任意对象的集合是没有问题的
在这个函数中,来源元素类型应该是目标列表元素的子类型
kotlin提供更优雅的表达方式,当函数的实现调用了那些类型参数只出现在out/in位置的方法时,可以利用这一点在函数定义中给特定用途的类型参数加上变型修饰符
可以为类型声明中类型参数任意的用法指定变型修饰符,用法包括:形参类型、局部变量类型、函数返回类型等。这里发生的一切被称作类型投影。我们说source不是一个常规的MutableList,而是一个投影(受限)的MutableList。只能调用返回类型是泛型类型参数的那些方法,严格地讲,只在out位置使用它的方法
kotlin的使用点变型直接对应java的限界通配符:MutableList<out T> 对应java中的MutableList<? extends T> ,MutableList<in T> 对应Java中MutableList<? super T>
星号投影:使用*代替类型参数
星号投影语法可以用来标明你不知道关于泛型实参的任何信息,比如一个包含未知元素的列表:List<*>
需要注意MutableList<*> 和MutableList<Any?> 不一样,前者是包含某种特定类型,后者是包含任意类型,不能向前者写入任何东西,因为你写入的任何值都可能违反调用代码的期望,但是可以读取
编译器会把MutableList<*> 当成out投影的类型,MutableList<*> 投影成了MutableList<out Any?> 。*也对应了java的通配符?
对像Consumer<*> 这样的逆变类型参数来说,星号投影等价于<in Nothing> ,在这种星号投影中无法调用任何签名中含有T的方法,如果类型参数是逆变的,它就只能表现为一个消费者,但我们不能让它消费任何东西
当类型实参的信息不重要时可以用星号投影的语法
或者引入一个泛型类型参数
例子
不能使用类型为FieldVr<*> 的验证器来验证字符串,因为当我们尝试把具体类型的值传给未知类型的验证器是不安全的。一种修正方法是把验证器显式转换成需要的类型,不过也是不安全的,因为此时得到的验证器键值类型可能和转换类型不一样。我们可以把其进行封装,保证只有正确的验证器被注册和返回
|