范型

定义

class A<T>(t:T){var a: Tinit {this.a = t}
}
......
var aa = A("kotlin")
println(aa.a) // 打印 kotlin

其中 A<String>("kotlin")可以根据类型推断简写写成 A("kotlin")

协变、逆变

所谓协变、逆变是指一个对象被意外地降级为子类/升级为父类。特别是集合操作中,很容易出现此类问题。

在 Java 中,List<String> 并不是 List<Object> 的子类,因此不能把 List<String> 赋值给 List<Object>

但是对于数组来说则不同,因为数组天然支持协变。看如下代码:

Object[] arr = new String[]{"aaa","bbb"};
arr[0] = new Object();

第一句由于数组天然支持协变,所以 String 数组属于 Object 数组子类,因此把一个 String 数组赋给 Object 数组是可行的。

但对于第二句,arr[0] 实际上指向的是一个 String,将一个 Object 赋值给他,实际上是将父类赋给了子类——在面向对象中这是绝对不行的。这实际上会导致一个运行时异常。但 Java 在编译时却不能检查出来,在编译器看来,Object 数组中的元素自然也是 Object,将一个 Object 对象赋值给他自然是行得通的。但是在实际使用这个数组的过程中,由于数组这种允许协变的特性,有可能会导致它的元素被意外降级为子类(比如将一个 String 数组赋值给它,那么它所有的元素实际上都被降级为 String ),因此协变的出现对 Java 来说是一个威胁。

为了解决此问题,Java 提供了 ?通配符:

List<? extends Object> list // 表示元素类型包括 Oject 及其子类。

因此List<String>List<? Extends Object> 的子类,而非List<Object>的子类。List<? Extends Object>这样的范型限制了类型的上界。当我们向 List 中放入一个 String 元素后再读去这个元素,这个元素缺只能被当成 Object 看待,这种情况称之为协变。

与此相反,是逆变的概念:

List<? super String> // 其元素类型只能是 String 及其父类

当你向这个 List 中写入对象时,不仅仅可以放入 String 对象,还可以放入 String 的父类对象,比如 Object。

Java 通过在读取时使用<? extends E> ,在写入时使用<? super E>来实现协变和逆变。

总结一下,所谓协变逆变,都是范型约束的一种泛化,协变是向下(到子类)的泛化,逆变是向上(到父类)的泛化。

如果一个范型被指定为允许协变,表示这个范型可以和子类兼容,反之,逆变则允许和父类兼容。

在Kotlin 中,用 out 关键字来解决协变的问题:

class A<out T>(t: T) { private var t: Tinit{this.t = t}fun get(): T = this.t
}

这里, 表示该范型仅仅用于读取(get 方法),因此它将允许向下协变,即可以涵盖 T 及其子类。比如:

var aa:A<Any> = A<String>("abc") // 虽然传入的是 String,但是可以当成 Any,当然读出来的时候已经变成 Any 而不是 String

虽然 aa 被定义为 A 类型,但是我们把一个 A 赋值给它也是没问题的,因为 String 是 Any 的子类。

类似地,in 关键字用于解决逆变问题:

class B<in T>(t: T) {private var t: Tinit{this.t = t}fun set(t: T){this.t = t}
}

表示该范型仅仅用于写入(set 方法),因此它允许向上逆变,即可以涵盖 T 及其父类:

var aa:A<Int> = A<Number>(3) // 虽然传入的是 Number, 但是可以当成 Int

虽然 aa 被定义为 A 类型,但是我们把一个 A 赋值给它,因为 Number 是 Int 的父类。

协变只能用于读,逆变只能用于写

请看如下 java 代码:

           List<Cat> cats = new ArrayList<>();cats.add(new Cat());List<? extends Animal> animals = cats;Animal animal = animals.get(0); // 允许读取到 AnimalSystem.out.println(animal); // 打印出来的实际是 com.Cat@66d3c617,但你仍然无法赋值给 Cat 对象animals.add(new Animal());

这时编译器在最后一句报错: capture<? extends com.Animal> cannot be applied to com.Cat

这说明,协变只允许读取,不允许写入(比如 add)。

