《Kotlin核心编程》笔记:可空类型&平台类型&装箱类型&数组类型&泛型&协变与逆变
可空类型
在Kotlin中,我们可以在任何类型后面加上“?
”,比如“Int?
”,实际上等同于“Int? = Int or null
”。
通过合理的使用,不仅能够简化很多判空代码,还能够有效避免空指针异常。
注意:由于null
只能被存储在 Java 的引用类型的变量中,所以在 Kotlin 中基本数据的可空版本都会使用该类型的包装形式。同样,如果你用基本数据类型作为泛型类的类型参数,Kotlin同样会使用该类型的包装形式。(即可空类型会自动装箱)
Java中对于null
的一些解决方案:
- 函数内对于无效值,可以抛异常处理。
- 采用
@NotNull
/@Nullable
标注。 - 使用专门的
Optional
对象对可能为null
的变量进行装箱。
可空类型相关的安全操作符
安全的调用 ?.
s.student?.glasses?.degreeOfMyopia
Elvis操作符 ?:
val result = student.glasses?.degreeOfMyopia ?: -1
又称合并运算符
非空断言 !!
val result = student!!.glasses
类型检查
在Kotlin中,我们可以用“is
”来判断。
if (obj is String) {
print(obj.length)
}
if (obj !is String) { // 等同于 !(obj is String)
print("Not a String")
} else {
print(obj.length)
}
when (obj) {
is String -> print(obj.length)
!is String -> print("Not a String")
}
类型智能转换
Smart Casts 可以将一个变量的类型转变为另一种类型,它是隐式完成的。
val stu: Any = Student(Glasses(189.00))
if(stu is Student) println(stu.glasses)
对于可空类型,我们可以使用 Smart Casts:
val stu: Student = Student(Glasses(189.00))
if (stu.glasses != null) println(stu.glasses.degreeOfMyopia)
我们将这个例子反编译成Java,核心代码如下
...
Intrinsics.checkParameterlsNotNull(args, "args");
Student stu = new Student(new Glasses(189.0D));
if (stu instanceof Student) {
Glasses var2 = ((Student)stu).getGlasses();
System.out.println(var2);
}
...
我们可以看到,这与我们写的Java版本一致,这其实是Kotlin的编译器帮我们做出了转换。
根据官方文档介绍:当且仅当Kotlin的编译器确定在类型检查后该变量不会再改变,才会产生SmartCasts。
利用这点,我们能确保多线程的应用足够安全。举个例子:
class Kot {
var stu: Student? = getStu()
fun dealStu() {
if (stu != null) {
print(stu.glasses)
}
}
}
上述代码中,我们将stu
声明为引用可空类型变量,这意味着在判断 stu != null
之后,stu
在其他线程中还是会被修改的,所以被编译器无情地拒绝了。
将var
改为val
就不会存在这样的问题,引用不可变能够确保程序运行不产生额外的副作用。你也许会觉得这样写不够优雅,我们可以用let
函数来简化?下:
class Kot {
var stu: Student? = getStu()
fun dealStu() {
stu?.let { print(it.glasses) }
}
}
这样就会满足 Smart Casts 的条件,就不会被拒绝编译。
在实际开发中,我们并不总能满足 Smart Casts 的条件。并且 Smart Casts 有时会缺乏语义,并不适用于所有场景。
当类型需要强制转换时,我们可以利用“ as
” 操作符来实现。
class Kot {
val stu: Student? = getStu() as Student?
fun dealStu() {
if (stu != null) {
print(stu.classes)
}
}
}
由于val
只允许赋值一次,这样,我们在外部已经确定了stu
的类型,当stu
不为空时,在dealStu
方法里就可以成功调用stu
的参数。
注意:
- 这里
stu
变量是在dealStu()
方法的外面,必须使用val
生声明,否则如果是var
,仍然可能有线程安全问题所以也会被拒绝编译。 - 如果
stu
变量是放在dealStu()
方法的里面,那么可以使用var
, 因为是方法本地局部变量,会被线程独占,此时也满足 Smart Casts 的条件。
因为getStu
可能为空,如果我们将其转换类型改为 Student
:
val stu: Student? = getStu() as Student
则会抛出类型转换失败的异常,因为它不是可空的。所以,我们通常称之为“ 不安全” 的类型转换。
那是否有安全版本的转换呢?除了上述写法外,Kotlin还提供了操作符“ as?
”,我们可以这样改写:
val stu: Student? = getStu() as? Student
这时,如果stu
为空将不会抛出异常,而是返回转换结果null
。
Any:非空类型的根类型
与 Object
作为 Java 类层级结构的顶层类似,Any
类型是 Kotlin 中所有非空类型(如String
、Int
)的超类:
与 Java 不同的是,Kotlin 不区分“原始类型”(primitivetype)和其他的类型,它们都是同一类型层级结构的一部分。
如果定义了一个没有指定父类型的类型,则该类型将是Any
的直接子类型。如:
class Animal(val weight: Double)
如果你为定义的类型指定了父类型,则该父类型将是新类型的直接父类型,但是新类型的最终根类型为Any
。
另外,Kotlin 把 Java 方法参数和返回类型中用到的 Object
类型看作 Any
(更确切地说是当作“平台类型”)。当在 Kotlin 函数中使用 Any
时,它会被编译成 Java 字节码中的 Object
。
什么是平台类型?
平台类型本质上就是 Kotlin 不知道可空性信息的类型,所有 Java 引用类型在 Kotlin 中都表现为平台类型。当在 Kotlin 中处理平台类型的值的时候,它既可以被当作可空类型来处理,也可以被当作非空类型来操作。
平台类型的引入是 Kotlin 兼容 Java 时的一种权衡设计。试想下,如果所有来自 Java 的值都被看成非空,那么就容易写出比较危险的代码。反之,如果 Java 中的值都强制当作可空,则会导致大量的 null
检查。综合考量,平台类型是一种折中的设计方案。
Any?:所有类型的根类型
如果说Any
是所有非空类型的根类型,那么 Any?
才是所有类型(可空和非空类型)的根类型。
Any
与 Any?
看起来没有继承关系,然而在我们需要用 Any?
类型值的地方,显然可以传入一个类型为Any
的值,这在编译上不会产生问题。比如在 Kotlin 中 Int
是 Number
的子类:
fun printNum(num : Number){
println(num)
}
val n : Int = 1
printNum(n)
反之却不然,比如一个参数类型为Any
的函数,我们传入符合 Any?
类型的null
值,就会出现如下的错误:
error: null can not be a value of a non-null type Any
Nothing 与 Nothing?
在 Kotlin 类型层级结构的最底层是 Nothing
类型。
Nothing
是没有实例的类型。Nothing
类型的表达式不会产生任何值。需要注意的是,任何返回值为Nothing
的表达式之后的语句都是无法执行的。你是不是感觉这有点像return
或者break
的作用?没错,Kotlin 中 return
、throw
等(流程控制中与跳转相关的表达式)返回值都为Nothing
。
Nothing?
是 Nothing
的父类型,其实,它只能包含一个值:null
,本质上与null
没有区别。所以我们可以使用null
作为任何可空类型的值。
自动装箱与拆箱
我们发现,Kotlin 中并没有 int
、float
、double
、long
这样的原始类型,取而代之的是它们对应的引用类型包装类Int
、Float
、Double
、Long
。
除了以上代表数值的类型,还有布尔(Boolean
)、字符(Char
)、字符串(String
)及数组(Array
)。这让 Kotlin 比起 Java 来更加接近纯面向对象的设计——一切皆对象。
但这么说其实也是不够严谨的。以Int
为例,虽然它可以像Integer
一样提供额外的操作函数,但这两个类型在底层实现上存在差异。Kotlin 中的 Int
在 JVM 中实际以 int
存储(对应字节码类型为I
)但是,作为一个“ 包装类型”,编译后应该装箱才对,难道,Kotlin 不会自动装箱?
我们可以简单地认为:
- Kotlin中的
Int
类型等同于int
; - Kotlin中的
Int?
等同于Integer
。
Int
作为一种小技巧,让Int
看起来是引用类型,这在语法上让 Kotlin 更接近纯面向对象语言。
数组类型
val funList = arrayOf() // 声明长度为0的数组
val funList = arrayOf(n1, n2, n3, ..., nt) // 声明并初始化长度为t的数组
Kotlin中 Array
并不是一种原生的数据结构,而是一种Array
类,甚至我们可以将 Kotlin 中的Array
视作集合类的一部分。
由于 Smart Casts,编译器能够隐式推断出funList
元素类型。当然,我们也可以手动指定类型
val funList = arrayOf<T>(n1, n2, n3..., nt)
在 Kotlin 中,还为原始类型额外引入了一些实用的类:IntArray
、CharArray
、ShortArray
等,分别对应 Java 中的int[]
、char[]
、short[]
等。
val x = intArrayOf(1,2,3)
注意:IntArray
等并不是Array
的子类,所以用两者创建的相同值的对象,并不是相同对象。
由于 Kotlin 对原始类型有特殊的优化(主要体现在避免了自动装箱带来的开销),所以我们建议优先使用原始类型数组。
泛型
class SmartList<T> : ArrayList<T>() {
fun find(t: T): T? {
val index = super.indexOf(t)
return if (index >= 0) super.get(index) else null
}
fun main(args: Array<String>) {
val smartList = SmartList<String>()
smartList.add("one")
println(smartList.find("one")) // 输出: one
println(smartList.find("two").isNullOrEmpty()) // 输出: true
}
}
由于扩展函数支持泛型,可以利用扩展函数实现上面的功能:
fun <T> ArrayList<T>.find(t: T): T? {
val index = this.indexOf(t)
return if (index >= 0) this.get(index) else null
}
fun main(args: Array<String>) {
val arrayList = ArrayList<String>()
arrayList.add("one")
println(arrayList.find("one")) // 输出: one
println(arrayList.find("two").isNullOrEmpty()) // 输出: true
}
类型约束:设定类型上界
class FruitPlate<T : Fruit>(val t: T)
class Noodles(weight: Double) // 面条类
val applePlate = FruitPlate<Apple>(Apple(100.0)) // 允许
val applePlate = FruitPlate(Apple(100.0)) // 允许, 同上简化写法
val noodlesPlate = FruitPlate<Noodles>(Noodles(200.0)) // 不允许
支持可空T
类型:
class FruitPlate<T: Fruit?>(val t:T)
val fruitPlate = FruitPlate(null)
多个泛型条件约束:
interface Ground {}
class Watermelon(weight: Double) : Fruit(weight), Ground
fun <T> cut(t: T) where T : Fruit, T : Ground {
print("You can cut me.")
}
cut(Watermelon(3.0)) // 允许
cut(Apple(2.0)) // 不允许
我们可以通过where
关键字来实现这种需求,它可以实现对泛型参数类型添加多个约束条件,比如这个例子中要求被切的东西是一种水果,而且必须是长在地上的水果。
Java 为什么无法声明一个泛型数组
我们先来看一个简单的例子,Apple
是Fruit
的子类,思考下Apple[]
和Fruit[]
,以及List<Apple>
和List<Fruit>
是什么关系呢?
Apple[] appleArray = new Apple[10];
Fruit[] fruitArray = appleArray; // 允许
fruitArray[0] = new Banana(0.5); // 编译通过,运行报 ArrayStoreException
List<Apple> appleList = new ArrayList<Apple>();
List<Fruit> fruitList = appleList; // 不允许
我们发现一个奇怪的现象,Apple[]
类型的值可以赋值给Fruit[]
类型的值,而且还可以将一个Banana
对象添加到fruitArray
,编译器能通过。作为对比,List<Friut>
类型的值则在一开始就禁止被赋值为List<Apple>
类型的值,这其中到底有什么不同呢?
其实这里涉及一个关键点,数组是协变的,而List
是不变的。简单来说,就是 Object[]
是所有对象数组的父类,而 List<Object>
却不是 List<T>
的父类。
Java 中的泛型是类型擦除的,可以看作伪泛型,简单来说,就是你无法在程序运行时获取到一个对象的具体类型。
我们可以用以下代码来对比一下List<T>
和数组:
System.out.println(appleArray.getClass());
System.out.println(appleList.getClass());
运行结果:
class [Ljavat.Apple;
class java.util.ArrayList
数组在运行时是可以获取自身的类型,而List<Apple>
在运行时只知道自己是一个List
,而无法获取泛型参数的类型。
而 Java 数组是协变的,也就是说任意的类 A 和 B,若 A 是 B 的父类,则 A[] 也是 B[] 的父类。但是假如给数组加入泛型后,将无法满足数组协变的原则,因为在运行时无法知道数组的类型。
Kotlin 中的泛型机制与 Java 中是一样的,所以上面的特性在 Kotlin 中同样存在。
比如通过下面的方式同样无法获取列表的类型:
val appleList = ArrayList<Apple>()
println(appleList.javaClass)
但不同的是,Kotlin 中的数组是支持泛型的,当然也不再协变,也就是说你不能将任意一个对象数组赋值给 Array<Any>
或者 Array<Any?>
。
val appleArray = arrayOfNulls<Apple>(3)
val anyArray: Array<Any?> = appleArray // 不允许
获取泛型参数类型
如何在运行时获取泛型类型的参数信息?可以利用匿名内部类。
val list1 = ArrayList<String>()
val list2 = object : ArrayList<String>() {} // 匿名内部类
println(list1.javaClass.genericSuperclass)
println(list2.javaClass.genericSuperclass)
结果:
java.util.AbstractList<E>
java.util.ArrayList<java.lang.String>
那么,为什么使用匿名内部类的这种方式能够在运行时获取泛型参数的类型呢?
其实泛型类型擦除并不是真的将全部的类型信息都擦除,还是会将类型信息放在对应class
的常量池中的。
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
open class GenericsToken<T> {
var type: Type = Any::class.java
init {
val superClass = this.javaClass.genericSuperclass
type = (superClass as ParameterizedType).actualTypeArguments[0]
}
fun main() {
val gt = object : GenericsToken<Map<String, String>>() {} // 使用 object 创建
println(gt.type)
}
}
结果:
java.util.Map<java.lang.String, ? extends java.lang.String>
匿名内部类在初始化的时候就会绑定父类或父接口的相应信息,这样就能通过获取父类或父接口的泛型类型信息来实现我们的需求。
你可以利用这样一个类来获取任何泛型的类型,我们常用的Gson
也是使用了相同的设计。(TypeToken)
private Type getTypeTokenTypeArgument() {
Type superclass = getClass().getGenericSuperclass();
if (superclass instanceof ParameterizedType) {
ParameterizedType parameterized = (ParameterizedType) superclass;
if (parameterized.getRawType() == TypeToken.class) {
return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]);
}
}
...
}
比如,我们在 Kotlin 中可以这样使用Gson
来进行泛型类的反序列化:
val json = ...
val rType = object: TypeToken<List<String>>(){}.type
val stringList = Gson().fromJson<List<String>>(json, rType)
使用内联函数获取泛型
在 Kotlin 中除了用这种方式来获取泛型参数类型以外,还有另外一种方式,那就是内联函数。
Kotlin中的内联函数在编译的时候编译器便会将相应函数的字节码插入调用的地方,也就是说,参数类型也会被插入字节码中,我们就可以获取参数的类型了。
inline fun <reified T> getType() {
return T::class.java
}
使用内联函数获取泛型的参数类型非常简单,只需加上reified
关键词即可。这里的意思相当于,在编译的会将具体的类型插入相应的字节码中,那么我们就能在运行时获取到对应参数的类型了。所以,我们可以在 Kotlin 中改进 Gson
的使用方式:
inline fun <reified T: Any> Gson.fromJson(json: String): T { // 对 Gson 进行扩展
return Gson().fromJson(json, T::class.java)
}
// 使用
fun main() {
val json = "..."
val stringList = Gson().fromJson<List<String>>(json)
}
这里利用了 Kotlin 的扩展特性对 Gson
进行了功能扩展,在不改变原有类结构的情况下新增方法,很多场景用 Kotlin 来实现便会变得更加优雅。
另外需要注意的一点是,Java 并不支持主动指定一个函数是否是内联函数,所以在 Kotlin 中声明的普通内联函数可以在Java中调用,因为它会被当作一个常规函数;而用reified
来实例化的参数类型的内联函数则不能在 Java 中调用,因为它永远是需要内联的。
泛型中的协变
在 Java 中不支持将List<String>
赋值给List<Object>
,如果支持这种行为的话,那么它将会和数组支持泛型一样,不再保证类型安全。
List<String> stringList = new ArrayList<String>();
List<Object> objList = stringList; // 假设可以,编译报错
objList.add(Integer.valueOf(1));
String str = stringList.get(0); // 将会出错
但是在 Kotlin 中却支持这样做:
val stringList: List<String> = ArrayList<String>()
val anyList: List<Any> = stringList // 编译成功
关键在于这两个List
并不是同一种类型。我们分别来看一下两种List
的定义:
public interface List<E> extends Collection<E> { // java 的 List
...
}
public interface List<out E> : Collection<E> { // kotlin 的 List
...
}
虽然都叫List
,也同样支持泛型,但是 Kotlin 的 List
定义的泛型参数前面多了一个out
关键词。普通方式定义的泛型是不变的,简单来说就是不管类型 A
和类型 B
是什么关系Generic<A>
与Generic<B>
(其中Generic
代表泛型类)都没有任何关系。比如,在 Java 中 String
是 Oject
的子类型,但 List<String>
并不是 List<Object>
的子类型,在 Kotlin 中泛型的原理也是一样的。
如果在定义的泛型类和泛型方法的泛型参数前面加上out
关键词,说明这个泛型类及泛型方法是协变,简单来说如果类型 A
是类型 B
的子类型,那么 Generic<A>
也是 Generic<B>
的子类型,比如在 Kotlin 中 String
是 Any
的子类型,那么List<String>
也是List<Any>
的子类型,所以List<String>
可以赋值给List<Any>
。
但是我们上面说过,如果允许这种行为,将会出现类型不安全的问题。那么Kotlin是如何解决这个问题的?我们来看一个例子:
val stringList: MutableList<String> = ArrayList<String>()
stringList.add("kotlin") // 编译报错,不允许
这又是什么情况,往一个List
中插入一个对象竟然不允许,难道这个List
只能看看?确实是这样的,因为这个List
支持协变,那么它将无法添加元素,只能从里面读取内容。(即只读的List
)
这点我们查阅List
的源码也可以发现:List
中本来就没有定义add
方法,也没有remove
及replace
等方法,也就是说这个List
一旦创建就不能再被修改,这便是将泛型声明为协变需要付出的代价。
那么为什么泛型协变会有这个限制呢?同样我们用反证法来看这个问题,如果允许向这个List
插入新对象,会发生什么?我们来看一个例子:
val stringList: List<String> = ArrayList<String>()
val anyList: List<Any> = stringList
anyList.add(1)
val str: String = anyList[0] // Int 无法转换为 String
从上面的例子可以看出,假如支持协变的List
允许插入新对象,那么它就不再是类型安全的了,也就违背了泛型的初衷。
所以我们可以得出结论:支持协变的List
只可以读取,而不可以添加。其实从out
这个关键词也可以看出,out
就是出的意思,可以理解为List
是一个只读列表。
在 Java 中也可以声明泛型协变,用通配符及泛型上界来实现协变:<? extends T>
,其中T
可以是任意类。比如在 Java 中声明一个协变的List
:
List<? extends Animal> list = new ArrayList<Dog>();
但泛型协变实现起来非常别扭,这也是 Java 泛型一直被诟病的原因。很庆幸,Kotlin 改进了它,使我们能用简洁的方式来对泛型进行不同的声明。
另外需要注意的一点的是:通常情况下,若一个泛型类Generic<out T>
支持协变,那么它里面的方法的参数类型不能使用 T
类型,因为一个方法的参数不允许传入参数父类型的对象,因为那样可能导致错误。
class Generic<out T> {
fun funs1(a: T) { // 编译报错
}
fun <E> fun2(a: E) { // 但是普通的泛型方法可以支持这样写
}
fun <E> fun3() : T { // 可以作为返回值
}
}
什么是逆变?
你说协变我还好理解,毕竟原来是父子,支持泛型协变后的泛型类也还是父子关系。但是反过来又是一个什么情 况? 比如 Double
是 Number
的子类型,反过来Generic<Double>
却是Generic<Number>
的父类型?那么到底有没有这种场景呢?
一个支持逆变的 Comparator
我们来思考一个问题,假设现在需要对一个MutableList<Double>
进行排序,利用其sortWith
方法,我们需要传入一个比较器,所以可以这么做:
val doubleComparator = Comparator<Double> { d1, d2 ->
d1.compareTo(d2)
}
val doubleList = mutableListOf(2.0, 3.0)
doubleList.sortWith(doubleComparator)
暂时来看, 没有什么问题。 但是现在我们又需要对MutableList<Int>
、MutableList<Long>
等进行排序,那么我们是不是又需要定义intComparator
、longComparator
等呢?现在看来这并不是一种好的解决方法。那么试想一下可不可以定义一个比较器,给这些列表使用。
我们知道,这些数字类有一个共同的父类Number
,那么Number
类型的比较器是否代替它的子类比较器?比如:
val numberComparator = Comparator<Number> { n1, n2 ->
n1.toDouble().compareTo(n2.toDouble())
}
val doubleList = mutableListOf(2.0, 3.0)
doubleList.sortWith(numberComparator)
val intList = mutableListOf(1,2)
intList.sortWith(numberComparator)
编译通过,验证了我们的猜想。
那么为什么numberComparator
可以代替 doubleComparator
、 intComparator
呢? 我们来看一下sortWith
方法的定义:
public fun <T> MutableList<T>.sortWith(comparator: Comparator<in T>): Unit
if (size > 1) java.util.Collections.sort(this, comparator)
}
这里我们又发现了一个关键词 in
,跟out
一样,它也使泛型有了另一个特性,那就是逆变。简单来说,假如类型 A
是类型 B
的子类型,那么 Generic<B>
反过来是 Generic<A>
的子类型,所以我们就可以将一个numberComparator
作为doubleComparator
传入。那么将泛型参数声明为逆变会不会有什么限制呢?
前面我们说过,用out
关键字声明的泛型参数类型将不能作为方法的参数类型,但可以作为方法的返回值类型,而in
刚好相反。比如声明以下一个列表:
class WriteableList<in T> {
fun add(a: T): Int { // 允许
}
fun get(index: T): T { // 不允许返回T类型
// Type parameter T is declared as 'in' but occurs in 'out' position in type T
}
fun get(index: T): Any { // 允许
}
}
我们不能将泛型参数类型当作方法返回值的类型,但是作为方法的输入参数类型没有任何限制,其实从in
这个关键词也可以看出,in
就是入的意思,可以理解为消费内容,所以我们可以将这个列表看作一个可写、但可读功能受限的列表,获取的值只能为Any
类型。在Java中使用<? super T>
可以达到相同效果。
如何使用 in 和 out
in
和out
是一个对立面,其中in
代表泛型参数类型逆变,out
代表泛型参数类型协变。从字面意思上也可以理解,in
代表着输入,而out
代表着输出。但同时它们又与泛型不变相对立,统称为型变,而且它们可以用不同方式使用。
比如:
public interface List<out E> : Collection<E> {}
这种方式是在声明处型变,另外还可以在使用处型变,比如前面例子中sortWith
方法。
假设现在有个需求,需要将数据从一个Double
数组拷贝到另一个Double
数组,我们该怎么实现呢?
一开始我们可能会这么做:
fun copy(dest: Array<Double>, src: Array<Double>) {
if (dest.size < src.size) {
throw IndexOutOfBoundsException()
} else {
src.forEachIndexed{index,value -> dest[index] = src[index]}
}
}
var dest = arrayOfNulls<Double>(3)
val src = arrayOf<Double>(1.0,2.0,3.0)
copy(dest, src)
但是学过泛型后的你一定不会这么做了,因为假如替换成Int
类型的列表,是不是又得写一个copy
方法?所以我们可以对其进一步抽象:
fun <T> copy(dest: Array<T>, src: Array<T>) {
if (dest.size < src.size) {
throw IndexOutOfBoundsException()
} else {
src.forEachIndexed{index,value -> dest[index] = src[index]}
}
}
var destDouble = arrayOfNulls<Double>(3)
val srcDouble = arrayOf<Double>(1.0,2.0,3.0)
copy(destDouble, srcDouble)
var destInt = arrayOfNulls<Int>(3)
val srcInt = arrayOf<Int>(1,2,3)
copy(destInt, srcInt)
那么这种方式有没有什么局限呢?我们发现,使用copy
方法必须是同一种类型,那么假如我们想把Array<Double>
拷贝到Array<Number>
中将不允许。
这时候我们就可以利用泛型型变了。
fun <T> copy(dest: Array<in T>, src: Array<out T>) {
if (dest.size < src.size) {
throw IndexOutOfBoundsException()
} else {
src.forEachIndexed{index,value -> dest[index] = src[index]}
}
}
fun test() {
val dest = arrayOfNulls<Number>(3)
val src = arrayOf<Double>(1.0,2.0,3.0)
copy(dest, src)
}
in
是声明在dest
数组上,而out
是声明在src
数组上,所以dest
可以接收T
类型的父类型的Array
,src
可以接收T
类型的子类型的Array
。当然这里的T
要到编译的时候才能确定。
Kotlin 与 Java 的型变比较:
型变类型 | Kotlin 实现方式 | Java 实现方式 | 含义 |
---|---|---|---|
协变 | <out T> | <? extends T> | 消费者,只能读取不能添加 |
逆变 | <in T> | <? super T> | 生产者,只能添加,读取受限 |
不变 | <T> | <T> | 既可以添加,也可以读取 |
如果你对泛型参数的类型不感兴趣,那么你可以使用类型通配符来代替泛型参数。前面已经接触过 Java 中的泛型类型通配符“?
”,而在 Kotlin 中则用“*
”来表示类型通配符。比如:
val list: MutableList<*> = mutableListOf(1, "kotlin")
list.add(2.0) // 出错
这个列表竟然不能添加,不是说好是通配吗?按道理应该可以添加任意元素。
其实不然,MutableList<*>
与MutableList<Any?>
不是同一种列表,后者可以添加任意元素,而前者只是通配某一种类型,但是编译器却不知道这是一种什么类型,所以它不允许向这个列表中添加元素,因为这样会导致类型不安全。
前面所说的协变也是不能添加元素,那么它们两者之间有什么关系呢?其实通配符只是一种语法糖,背后上也是用协变来实现的。所以MutableList<*>
本质上就是MutableList<out Any?>
,使用通配符与协变有着一样的特性。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!