《Kotlin核心编程》笔记:可空类型&平台类型&装箱类型&数组类型&泛型&协变与逆变

可空类型

在Kotlin中,我们可以在任何类型后面加上"?",比如"Int?",实际上等同于"Int? = Int or null"。

通过合理的使用,不仅能够简化很多判空代码,还能够有效避免空指针异常。

注意:由于null只能被存储在 Java 的引用类型的变量中,所以在 Kotlin 中基本数据的可空版本都会使用该类型的包装形式 。同样,如果你用基本数据类型作为泛型类的类型参数,Kotlin同样会使用该类型的包装形式。(即可空类型会自动装箱)

Java中对于null的一些解决方案:

  • 函数内对于无效值,可以抛异常处理。
  • 采用@NotNull/@Nullable标注。
  • 使用专门的Optional对象对可能为null的变量进行装箱。

可空类型相关的安全操作符

安全的调用 ?.

kotlin 复制代码
s.student?.glasses?.degreeOfMyopia

Elvis操作符 ?:

kotlin 复制代码
val result = student.glasses?.degreeOfMyopia ?: -1

又称合并运算符

非空断言 !!

kotlin 复制代码
val result = student!!.glasses

类型检查

在Kotlin中,我们可以用"is"来判断。

kotlin 复制代码
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 可以将一个变量的类型转变为另一种类型,它是隐式完成的。

kotlin 复制代码
val stu: Any = Student(Glasses(189.00)) 
if(stu is Student) println(stu.glasses) 

对于可空类型,我们可以使用 Smart Casts:

kotlin 复制代码
val stu: Student = Student(Glasses(189.00))
if (stu.glasses != null) println(stu.glasses.degreeOfMyopia) 

我们将这个例子反编译成Java,核心代码如下

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。

利用这点,我们能确保多线程的应用足够安全。举个例子:

kotlin 复制代码
class Kot {
    var stu: Student? = getStu()
    
    fun dealStu() {
        if (stu != null) {
            print(stu.glasses)
        }
    }
}

上述代码中,我们将stu声明为引用可空类型变量,这意味着在判断 stu != null 之后,stu在其他线程中还是会被修改的,所以被编译器无情地拒绝了。

var改为val就不会存在这样的问题,引用不可变能够确保程序运行不产生额外的副作用。你也许会觉得这样写不够优雅,我们可以用let函数来简化⼀下:

kotlin 复制代码
class Kot {
    var stu: Student? = getStu()
    
    fun dealStu() {
        stu?.let { print(it.glasses) }
    }
}

这样就会满足 Smart Casts 的条件,就不会被拒绝编译。

在实际开发中,我们并不总能满足 Smart Casts 的条件。并且 Smart Casts 有时会缺乏语义,并不适用于所有场景。

当类型需要强制转换时,我们可以利用" as" 操作符来实现。

kotlin 复制代码
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

kotlin 复制代码
val stu: Student? = getStu() as Student

则会抛出类型转换失败的异常,因为它不是可空的。所以,我们通常称之为" 不安全" 的类型转换。

那是否有安全版本的转换呢?除了上述写法外,Kotlin还提供了操作符" as?",我们可以这样改写:

kotlin 复制代码
val stu: Student? = getStu() as? Student

这时,如果stu为空将不会抛出异常,而是返回转换结果null

Any:非空类型的根类型

Object 作为 Java 类层级结构的顶层类似,Any 类型是 Kotlin 中所有非空类型 (如StringInt)的超类:

与 Java 不同的是,Kotlin 不区分"原始类型"(primitivetype)和其他的类型,它们都是同一类型层级结构的一部分。

如果定义了一个没有指定父类型的类型,则该类型将是Any的直接子类型。如:

kotlin 复制代码
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? 才是所有类型(可空和非空类型)的根类型。
AnyAny? 看起来没有继承关系,然而在我们需要用 Any? 类型值的地方,显然可以传入一个类型为Any的值,这在编译上不会产生问题。比如在 Kotlin 中 IntNumber 的子类:

kotlin 复制代码
fun printNum(num : Number){
    println(num)
}

val n : Int = 1
printNum(n)

反之却不然,比如一个参数类型为Any的函数,我们传入符合 Any? 类型的null值,就会出现如下的错误:

kotlin 复制代码
error: null can not be a value of a non-null type Any 

Nothing 与 Nothing?

在 Kotlin 类型层级结构的最底层是 Nothing 类型。

Nothing是没有实例的类型。Nothing类型的表达式不会产生任何值。需要注意的是,任何返回值为Nothing的表达式之后的语句都是无法执行的。你是不是感觉这有点像return或者break的作用?没错,Kotlin 中 returnthrow 等(流程控制中与跳转相关的表达式)返回值都为Nothing