这其中的道理很容易理解,因为 animals 定义为List<? extends Animal> 它可以在运行时被赋值为 List、List…因为它们都是List<? extends Animal> 的子类。 如果像代码中一样,animals 被一个 List 赋值,那么它里面的元素就应该是 Cat 对象,如果 java 允许对元素进行赋值,那么很可能会错误地把一个 Animal 对象赋给一个 Cat 元素,这在 Java 中是绝对不允许的(在 java 中,将父类赋值给子类对象是不允许的,反之可以)。为了防止这类错误发生,编译器直接禁止了对协变的类型进行 set 操作。

逆变则相反:

List<Animal> animals = new ArrayList<>();
List<? super Cat> cats = animals;
cat.add(new Cat());
Cat cat = cats.get(0);

此时编译器报错: Required: com.Cat, Found: capture<? super com.Cat>,说明逆变不允许读取,但可以写入(比如 add)。

我们打印 get(0) 得到的对象类型,确实就是 Cat:

System.out.println(cats.get(0)); // 打印:com.study.Cat@66d3c617

但编译器不允许对 cats 进行读取——准确地说,不允许读取到一个 T 类型的变量中,如果读取到 Object 对象是没问题的:

Object cat = cats.get(0);

这其中的道理不难理解,因为 cats 的定义是List<? super Cat> ——集合中的元素可能是 Cat 及其父类类型,那么当你从中读取一个元素再赋给一个 Cat 对象时,读取的元素很可能是一个 Animal,在 java 中,将父类赋值给子类对象是不允许的(反之可以)。

out 允许范型降级

Kotlin 通过 out 关键字允许协变,或范型降级(即声明范型时是某个类型,但通过某些手段可以将它降级为声明时类型的子类):

class A<out T>(private val value: T) { // 1
}
... ...
val aa = A("out sample") // 2
val aaa: A<Any> =  aa // 3
  1. class A 有一个允许协变的范型 T(用 in 关键字),因此这个 T 允许降级,即可以将子类赋值给父类。
  2. aa 是一个 A 类型(通过类型推断)。
  3. aaa 是一个 A 类型,由于允许协变,虽然 T 为 Any,但我们可以将 Any 的子类 String 赋值给 T。这样,实际上 T 从 Any 被降级为 String 了。因为这中操作限制了写入操作,你实际上不可能将 value 读取到一个 String 变量中了(编译器报错)。

out 用作返回类型

如果一个范型被定义为 out ,那么这个范型可以作为方法返回类型,但不能作为方法的参数类型,这点和 out 的字面意义是一致的。反过来,如果你发现一个范型仅仅是充当方法的返回类型使用,那么我们可以将该范型定义为 out。

in 允许范型升级

与此相反,in 关键字允许逆变,即将范型升级为父类。

class B<in T> { // 1fun toString(value: T): String {return value.toString() // 2}
}
... ...
val bb = B<Number>() // 3
val bbb: B<Int> = bb // 4
assertTrue(bbb is B<Int>) // 5
  1. class B 有一个允许逆变的范型 T(范型升级),使用 in 关键字。
  2. 逆变只能用于写,不能用于读,因此直接返回 value 是不行的(因为它是 T 类型),但返回 value 的 toString 方法是可以的(它不再是 T 类型,而是 String 类型)。
  3. bb 是一个 B 类型。
  4. 逆变允许升级,虽然 bbb 在声明时指定 T 为 Int,但是可以将 Int 的父类 Number 赋值给 T,实际上 T 是被升级了。
  5. 虽然 T 被升级,但它仍然还是定义时的 Int。因此断言通过。

in 用作参数类型

如果一个范型被定义为 in ,那么这个范型可以作为方法参数类型,但不能作为方法的返回类型,这点和 in 的字面意义是一致的。反过来,如果你发现一个范型仅仅是充当方法的参数类型使用,那么我们可以将该范型定义为 in。

范型不能同时修饰 out 和 in

同时,不能在同一个范型上既使用 out 又使用 in。如果一个范型既要作为方法的参数类型,又要作为方法的返回类型,那么这个范型不使用任何关键字修饰(这被称之为不变)。

out 和 in 本质上是多态

多态中规定,子类实例可以赋值给一个父类的引用。

这在于协变很好理解——因为它本就是范型降级,逆变则比较难于理解了,让我们举一个逆变的例子说明:

Interface Consumer<in T> {fun consumer(item: T)
}

