泛型的由来和作用
有许多原因促成了泛型的出现,而最引人注意的一个原因,就是为了创建容器类(集合类)—— 《Java编程思想 (4)》
通常情况的类和参数,开发者只需要使用具体类型的即可 —— 基本类型和自定义的类。
在集合类的场景下,需要编写可以应用于多种类型的代码,如果针对每种类型都写一套代码,那么代码的复用率很低,抽象也没有做好 —— 为什么不把“类型”也抽象成参数呢?
Java5引入的泛型机制实现了“参数化类型”,将原来的具体类型 参数化,将类型定义成参数。
- 泛型提升了代码的抽象程度,提高了代码的复用率;
- 泛型的优点是让编译器追踪参数类型,执行类型检查和类型转换,避免出错。
泛型可以用来限制集合类持有的对象类型,使得类型更加安全。当在集合中放入错误的类型对象时,编译器会报错。
有了泛型,可以开发出更强大、更安全的类型检查,无须手工进行类型转换,并且可以开发出更加通用的代码。
使用泛型
泛型接口
/**
* 泛型接口,
* 类型参数放在接口名后面 <T>
*/
interface Generator<T> {
// 函数中使用类型 T
operator fun next() :T
}
// 测试
fun testGenerator(): Unit {
// 对象表达式
// 使用 object 关键字声明接口Generator的实现类
val gen= object : Generator<Int> {
// 在lambda表达式中 实现 next()函数
override fun next(): Int {
return Random().nextInt(100)
}
}
println(gen.next())
println(gen.next())
println(gen.next())
}
泛型类
/**
* 声明一个带类型参数的类
*/
class Container<K, V>(var key: K, var value: V) {
override fun toString(): String {
return "Container{key=$key, value=$value}"
}
}
// 测试
fun testContainer(): Unit {
val c1 = Container<Int, String>(12, "XYZ") // 具体化<Int, String>
println(c1)
val c2 = Container("UID", 6.66) // 类型推断,省略<String,Double>内容
println(c2)
}
泛型函数
在 函 数 中 直 接 声 明 泛 型 参 数 在函数中直接声明泛型参数 在函数中直接声明泛型参数。
声明泛型函数
// 函数 1
fun isEqualInt(a: Int, b: Int): Boolean = a==b
// 函数 2
fun isEqualStr(a: String, b: String): Boolean = a==b
函数 1、2,除了函数名、参数类型外,函数体内容、返回值类型都是一样的,
这样的写法代码复用率很低,可以采用泛型函数
来优化:
fun <T> isEqual(a: T, b: T): Boolean = a==b
<T>
是声明类型参数,T
是类型参数。
泛型中的类型参数,可以是小写或大写的字母,一般情况建议使用大写英文字母。
多类型参数
多个泛型类型参数之间使用逗号“,”隔开。
fun <X,Y,Z> logs(a: X, b: Y, c: Z) = println("log info {$a, $b, $c}")
// 测试
fun testLogsF(): Unit {
logs("Jack", 18, "student") // 输出:log info {Jack, 18, student}
logs("Lucy", 19, 168.5) // 输出:log info {Lucy, 19, 168.5}
}
logs()
函数定义了3种类型,X,Y,Z。
泛型约束
函数isEqual()
存在一个问题,不是所有类型的T
都具有可比性,最好限定T
的类型范围。
// 限定T的类型范围 —— T 必须都是继承自接口Comparable<T> 的类型.
fun <T :Comparable<T>> isEqualPro(a: T, b: T): Boolean = a==b
这里的Comparable
是类型T
的上界,类型参数T
代表的是实现了Comparable
接口的类。
可空类型参数
泛型函数声明中,参数类型如果没有泛型约束
,则表示函数可以接收任何类型的数据作为参数,包含可空和非空数据。
fun <T> isEqual(a: T, b: T): Boolean = a==b
fun testIsEqual(): Unit {
isEqual(2,9)
isEqual(null, 7)
}
如果不想接收空类型的数据,可以使用Any
作为约束条件:
// 采用Any作为约束条件,限制函数只能接收非空数据
fun <T: Any> isEqualWithoutNull(a: T, b: T): Boolean = a==b
协变与逆变
Java和Kotlin 的协变
、逆变
Java中:
List<Integer> ints = new ArrayList<>();
List<Object> objs = ints; // 这里会报错,无法编译
Object
是 Integer
的父类型,Object
类型大于Integer
;
Java中不支持型变,不能将 List<Integer>
赋值给 List<Object>
.
- 使用
上界通配符
:<? extends T>
List<Integer> ints = new ArrayList<>();
List<? extends Object> objs = ints; // 这里可以编译通过
Object
是 Integer
的父类型,Object
类型大于Integer
;
使用上界通配符
后,List<? extends Object>
的类型就大于 List<Integer>
的类型了,就实现了协变
。
协变
的定义:
如果类型 A大于B,经过一个变化trans后得到的 trans(A) 也是大于trans(B) 的,那么称之为协变。
- 使用
下界通配符
:<? super T>
List<Object> oList = new ArrayList<>();
List<? super Integer> iList = oList; // 这里可以编译通过
Object
是 Integer
的父类型,Object
类型大于Integer
;
List<? super Integer>
类型大于 List<Object>
,类型关系反转了,这里实现了逆变
。
逆变
的定义:
如果类型 A大于B,经过一个变化trans后得到的 trans(A) 小于trans(B) ,那么称之为逆变。
对应的在Kotlin中:
val ints = ArrayList<Int>()
val objs: ArrayList<Any> = ints // 编译不通过
增加 out
关键字:
val ints = ArrayList<Int>()
val objs: ArrayList<out Any> = ints // 这里可以编译通过
这里实现了协变。
in
关键字:
val oList = ArrayList<Any>()
val iList: ArrayList<in Int> = oList // 这里可以编译通过
这里实现了逆变。
Java通配符与Kotlin投射类型
Java的泛型使用到了通配符,用 ? extends T
指定类型参数的上界,用 ? super T
指定类型参数的下界;
Kotlin中没有了这个通配符,使用投射类型(projected type)来实现与Java通配符相同的功能。使用 out T
指定类型参数的上界,代表生产者对象;使用 in T
指定类型参数的下界,代表消费者对象.
看一段代码:
// java.util.Collections 的 copy() 函数
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
}
这段代码中,List<? super T> dest
是消费数据的对象,数据会被写入 dest 对象中,它保证写入数据时类型安全,在kotlin中用 in T
标记;
List<? extends T> src
是生产者,提供数据的对象,它保证读取时类型安全,在kotlin中用 out T
标记;
out T
相当于? extends T
in T
相当于? super T