Nothing?Nothing 的父类型,其实,它只能包含一个值:null,本质上与null没有区别。所以我们可以使用null作为任何可空类型的值。

自动装箱与拆箱

我们发现,Kotlin 中并没有 intfloatdoublelong这样的原始类型,取而代之的是它们对应的引用类型包装类IntFloatDoubleLong

除了以上代表数值的类型,还有布尔(Boolean)、字符(Char)、字符串(String)及数组(Array)。这让 Kotlin 比起 Java 来更加接近纯面向对象的设计------一切皆对象。

但这么说其实也是不够严谨的。以Int为例,虽然它可以像Integer一样提供额外的操作函数,但这两个类型在底层实现上存在差异。Kotlin 中的 Int 在 JVM 中实际以 int 存储(对应字节码类型为I)但是,作为一个" 包装类型",编译后应该装箱才对,难道,Kotlin 不会自动装箱?

我们可以简单地认为:

  • Kotlin中的 Int 类型等同于 int
  • Kotlin中的 Int? 等同于 Integer

Int作为一种小技巧,让Int看起来是引用类型,这在语法上让 Kotlin 更接近纯面向对象语言。

数组类型

kotlin 复制代码
val funList = arrayOf() // 声明长度为0的数组
val funList = arrayOf(n1, n2, n3, ..., nt) // 声明并初始化长度为t的数组 

Kotlin中 Array 并不是一种原生的数据结构,而是一种Array类,甚至我们可以将 Kotlin 中的Array视作集合类的一部分。

由于 Smart Casts,编译器能够隐式推断出funList元素类型。当然,我们也可以手动指定类型

kotlin 复制代码
val funList = arrayOf<T>(n1, n2, n3..., nt)

在 Kotlin 中,还为原始类型额外引入了一些实用的类:IntArrayCharArrayShortArray等,分别对应 Java 中的int[]char[]short[]等。

kotlin 复制代码
val x = intArrayOf(1,2,3) 

注意:IntArray等并不是Array的子类,所以用两者创建的相同值的对象,并不是相同对象。

由于 Kotlin 对原始类型有特殊的优化(主要体现在避免了自动装箱带来的开销),所以我们建议优先使用原始类型数组。

泛型

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
    }
}

由于扩展函数支持泛型,可以利用扩展函数实现上面的功能:

kotlin 复制代码
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
}

类型约束:设定类型上界

kotlin 复制代码
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类型:

kotlin 复制代码
class FruitPlate<T: Fruit?>(val t:T) 
val fruitPlate = FruitPlate(null) 

多个泛型条件约束:

kotlin 复制代码
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 为什么无法声明一个泛型数组

我们先来看一个简单的例子,AppleFruit的子类,思考下Apple[]Fruit[],以及List<Apple>List<Fruit>是什么关系呢?

java 复制代码
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>和数组:

java 复制代码
System.out.println(appleArray.getClass()); 
System.out.println(appleList.getClass()); 

运行结果:

java 复制代码
class [Ljavat.Apple; 
class java.util.ArrayList 

数组在运行时是可以获取自身的类型,而List<Apple>在运行时只知道自己是一个List,而无法获取泛型参数的类型。

而 Java 数组是协变 的,也就是说任意的类 AB ,若 AB 的父类,则 A[] 也是 B[] 的父类。但是假如给数组加入泛型后,将无法满足数组协变的原则,因为在运行时无法知道数组的类型。

Kotlin 中的泛型机制与 Java 中是一样的,所以上面的特性在 Kotlin 中同样存在。

比如通过下面的方式同样无法获取列表的类型:

kotlin 复制代码
val appleList = ArrayList<Apple>() 
println(appleList.javaClass)

但不同的是,Kotlin 中的数组是支持泛型的,当然也不再协变 ,也就是说你不能将任意一个对象数组赋值给 Array<Any> 或者 Array<Any?>

kotlin 复制代码
val appleArray = arrayOfNulls<Apple>(3)
val anyArray: Array<Any?> = appleArray // 不允许

获取泛型参数类型

如何在运行时获取泛型类型的参数信息?可以利用匿名内部类

kotlin 复制代码
val list1 = ArrayList<String>()
val list2 = object : ArrayList<String>() {} // 匿名内部类
println(list1.javaClass.genericSuperclass)
println(list2.javaClass.genericSuperclass)

结果:

kotlin 复制代码
java.util.AbstractList<E>
java.util.ArrayList<java.lang.String> 

那么,为什么使用匿名内部类的这种方式能够在运行时获取泛型参数的类型呢?

其实泛型类型擦除并不是真的将全部的类型信息都擦除,还是会将类型信息放在对应class常量池中的。

kotlin 复制代码
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 复制代码
java.util.Map<java.lang.String, ? extends java.lang.String> 

匿名内部类在初始化的时候就会绑定父类或父接口的相应信息这样就能通过获取父类或父接口的泛型类型信息来实现我们的需求

你可以利用这样一个类来获取任何泛型的类型,我们常用的Gson也是使用了相同的设计。(TypeToken

kotlin 复制代码
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来进行泛型类的反序列化:

kotlin 复制代码
val json = ...
val rType = object: TypeToken<List<String>>(){}.type 
val stringList = Gson().fromJson<List<String>>(json, rType)

使用内联函数获取泛型

在 Kotlin 中除了用这种方式来获取泛型参数类型以外,还有另外一种方式,那就是内联函数

Kotlin中的内联函数在编译的时候编译器便会将相应函数的字节码插入调用的地方,也就是说,参数类型也会被插入字节码中,我们就可以获取参数的类型了。

kotlin 复制代码
inline fun <reified T> getType() { 
	return T::class.java
}

使用内联函数获取泛型的参数类型非常简单,只需加上reified关键词即可。这里的意思相当于,在编译的会将具体的类型插入相应的字节码中,那么我们就能在运行时获取到对应参数的类型了。所以,我们可以在 Kotlin 中改进 Gson 的使用方式:

kotlin 复制代码
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>,如果支持这种行为的话,那么它将会和数组支持泛型一样,不再保证类型安全。

java 复制代码
List<String> stringList = new ArrayList<String>();
List<Object> objList = stringList; // 假设可以,编译报错
objList.add(Integer.valueOf(1));
String str = stringList.get(0); // 将会出错 

但是在 Kotlin 中却支持这样做:

kotlin 复制代码
val stringList: List<String> = ArrayList<String>()
val anyList: List<Any> = stringList // 编译成功

关键在于这两个List并不是同一种类型 。我们分别来看一下两种List的定义:

java 复制代码
public interface List<E> extends Collection<E> { // java 的 List
	...
}
kotlin 复制代码
public interface List<out E> : Collection<E> { // kotlin 的 List
	...
}

虽然都叫List,也同样支持泛型,但是 Kotlin 的 List 定义的泛型参数前面多了一个out关键词。普通方式定义的泛型是不变 的,简单来说就是不管类型 A 和类型 B 是什么关系Generic<A>Generic<B>(其中Generic代表泛型类)都没有任何关系 。比如,在 Java 中 StringOject 的子类型,但 List<String> 并不是 List<Object> 的子类型,在 Kotlin 中泛型的原理也是一样的。

如果在定义的泛型类和泛型方法的泛型参数前面加上out关键词,说明这个泛型类及泛型方法是协变 ,简单来说如果类型 A 是类型 B 的子类型,那么 Generic<A> 也是 Generic<B> 的子类型 ,比如在 Kotlin 中 StringAny 的子类型,那么List<String>也是List<Any>的子类型,所以List<String>可以赋值给List<Any>

但是我们上面说过,如果允许这种行为,将会出现类型不安全的问题。那么Kotlin是如何解决这个问题的?我们来看一个例子:

kotlin 复制代码
val stringList: MutableList<String> = ArrayList<String>()
stringList.add("kotlin") // 编译报错,不允许 

这又是什么情况,往一个List中插入一个对象竟然不允许,难道这个List只能看看?确实是这样的,因为这个List支持协变,那么它将无法添加元素,只能从里面读取内容。(即只读List)

这点我们查阅List的源码也可以发现:List中本来就没有定义add方法,也没有removereplace等方法,也就是说这个List一旦创建就不能再被修改,这便是将泛型声明为协变需要付出的代价。

那么为什么泛型协变会有这个限制呢?同样我们用反证法来看这个问题,如果允许向这个List插入新对象,会发生什么?我们来看一个例子:

kotlin 复制代码
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

java 复制代码
List<? extends Animal> list = new ArrayList<Dog>();

但泛型协变实现起来非常别扭,这也是 Java 泛型一直被诟病的原因。很庆幸,Kotlin 改进了它,使我们能用简洁的方式来对泛型进行不同的声明。

另外需要注意的一点的是:通常情况下,若一个泛型类Generic<out T>支持协变,那么它里面的方法的参数类型不能使用 T 类型,因为一个方法的参数不允许传入参数父类型的对象,因为那样可能导致错误。

kotlin 复制代码
class Generic<out T> {
    fun funs1(a: T) { // 编译报错
        
    }
    fun <E> fun2(a: E) { // 但是普通的泛型方法可以支持这样写

    }
     fun <E> fun3() : T { // 可以作为返回值

    }
}

什么是逆变?

你说协变我还好理解,毕竟原来是父子,支持泛型协变后的泛型类也还是父子关系。但是反过来又是一个什么情 况? 比如 DoubleNumber 的子类型,反过来Generic<Double>却是Generic<Number>的父类型?那么到底有没有这种场景呢?

一个支持逆变的 Comparator

我们来思考一个问题,假设现在需要对一个MutableList<Double>进行排序,利用其sortWith方法,我们需要传入一个比较器,所以可以这么做:

kotlin 复制代码
val doubleComparator = Comparator<Double> { d1, d2 -> 
	d1.compareTo(d2)
}

val doubleList = mutableListOf(2.0, 3.0)
doubleList.sortWith(doubleComparator)

暂时来看, 没有什么问题。 但是现在我们又需要对MutableList<Int>MutableList<Long>等进行排序,那么我们是不是又需要定义intComparatorlongComparator等呢?现在看来这并不是一种好的解决方法。那么试想一下可不可以定义一个比较器,给这些列表使用。

我们知道,这些数字类有一个共同的父类Number,那么Number类型的比较器是否代替它的子类比较器?比如:

kotlin 复制代码
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可以代替 doubleComparatorintComparator 呢? 我们来看一下sortWith方法的定义:

kotlin 复制代码
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刚好相反。比如声明以下一个列表:

kotlin 复制代码
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

inout是一个对立面,其中in代表泛型参数类型逆变,out代表泛型参数类型协变。从字面意思上也可以理解,in代表着输入,而out代表着输出。但同时它们又与泛型不变相对立,统称为型变,而且它们可以用不同方式使用。

比如:

kotlin 复制代码
public interface List<out E> : Collection<E> {} 

这种方式是在声明处型变,另外还可以在使用处型变,比如前面例子中sortWith方法。

假设现在有个需求,需要将数据从一个Double数组拷贝到另一个Double数组,我们该怎么实现呢?

一开始我们可能会这么做:

kotlin 复制代码
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方法?所以我们可以对其进一步抽象:

kotlin 复制代码
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>中将不允许。

这时候我们就可以利用泛型型变了。

kotlin 复制代码
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类型的父类型的Arraysrc可以接收T类型的子类型的Array。当然这里的T要到编译的时候才能确定。

Kotlin 与 Java 的型变比较:

型变类型 Kotlin 实现方式 Java 实现方式 含义
协变 <out T> <? extends T> 消费者,只能读取不能添加
逆变 <in T> <? super T> 生产者,只能添加,读取受限
不变 <T> <T> 既可以添加,也可以读取

如果你对泛型参数的类型不感兴趣,那么你可以使用类型通配符来代替泛型参数。前面已经接触过 Java 中的泛型类型通配符"?",而在 Kotlin 中则用"*"来表示类型通配符。比如:

kotlin 复制代码
val list: MutableList<*> = mutableListOf(1, "kotlin")
list.add(2.0) // 出错

这个列表竟然不能添加,不是说好是通配吗?按道理应该可以添加任意元素。

其实不然,MutableList<*>MutableList<Any?>不是同一种列表,后者可以添加任意元素,而前者只是通配某一种类型,但是编译器却不知道这是一种什么类型,所以它不允许向这个列表中添加元素,因为这样会导致类型不安全。

前面所说的协变也是不能添加元素,那么它们两者之间有什么关系呢?其实通配符只是一种语法糖,背后上也是用协变 来实现的。所以MutableList<*>本质上就是MutableList<out Any?>,使用通配符与协变有着一样的特性。

相关推荐
居居飒14 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
刘争Stanley2 天前
如何高效调试复杂布局?Layout Inspector 的 Toggle Deep Inspect 完全解析
android·kotlin·android 15·黑屏闪屏白屏
sickworm陈浩2 天前
Java 转 Kotlin 系列:究竟该不该用 lateinit?
android·kotlin
droidHZ3 天前
Compose Multiplatform 之旅—声明式UI
android·kotlin
zhangphil3 天前
Android基于Path的addRoundRect,Canvas剪切clipPath简洁的圆角矩形实现,Kotlin(1)
android·kotlin
alexhilton5 天前
Android技巧:学习使用GridLayout
android·kotlin·android jetpack
zhangphil6 天前
Android使用PorterDuffXfermode的模式PorterDuff.Mode.SRC_OUT实现橡皮擦,Kotlin(1)
android·kotlin
IH_LZH7 天前
OkHttp源码分析:分发器任务调配,拦截器责任链设计,连接池socket复用
android·java·okhttp·kotlin
casual_clover8 天前
Android之RecyclerView显示数据列表和网格
android·kotlin
氤氲息9 天前
导入kotlin
android·开发语言·kotlin