Consumer 接口定义了一个范型 in T,表示这是一个逆变范型,即允许声明 T 时用子类声明,但使用时指定一个父类。同时,在方法中使用了范型 T。

然后定义两个类:

class Human:Consumer<Fruit> {override fun consume(item: Fruit){println("Consume Fruit")}
}
class Man: Consumer<Apple> {override fun consumer(item:Apple){println("Consume Apple")}
}

Human 和 Man 之间没有继承关系。Human ''消费" 的是 Fruit,Man “消费” 的是 Apple。这里的消费即指方法所接收的参数。则我们可以这样做:

val consumer1: Consumer<Fruit> = Human()
val consumer2: Consumer<Apple> = Human()

consumer1 声明的时候 T 是 Fruit,使用的时候是也是 Fruit(查看 Human 的定义),声明和使用一致,即所谓的不变。这是没有任何问题的。

consumer2 声明的时候 T 是 Apple,但使用的时候却是 Fruit(Fruit 是 Apple 的父类),T 在使用时中被升级了,这就是所谓的逆变。多态中规定,子类实例可以赋值给父类引用。但这里明明是将一个父类赋值给了子类,怎么解释?

但是本质上这仍然是多态,consumer2 将 T 声明为 Apple,则你在调用 consume 方法时必然只能传递一个 Apple 参数给它(否则编译器不通过):

consumer2.consume(new Apple())

但是 consumer2 实际上是一个 Human,因此调用的是 Human 的 consume(item: Fruit) 方法。那么相当于是把一个 Apple 对象传给了 Fruit 参数,这不正是把子类实例(Apple)赋值给父类引用(Fruit)吗?符合多态的定义。

使用处协变(类型投影)

前面提到 kotlin 是声明处协变,Java 是使用处协变。但实际上 kotlin 也支持使用处协变(又称类型投影)。声明处协变的限制在于它实际上对范型进行了约束,对于声明为 out 的范型,你在使用时就只能作为方法返回值而不能作为参数,这一点是不现实的。

以 Array 为例,Array 声明时即没有使用 out 也没有使用 in 修饰,这是因为 T 既在返回类型时使用(get 方法),也在参数中使用(set方法)。

在这种情况下,Array 有可能产生使用处协变。

fun copy(from: Array<Any>, to: Array<Any>) {assertTrue(frome.size == to.size)for(i in from.indices) {to[i]=from[i]}
}
fun main(args: Array<String>){val from:Array<Int> = arrayOf(1,3,5,6)val to:Array<Any> = Array<Any>(4, {"hello"+it})for(item in to){ println(item)// "hello0","hello1","hello2","hello3"}copy(from, to)
}

在 copy(from, to) 处出现警告, from 的类型不匹配:定义的是 Array 但是提供了一个 Array。虽然 Int 是 Any 的子类,但 Array 并不是 Array 的子类。

我们可以将 copy 方法中的 from 参数重定义为 Array ,这样编译就可以通过了。这就是使用时协变。它和声明时协变的区别在于,它不是在定义该范型的地方使用 out,而是在实例化的地方使用 out:

// 范型声明
Consumer<in T>
// 范型实例化
Array<out Any>

范型声明,是声明某个类型不确定,使用一个占位符来代替这个类型,你可以用任意名字,比如 T 或 S (只要符合明明规范)来暂代这个类型名,这个占位符,我们可以称之为“范型变量”。而范型的实例化,则是指将某个类型赋值范型变量,类似于 T = Any 这样。使用处协变,就是指在类型名前使用 out 关键字,声明处协变,就是指在范型变量名前使用 out 关键字。

因此 out 不仅仅可以修饰范型变量, 也可以修饰某个类型。

此外,使用处协变仍然受到和声明时协变一样的限制,即可以读不能写,比如:

for (i in from.indices) {to[i] = from[i]from[i] = i // 编译错误
}

编译器将在 from[i] 处报错,因为 from 中的元素是 类型,你不能向< out Any> 进行写入操作。但是对 to[i] 进行写入操作就没事:

to[i] = from[i]

因为 to 中的元素是 类型,没有 out 的限制。

本质上讲,使用处协变仍然是通过类型约束来保证范型的安全性。

使用处逆变

类型投影还包括另外一种情况,即使用处逆变。

fun setValue(to:Array<String>, index: Int, value:String){to[index] = value
}
fun main(args:Array<String>){val array:Array<Any> = Array<Any>(4, {it->"hello"+it})for(item in array){println(item) // "hello0","hello1","hello2","hello3"}setValue(array, 1, "world") // 编译错误
}

此时 setValue(array, 1, “world”) 一句报错,依然是 array 类型不匹配,需要 Array,但提供了 Array。解决的办法是使用使用处逆变,在 setValue 方法第一个参数处使用 in:

fun setValue(to:Array<in String>, index: Int, value: String)

道理和使用处协变是一样的。限制仍然是允许对 to 进行写入操作,但不能读取。

星投影

星投影其实指的是<*>。在范型协变中,<*>可以用于表示,在范型逆变 中,<*> 可以用于表示 。Nothing 在 kotlin 中表示这个类型没有任何实例,因此不能向其中写入任何值。

class Star<out T> {}
class Star2<in T> {fun setValue(t:T){}
}
fun main(args: Array<String>) {var star: Star<Number> = Star<Int>()var star2: Star<*> = star          // 1var star3: Star2<Int> = Star2<Number>()var star4: Star2<*> = star3          // 2star4.setValue(3)           // 3 编译错误
}
  1. star2 的类型为 Star<*> 表示范型 T 的类型不确定,但将 star 赋值给它后,T 可以依靠类型推断。因为 val star: Star,T 的上界(Tupper)就是 Number,因此将 star 赋值给 star2 之后,Star<*>就相当于 Star。
  2. star4 的类型为 Star2<*> 表示范型 T 的类型不确定,依靠 star3 的类型推断。因为 var star3: Star2 ,因此 Star2<*>就相当于 ,类型未知。
  3. 因为 不能写入任何类型,因此这句报错。但是将 star4 改成 star3 之后,就可以了。

如果对于不变的范型(没有 in 也没有 out 修饰),星投影<*>又会怎么样呢?看一个例子:

class Star3<T>(private var t:T) {fun setValue(t:T){}fun getValue():T{ return this.t }
}
fun main(args:Array<String>){var star5: Star3<String> = Star3<String>("hell0")var star6: Star3<*> = star5star6.getValue()star6.setValue("whatever") // 编译错误
}

star6.setValue("whatever")一句编译器报错,这是因为:

如果在一个不变范型上使用星投影,那么它等于是+。

具体到 star6 上来说,相当于 和 ,自然是不能写到。

再来看一个例子:

val list:MutableList<*> = mutableListOf("1","2","3")
list[0] = "4" // 编译错误

道理是同样的。

@UnsafeVariance

正常情况下,以下代码中,setValue 方法是不能通过编译的:

class MyClass<out T>(private var t:T) {fun getValue(): T {return this.t}fun setValue(t: T) { this.t = T}
}

因为 T 是 out, 不能用于方法参数,但通过 @UnsafeVariance 注解,可以突破编译器的这种限制:

fun setValue(t: T) { this.t = T}

这是因为范型擦除的缘故。

范型擦除

何谓范型擦除,看以下代码:

var myClass1: MyClass<Int> = MyClass(5)
var myClass2: MyClass<Any> = myClass1
println(myClass2.getValue())
myClass2.setValue("hello")
println(myClass2.getVlaue)

myClass1 的类型是MyClass,它只能存放 Int,但是当我们将它赋值给一个 MyClass 之后,就可以向其中放任何东西了。这是因为从字节码的层级,范型的真实类型其实被丢失了(范型擦除),不管什么类型对它来说统统都是 Object。

范型方法

不管类是不是范型的,它的函数都可以使用范型:

fun <T> getValue(item: T): T {return item
}

getValue 函数时一个顶层函数,调用时可以这样调用它:

val item = getValue<Int>(100) // 根据类型推断,可以简写成:getValue(100)
println(item)

范型约束

class MyClass<T: List<T>> { // 范型约束:T 只能是 List 或 List 的子类
}

如果不指定范型约束,那么就是 Any?。

多个约束通过 where 关键字定义(同 swift):

class MyClass<T> where T:Comparable<T>, T: Any {}

【深入kotlin】 - 范型相关推荐

  1. Typescript之 范型

    范型 typescript在javascript基础上扩充了类型,并且可以进行静态类型检查.它在某种成都上限制javascript的灵活性,但是这种限制是必要的,在类型体系内提供灵活性,才是可控的.范 ...

  2. java数组的协变_Java数组协变与范型不变性

    变性是OOP语言不变的大坑,Java的数组协变就是其中的一口老坑.因为最近踩到了,便做一个记录.顺便也提一下范型的变性. 解释数组协变之前,先明确三个相关的概念,协变.不变和逆变. 一.协变.不变.逆 ...

  3. Generic Data Access Objects -范型DAO类设计模式

    Generic Data Access Objects 普通数据访问对象,这个是Hibernate官方网站上面的一个DAO类的设计模式,基于JDK5.0范型支持,文章地址如下: http://www. ...

  4. 使用范型观察者模式观察多个数据的实现

    观察者模式是最灵活.最多变的一种模式.在现实开发中,我常常会遇到观察者很多而且观察的数据也各不相同的情况,如果采用经典的观察者实现方法,在观察者的Update方法中难免要传递Subject中自己并不关 ...

  5. ?通配符 以及扩展通配符在范型中的应用。。。。。。。。。。。。。。。。。。...

    一.通配符 ?标识的范型化对象,可以标识任意类型的范型化   ,可以将任意类型化的值赋值给 ?通配符所规范化的类.  可以将任意类型的范型化类型  赋值给?通配符范型化的类型 . Collection ...

  6. Java 数组转型和范型

    今天写代码遇到一个奇怪的问题,代码结构如下: [java] view plaincopy print? ArrayList<String> list = new ArrayList< ...

  7. “主要的编程范型”及其语言特性关系(多图)

    "主要的编程范型"(The principal programming paradigms)这幅图,其实出现得不算早,作者在2007年完成了该图的1.0版,到2008年更新至v1. ...

  8. C++ Primer 第十六章 模板与范型编程

    16.1 模板定义     模板和c#范型一样,建立一个通用的类或函数,其参数类型和返回类型不具体指定,用一个虚拟的类型来代表,通过模板化函数或类实现代码在的重用.     定义语法是:    tem ...

  9. 分析 C# 2.0 新特性 -- 范型(Generics)

    分析 C# 2.0 新特性 -- 范型(Generics) 作者:梁振[MS-MVP]   范型是提高面向对象程序多态性设计衍生的. 1,C# 多态性设计回顾和展望 在引入范型这个概念之前,回顾一下1 ...

最新文章

  1. 上海高考听说测试什么软件,2021上海市高考外语听说测试模拟系统使用方法及注意事项...
  2. 关于C# WinForm中进度条的实现方法
  3. 使用docker搭建gitlab服务器
  4. MATLAB从入门到精通-以实例的形式带你玩转Matlab三角函数
  5. 一些 Linux 系统故障修复和修复技巧
  6. 手机MODEM开发(31)---LTE 速率低
  7. 为用户设计的产品,就应该用用户熟悉的语言
  8. Python 标准库 —— urllib(下载进度)
  9. MySQL 索引背后的数据结构及算法原理
  10. Netty websocket 推送数据压缩以 js解压
  11. SRCNN-pytoch代码讲解
  12. 开心问答—首个基于迅雷链智能合约上执行的问答游戏
  13. 如何在宝塔面板中屏蔽垃圾蜘蛛?
  14. 光衰高怎么办_灯太亮了怎么办 led灯该如何选择
  15. 【高级持续性威胁追踪】SolarWinds供应链攻击持续跟踪进展
  16. Altium Designer 入门及环境配置
  17. 用好这28个工具,开发效率爆涨
  18. 亚马逊测评提升销量有什么好办法,分享6点技巧
  19. 【二维码识别】灰度+二值化+校正二维码生成与识别【含GUI Matlab源码 635期】
  20. uboot中 使用i2c

热门文章

  1. Java锁--Lock实现原理(底层实现)
  2. DSFD : Dual Shot Face Detector [CVPR 2019]
  3. python 带账号密码的爬取
  4. GELU()更适合NLP任务的激活函数
  5. 双机调试环境搭建 windbg + virtualkd
  6. Anaconda 安装第三方包Freecad
  7. Postman打开一直转圈
  8. 计算机操作员职业资格培训教程(中级),计算机操作员(中级国家职业技能鉴定考试指导国家职业资格培训教程配套辅导练习)...
  9. phpcms 模块开发(一)
  10. html怎么使用visible属性,enable与visible属性