Kotlin基础学习笔记之第六章——kotlin的类型系统

 一、本章简介

        与java相比,kotlin中引入了一些新特性,他们是提升代码可读性的基本要素,比如:对可空的类型只读集合的支持。与此同时,kotlin去掉了一些java类型系统中不必要的或者有问题的特性,比如把数组作为头等公民来支持

二、可空性

        可空性是kotlin类型系统中帮助你避免NullPointerException错误的特性。现代编译语言包括kotlin解决空指针这类问题的方法是把运行时的错误转换成编译期的错误通过支持作为类型系统的一部分的可空性,编译器就能在编译期发现很多潜在的错误,从而减少运行时抛出异常的可能性

1、可空类型

      kotlin和java的类型系统之间第一条也可能是最重要的一条区别是:kotlin对可空类型的显式支持。这意味着:它指出你的程序中哪些变量和属性允许为null的方式。如果一个变量可以为null,对变量的方法的调用就是不安全的,因为这样会导致NullPointerException。kotlin不允许这样的调用,因而可以阻止 许多可能得异常。      

        重申一下,没有问号的类型表示这种类型的变量不能存储null引用。这说明所有常见类型默认都是非空的,除非显式地把它标记为可空。

        一旦你有一个可空类型的值,能对它进行的操作也会受到限制。例如,不能在调用它的方法: 

         那么你可以对它做什么呢?最重要的操作就是和null进行比较而且一旦你进行了比较操作,编译器就会记住,并且在这次比较发生的作用域内把这个值当做非空对待

         如果if检查是唯一处理可空性的工具,你的代码很快将会变得冗长。幸运的是,kotlin还提供了其他一些工具来帮助我们用更简洁的方式来处理可空值。

2、类型的含义

        以String类型为例,在java中,变量可以持有两种值,分别是String的实例和null。这两种值完全不一样:就连java自己的instanceOf运算符都会告诉你null不是String。这两种值的操作也完全不一样:真实的String实例允许你调用它的任何方法,而null值只允许非常有限的操作。

        这说明java的类型系统在这种情况下不能很好地工作。

3、安全调用运算符:“?.” 

        安全调用符?.允许你把一次null检查一次方法调用合并成一个操作。例如,表达式s?.toUpperCase()等同于下面这种繁琐的写法:if (s != null) s.toUpperCase() else null。

        换句话说,如果你试图调用一个非空值的方法,这个方法调用会被正常执行。但如果是null,这次调用不会发生,而整个表达式的值为null。

          注意,这次调用的结果类型可是可空的。尽管String.toUpperCase()会返回String类型的值,但s是可空的时候,表达式s?.toUpperCase()的结果类型是String? :

        安全调用不仅可以调用方法,还可以用来访问属性。

         如果你的对象图中有多个可空类型的属性,通常可以在同一个表达式中方便地使用多个安全调用

         带null检查的方法调用序列在java代码中太常见了,现在你看到了kotlin可以让它们变得更简洁。

4、Elvis运算符:“?:”

        kotlin有方便的运算符来替换代替null的默认值。它被称作为Elvis运算符(或者null合并运算符

        Elvis运算符接收两个运算数,如果第一个运算数不为null,运算结果就是第一个运算数,如果第一个运算数为null,运算结果就是第二个运算数。

          Elvis运算符经常和安全调用符一起使用,用一个值代替对null对象调用方法时返回的null。

         在kotlin中有一种场景下Elvis运算符会特别顺手,像return和throw这样的操作其实是表达式,因此可以把它们写在Elvis运算符的右边。这种情况下,如果Elvis运算符左边的值为null,函数就会立即返回一个值或抛出一个异常。如果函数中需要检查先决条件,这个方式特别有用。

         如果一切正常,函数printShippingLable会打印出标签。如果地址不存在,它不会只是抛出一个带行号的NullPointerException,相反,它会报告一个有意义的错误。如果地址存在,标签会包含街道地址、编码、城市和国家。

        现在,你了解了kotlin进行“if非空”检查的方式,我们接下来介绍kotlin中instanceOf检查的安全版本:常常和安全调用及Elvis运算符一起出现的安全转换运算符

5、安全转换:“as?”

        之前学习过kotlin用来转换类型的常规运算符:as运算符和常规的java类型转换一样,如果被转换的值不是你试图转换的类型,就会抛出ClassCastException异常。当然可以结合is检查来确保这个值拥有合适的类型。但是作为一种安全简洁的语言,kotlin有更优雅的解决方案。

         一种常见的模式是把安全转换和Elvis运算符结合使用。例如实现equals方法的时候这样使用非常方便。

        使用这种模式,可以非常容易地检查实参是否是适当的类型,转换它,并在它的类型不正确的时候返回false,而且这些操作全部在同一个表达式中。当然,这种场景下智能转换也会生效:当你检查过类型并拒绝了null值,编译器就确定了变量otherPerson值的类型是Person并让你能够相应地使用它。 

        安全调用、安全转换和Elvis运算符都非常有用,他们出现在kotlin代码中的频率非常高但有时候你并不需要Kotlin的这些支持来处理null值,你只需要直接告诉编译器这个值实际上并不是null。接下来看看是如何做到这一点的。

6、非空断言:“!!”

        非空断言是kotlin提供给你的最简单直率的处理可空类型值的工具。它使用双感叹号表示,可以把任何值转换成非空类型如果对null值做非空断言,则会抛出异常

         如果上面函数中s为null会发生什么呢?kotlin没有其他选择,它会在运行时抛出一个异常(一种特殊的NullPointerException)。但是注意异常抛出的位置是非空断言所在那一行,而不是接下来试图使用那个值的一行。本质上,你在告诉编译器:“我知道这个值不为null,如果我错了我准备好了接受这个异常”。

        但是确实存在这样的情况,某些问题适合用非空断言来解决当你在一个函数中检查一个值是否为null,而在另一个函数中使用这个值时,这种情况下编译器无法识别这种用法是否安全。如果你确信这样的检查一定在其他某个函数中存在,你可能不想在使用这个值之前重复检查,这时候就可以使用非空断言

        还有一个需要牢记的注意事项,当你使用!!并且它的结果是异常时,异常调用栈的跟踪信息只表明异常发生在哪一行,而不会表明异常发生在哪一个表达式。为了让跟踪信息更清晰准确地表示哪个值为null,最好避免在同一行中使用多个!!断言

         到目前为止,我们讨论的都是如何访问可空类型的值。但是如果你要将一个可空值作为实参传递给一个只接受非空值的函数时,应该怎么办?编译器不允许在没有检查的情况下这样做,因为这样不安全。kotlin语言并没有对这样使用场景的特殊支持,但是标准库函数可以帮到你:这个函数叫作let

 7、let函数

        let函数让处理可空表达式变得更容易。和安全调用运算符一起,它允许你对表达式求值,检查求值结果是否为null,并把结果保存为一个变量。所有这些动作都在同一个简洁的表达式中

        可空参数最常见的一种用法应该是被传递给一个接收非空参数的函数。

        但你还有一种处理方式:使用let函数,并通过安全调用来调用它。let函数做的所有事情就是把一个调用它的对象变成lambda表达式的参数。如果结合安全调用语法,它能有效地把调用let函数的可空对象,转变成非空类型

        let函数只在email的值非空时才被调用,上面的代码变得更短了:email?.let { sendEmailTo(it) }。

        注意,如果有一些很长的表达式结果不为null,而你又要使用这些结果时,let表示法特别方便。在这种情况下你不必创建一个单独的变量。对比一下显式地if检查。

         当你需要检查多个值是否为null时,可以用嵌套的let调用来处理。但在大多数情况下,这种代码相当啰嗦又并且难以理解。用普通的if表达式来一次性检查所有值通常更简单

         另外一种常见的情况是,属性最终是非空的,但不能使用非空值构造方法初始化。接下来看看kotlin是如何处理这种情况的。

        这里还是要看看let函数的源码的。

/**
 * Calls the specified function [block] with `this` value as its argument and returns its result.
 *
 * For detailed usage information see the documentation for [scope functions](https://kotlinlang.org/docs/reference/scope-functions.html#let).
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

        可以看到let函数是任何类型的扩展函数 ,所以任何类型对象都可以调用let函数。并且let函数参数是一个lambda表达式,lambda表达式的参数的类型为调用let函数的类型,返回类型为R其他类型。函数体实际执行逻辑是block(this)。也就是说整个let函数做的事情就是使用把调用let函数的对象传递给lambda表达式作为实参,然后执行lambda表达式

        可以理解为block本质上是接受一个非空类型参数的函数。

        原本fun(T)现在变成了T?.let {fun(T)} 。看上去是变得更复杂了。安全调用?.能确保如果T为null就不执行,如果T不为null则执行let函数。所以let函数体内部能确保T不为null,就可以把T传递给一个调用非空参数的函数了所以let函数必须结合安全调用才有意义。       

8、延迟初始化的属性

        很多框架会在对象实例创建之后用专门的方法来初始化对象。例如在android中,Activity的初始化就发生在onCreate方法中。

        但是你不能在构造方法中完全放弃非空属性的初始化,仅仅在一个特殊的方法里初始化它。kotlin通常要求你在构造方法中初始化所有属性,如果某个属性是非空类型,你就必须提供非空的初始化值。否则,你就必须使用可空类型。如果你这样做,该属性的每一次访问都需要null检查或者!!运算符。

         这段代码很难看,尤其是你要反复使用这个属性的时候。为了解决这个问题,可以把myService属性声明成可以延迟初始化的,使用lateinit修饰符来完成这样的声明。

        注意,延迟初始化的属性都是var,因为需要在构造函数外修改它,而val属性会被编译成必须在构造方法中初始化的final字段。尽管这个属性是非空类型,但是你不需要在构造方法中初始化它。如果你在属性被初始化之前就访问了它,会得到这个异常“lateinit property myService has not been initialized”。 

依赖注入??

9、可空类型的扩展

        为可空类型定义扩展函数时一种强大的处理null值的方式。可以允许接收者为null的(扩展函数)调用,并在该函数中处理null,而不是在确保变量不为null之后再调用它的方法。只有扩展函数才能做到这一点,普通成员方法的调用是通过对象实例来分发的,因此实例为null时(成员方法)永远不能被执行。

/**
 * Returns `true` if this nullable char sequence is either `null` or empty or consists solely of whitespace characters.
 *
 * @sample samples.text.Strings.stringIsNullOrBlank
 */
@kotlin.internal.InlineOnly
public inline fun CharSequence?.isNullOrBlank(): Boolean {
    contract {
        returns(false) implies (this@isNullOrBlank != null)
    }

    return this == null || this.isBlank()
}

        不需要安全访问,可以直接调用为可空接收者声明的扩展函数,这样的函数会处理可能的null值。

         函数isNullOrBlank显式地检查了null,这种情况下返回true,然后调用isBlank,它只能在非空String上调用。

         当你为一个可空类型(以?结尾)定义扩展函数时,这意味着你可以对可空的值调用这个函数:并且函数体中的this可能为null,所以你必须显式地检查在java中,this永远是非空的,因为它引用的是当前你所在这类的实例。而在kotlin中,这并不永远成立:在可空类型的扩展函数中,this可以为null

       注意,我们之前讨论的let函数也能被可空的接收者调用,但它并不检查值是否为null。如果你在一个可空类型上直接调用let函数,而没有使用安全调用运算符,lambda的实参将会是可空的。

         这一节展示了一些意外的状态,如果你没有使用额外的检查来解引用一个变量,比如s.isNullOrBlank(),它并不会立即意味着变量是非空的:这个函数有可能是非空类型的扩展。

 10、类型参数的可空性

        kotlin中所有泛型类和泛型函数的类型参数默认都是可空的。任何类型,包括可空类型在内,都可以替换类型参数。这种情况下,使用类型参数作为类型的声明都允许为null,尽管类型参数T并没有用问号结尾

        要使类型参数非空,必须要为它指定一个非空的上界,那样泛型会拒绝可空值作为实参

        注意必须使用问号结尾来标记类型为可空的,没有问号就是非空的。类型参数是这个规则唯一的例外

 11、可空性和java

        首先,有些时候java代码包含了可空性的信息,这些信息使用注解来表达。当代码中出现了这样的信息时,kotlin就会使用它。因此java中的@Nullable String被kotlin当做String ?,而@NotNull String就是String。

        kotlin可以识别多种不同风格的可空性注解,包括在javax.annotation包之中的注解、Android的注解(android.support.annotation)和JetBrains工具支持的注解(org.jetbrains.annotations)。

        如果没有这些注解,java类型会变成Kotlin中的平台类型

平台类型

package main.part6;

public class Person {
    private final String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

        getName能不能返回null?这种情况下kotlin编译器完全不知道String类型的可空性,所以你必须自己处理它。如果你确定name不为null,就可以像java中一样按照通常的方式对它解引用,不需要额外的检查。但是这种情况下请准备好接收异常

        注意,这里你看到的不是一个干巴巴的NullPointerException,而是更详细的错误消息,告诉你方法toUpperCase不能在null的接收者上调用。

        事实上,对于公有的kotlin函数,编译器会生成对每个非空类型的参数(和接收者)的检查,所以,使用不正确的参数的调用尝试都会立即被报告为异常。注意,这种值检查在函数调用的时候就执行了,而不是等到这些参数被使用的时候,这确保了不正确的调用会被尽早发现,那些由于null值被传给代码不同层次的多个函数之后,并被这些函数访问时而产生难以理解的异常就能被避免。 

        另一种选择是把getName()的返回类型解释为可空的并安全地访问它

        上面这个例子中,null值被正确地处理了,没有抛出运行时异常。

        使用java API时要特别小心。大部分的库都没有(可空性)注解,所以可以把所有类型都解释为非空,但那样会导致错误。 为了避免错误,你应该阅读要用到的java方法的文档(必要时还要查看它的实现),搞清楚它们什么时候会返回null,并给那些方法加上检查。 

         在kotlin中不能声明一个平台类型的变量,这些类型只能来自java代码,但你可能会在IDE的错误消息中见到它们:

        String! 表示法被kotlin编译器用来表示来自java代码的平台类型,你不能在自己的代码中使用这种语法。而且感叹号通常与问题的来源无关,所以通常可以忽略它。它只是强调类型的可空性是未知的

        如前所述,可以用你喜欢的方式来解释平台类型即可以是可空的也可以是非空的,所以下面两种声明都是有效的

         我们已经讨论了kotlin怎样看待java的类型。下面我们说说创建混合的kotlin和java类层级关系时会遇到的一些陷阱

继承

       当在kotlin中重写java的方法时,可以选择把参数和返回类型定义成可空的,也可以选择把它们定义成非空的

         注意:在实现java类或接口的方法时一定要搞清楚它的可空性。因为方法的实现可以在非kotlin的代码中被调用,kotlin编译器会为你声明的每一个非空的参数生成一个非空断言。如果java代码传给这个方法一个null值,断言将会触发,你会得到一个异常,即便你从没有在你的实现中访问这个参数的值。

二、基本数据类型和其他基本类型

1、基本数据类型:Int、Boolean及其他

        java把基本数据类型和引用类型做了区分。一个基本数据类型(如int)的变量直接存储了它的值,而一个引用类型(如String)的变量存储的是指向包含该对象的内存地址的引用。

        基本数据类型的值能够更高效地存储和传递,但你不能对这些值调用方法,或是把它们存放在集合中。java提供了特殊的包装类型(比如java.lang.Integer),在你需要对象的时候对基本数据类型进行封装。因此,你不能用Collection<int>来定义一个整数的集合,而必须使用Collection<Integer>来定义。

        kotlin并不区分基本数据类型和包装类型,你使用的永远是同一个类型(比如:Int):

        如果基本数据类型和引用类型是一样的,是不是意味着kotlin使用对象来表示所有数字?这样不是非常低效吗?确实低效,所以kotlin并没有这样做。 

        在运行时,数字类型会尽可能地使用最高效的方式表示大多数情况下——对于变量、属性、参数和返回类型——kotlin的Int类型会被编译成Java基本数据类型int。唯一不可行的例外是泛型类,比如集合。用作泛型类型参数的基本数据类型会被编译成对应的java包装类型。例如Int类型被用作集合类的类型参数时,集合类将会保存对应包装类型java.lang.Integer的实例

        对应到java基本数据类型的类型完整列表如下:

         像Int这样的kotlin类型在底层可以轻易地编译成对应的java基本数据类型。因为两种类型都不能存储null引用。反过来也差不多:当你在kotlin中使用java声明时,java基本数据类型会变成非空类型(而不是平台类型),因为他们不能持有null值。 现在来讨论下这些类型的可空版本。

 2、可空的基本数据类型:Int?、Boolean?及其他

        kotlin中的可空类型不能用java的基本数据类型表示,因为null只能被存储在java的引用类型的变量中。这意味着任何时候只要使用了基本数据类型的可空版本,它就会编译成对应的包装类型

        注意,普通的可空性规则如何在这里应用。你不能就这样比较Int?类型的两个值,因为他们之中任何一个都可能为null。 相反,你必须检查两个值都不为null。然后,编译器才允许你正常地比较它们。

3、数字转换

        kotlin和java之间一条重要的区别就是处理数字转换的方式。kotlin不会自动地把数字从一种类型转换成另一种,即便是转换成范围更大的类型

        每一种基本数据类型(Boolean除外)都定义有转换函数:toByte()、toShort()、toChar()等。这些函数支持双向转换:既可以把小范围的类型扩展到大范围,比如Int.toLong(),也可以把大范围的类型截取到小范围,比如Long.toInt()。

        为了避免意外情况,kotlin要求转换必须是显式的,尤其是在比较装箱值的时候。比较两个装箱值的equals方法不仅会检查他们存储的值,还要比较装箱类型。所有,在java中new Integer(42).equals(new Long(42))会返回false。

         注意,当你书写数字字面值的时候一般不需要使用转换函数,一种可能性是用这种特殊的语法来显示地标记常量的类型,比如42L或者42.0f。而且即使你没有用这种语法,当你使用数字字面值去初始化一个类型已知的变量时,又或者是把字面值作为实参传给函数时,必要的转换会自动地发生。此外,算术运算符也被重载了,它们可以接收所有适当的数字类型。例如,下面这段代码并没有任何显式转换,但可以正确地工作:

        注意,kotlin算术运算符关于数值范围溢出的行为和java完全一致:kotlin并没有引入由溢出检查带来的额外开销。

 4、Any和Any?:根类型

        和Object作为java类层级结构的根差不多,Any类型是Kotlin所有非空类型的超类型(非空类型的根)。但是在java中,Object只是所有引用类型的超类(引用类型的根),而基本数据类型并不是类层级结构的一部分。这意味着当你需要Object的时候,不得不使用像Integer这样的包装类型来表示基本数据类型的值。而在kotlin中,Any是所有类型的超类型(所有类型的根),包括像Int这样的基本数据类型。

        注意Any是非空类型,所以Any类型的变量不可以持有null值。在kotlin中如果你需要持有任何可能值的变量,包括null在内,必须使用Any?类型。

        在底层,Any类型对应Object。kotlin把java方法参数和返回类型中用到的Object类型看作Any(更确切地是当做平台类型,因为其可空性是未知的)。当kotlin函数使用Any时,它会被编译成java字节码中的Object。

        在第四章学习了,所有kotlin类都包含下面三个方法:toString、equals和hashCode。这些方法都继承自Any。Any并不能使用其他Object的方法(比如wait和notify),但可以通过手动把值转换成Object来调用这些方法。

5、Unit类型:Kotlin的void

        kotlin中的Unit类型完成了java中的void一样的功能。当函数没有什么有意义的结果需要返回时,它可以用作函数的返回类型:

         大多数情况下,你不会留意到void和Unit之间的区别。如果你的kotlin函数使用Unit作为返回类型并且没有重写泛型函数,在底层它会被翻译成旧的void函数。如果你要在java代码中重写这个函数,新的java函数需要返回void。

        Unit是一个完备的类型,可以作为类型参数,而void却不行。只存在一个值是Unit类型,这个值也叫做Unit,并且(在函数中)会被隐式地返回。当你在重写返回泛型参数的函数时这非常有用,只需要让方法返回Unit类型的值

        和java对比一下,java中为了解决使用“没有值”作为类型参数的任何一种可能解法,都没有kotlin的解决方案这样漂亮一种选择是使用分开的接口定义来分别表示需要和不需要返回值的接口(如Callable和Runnable)另一种是用特殊的Void类型作为类型参数。即使你选择了后面这种方式,你还是需要加入一个return null;语句来返回唯一能匹配这个类型的值,因为只要返回类型不是void,你就必须始终显式地return语句

        你也许会好奇为什么kotlin选择使用一个不一样的名字Unit而不是把它叫做Void。在函数式编程语言中,Unit习惯上被用来表示“只有一个实例”,这正式kotlin的Unit和java的void的区别。我们本可以沿用Void这个名字,但是kotlin中还有一个叫做Nothing的类型,它有完全不同的功能。Void和Nothing两种类型的名字含义如此相近,会令人困惑,所以用Unit而不用Void。

6、Nothing类型:这个函数永不返回

      对某些kotlin函数来说,“返回类型”的概念没有任何意义,因为它们从来不会成功地结束。例如,许多测试库都有一个叫做fail的函数,它通过抛出带有特定消息的异常来让当前测试失败。一个包含无限循环的函数也永远不会成功地结束。

        当分析调用这样函数的代码时,知道函数永远不会正常终止是很有帮助的。kotlin使用一种特殊的返回类型Nothing来表示:

        Nothing类型没有任何值,只有被当做函数返回值使用,或者被当作泛型函数返回值的类型参数使用才会有意义。在其他所有情况下,声明一个不能存储任何值的变量时没有意义的

        注意,返回Nothing的函数可以放在Elvis运算符的右边来做先决条件检查

         上面这个例子展示了在类型系统中使用Nothing为什么极其有用。编译器知道这种返回类型的函数从不正常终止,然后再分析调用这个函数的代码时利用这个信息。在上面这个例子中,编译器会把address的类型推断成非空,因为它为null时的分支处理会始终抛出异常

3、集合和数组

1、可空性和集合

        kotlin支持类型参数的可空性。就像变量的类型可以加上?字符来表示变量可以持有null一样,类型在被当做类型参数时也可以用同样的方式标记。            List<Int?>是能持有Int?类型值的列表:换句话说,可以持有Int或者null。

         在第一种情况下,列表本身始终不为null,但列表中的每个值都可以为null。第二种类型的变量可能包含空引用而不是列表实例,但列表中的元素保证是非空的。

        在另一种上下文中,你可能需要声明一个变量持有可空的列表,并且包含可空的数字。kotlin中的写法是List<Int?>?, 用两个问号。使用变量自己的值的时候,以及使用列表中每个元素的值的时候,都需要使用null检查。

         遍历一个包含可空之的集合并过滤掉null是一个非常常见的操作,因此kotlin提供了一个标准库函数filterNotNull来完成。

        当然,这种过来也影响了集合的类型。validNumbers的类型是List<Int>,因为过滤保证了集合不会再包含任何为null的元素。

        最后,来看看filterNotNull库函数的源码实现

/**
 * Returns a list containing all elements that are not `null`.
 * 
 * @sample samples.collections.Collections.Filtering.filterNotNull
 */
public fun <T : Any> Iterable<T?>.filterNotNull(): List<T> {
    return filterNotNullTo(ArrayList<T>())
}

/**
 * Appends all elements that are not `null` to the given [destination].
 */
public fun <C : MutableCollection<in T>, T : Any> Iterable<T?>.filterNotNullTo(destination: C): C {
    for (element in this) if (element != null) destination.add(element)
    return destination
}

 这里我尝试自己实现了一个简化版本:

fun <T : Any> Iterable<T?>.filterNotNull(): List<T> {
    val newList = ArrayList<T>()
    this.forEach {
        it?.let { newList.add(it) }
    }
    return newList
}

2、只读集合与可变集合

        kotlin的集合设计和java不同的另一项重要特质是:它把访问集合数据的接口和修改集合数据的接口分开了。这种区别存在于最基础的使用集合的接口之中:Collection。使用这个接口,可以遍历集合中的元素、获取集合大小、判断集合中是否包含某个元素,以及执行其他从该集合中读取数据的操作。但这个接口没有任何添加或移除元素的方法

        使用MutableCollection接口可以修改集合中的数据。它继承了普通的Collection接口,还提供了方法来添加和移除元素、清空集合等。

         一般的规则是,在代码的任何地方都应该使用只读接口,只在代码需要修改集合的地方使用可变接口的变体

        就像val和var之间的分离一样,只读集合接口和可变集合接口的分离能让程序中的数据发生的事情变得更容易理解。如果函数接受Collection而不是MutableCollection作为形参,你就知道它不会修改集合,而只是读取集合中的数据。如果函数要求你传递给它MutableCollection,可以认为它将会修改数据。        使用集合接口时需要牢记的一个关键点只读集合不一定是不可变的如果你使用的变量拥有一个只读接口类型,它可能只是同一个集合的众多引用中的一个。任何其他的引用都可能拥有一颗可变接口类型

        如果你调用了这样的代码,它持有其他指向你集合的引用,或者并行地运行了这样的代码。你依然会遇到这样的情况,你正在使用集合的时候它被其他代码修改了,这会导致concurrentModificationException错误和其他一些问题。因此,必须了解只读集合并不总是线程安全的 如果你在多线程环境下处理数据,你需要保证代码正确地同步了对数据的访问,或者使用支持并发访问的数据结构

  3、kotlin集合和java

        在kotlin和java之间转移并不需要转换:不需要包装器也不需要拷贝数据。但是每一种java集合接口在kotlin中都有两种表示:一种是只读的,另一种是可变的。        

        上图中所有集合接口都是在kotlin中声明的。kotlin中只读接口和可变接口的基本结构与java.util中的java集合接口的结构是平行的。可变接口直接对应java.util包中的接口,而它们的只读版本缺少了所有产生可变的方法。 

        上图还包含了java类的java.util.ArrayList和java.util.HashSet,展示了kotlin是怎样对待java标准类的。在kotlin看来,它们分别继承自MutableList接口和MutableSet接口。这里没有展示其他java集合库的实现(LinkedList、SortedSet等),但从kotlin的角度来看,它们都有相似的超实现。这样你可以鱼与熊掌兼得,即得到了兼容性,也得到了可变接口和只读接口之间情绪的分离。

        除了集合之外,kotlin中Map类(它并没有继承Collection或是Iterable)也被表示成了两种不同的版本:Map和MutableMap。

        当你需要调用一个java方法并把集合作为实参传给它时,可以直接这么做,不需要任何额外的步骤,例如你有一个java.util.Collection做形参的java方法,你可以把任意Collection或MutableCollection的值作为实参传递给这个形参。

        这对集合的可变性有重要影响。因为java并不区分只读集合和可变集合,即使kotlin中把集合声明成只读的,java代码也能够修改这个集合。kotlin编译器不能完全地分析java代码到底对集合做了什么,因此kotlin无法拒绝向可以修改集合的java代码传递只读Collection

         下面的代码组成了一个跨语言兼容的kotlin/java程序:

        因此,如果你写了一个kotlin函数,使用了集合并传递给了java,你有责任使用正确的参数类型,这取决于你调用的java代码是否会修改集合(没看懂

         注意,上面注意事项也适用于包含非空类型元素的集合类如果你向java方法传递了这样的集合,该方法就可能在其中写入null值:kotlin没有办法在不影响性能的情况下,禁止它的发生,或者察觉它的发生。因此,当你向可以修改集合的java代码传递集合的时候,你需要采取特别的预防措施,来确保kotlin类型正确地反映出集合上所有可能的修改

        总结一句话,就是java代码能够修改kotlin集合的可变性,所以你需要在kotlin代码中对java代码调用完之后再对集合的类型,元素值等做校验等预防措施

4、作为平台类型的集合

        上面讲了java中对kotlin集合的影响,现在来看看kotlin如何处理java代码中声明的集合。        

        前面学习过了kotlin把那些定义在java中的类型看成平台类型。kotlin没有任何关于平台类型的可空信息,所有编译器允许kotlin代码将其视为可空或者非空。同样,java中声明的集合类型的变量也被视为平台类型。一个平台类型的集合本质上就是可变性未知的集合——kotlin代码将其视为只读或者可变的。通常这并不重要,因为实际上你想要执行的所有操作都能正常工作。

        当你重写或者实现签名中有集合类型的java方法时这种差异才变得重要。这里,像平台类型的可空性一样,你需要决定使用哪一种kotlin类型来表示这个java类型,它来自你要重写或实现的方法。

        这种情况下,你要做出多种选择,它们都会反映在产生的kotlin参数类型上:

  •         集合是否可空?
  •         集合中的元素是否可空?
  •         你的方法会不会修改集合?

        为了看到差异,考虑下面这些情况。第一个例子中,一个java接口表示一个能处理文件中文本的对象。

         下面就是这种实现看起来的样子:

        注意,同样的java类型——List<String>——如何表示成了两种不同的kotlin类型:一种是List<String>?(包含字符串的可空列表),另一种是MutableList<String?>(包含可空字符串的可变列表)。为了做出正确的选择,你必须知道java接口或者类必须遵守的确切契约。基于你的实现要做的事情这通常很容易理解。

        再来看看数组。如前所述,默认情况下,你应该优先使用集合而不是数组。但是因为还有大量java API仍然在使用数组,我们将介绍如何在kotlin中使用它们。

5、对象和基本数据类型的数组

        kotlin中的一个数组是一个带有类型参数,其元素类型被指定为相应的类型参数。 

        要在kotlin中创建数组,有下面这些方法供你选择:

  • arrayOf函数创建一个数组,它包含的元素是指定为该函数的实参;
  • arrayOfNulls创建一个给定大小的数组,包含的是null元素。当然,它只能用来创建包含元素类型可空的数组;
  • Array构造方法接受数组的大小和一个lambda表达式,调用lambda表达式来创建每一个数组元素。这就是使用非空元素类型来初始化数组,但不用显示地传递每个元素的方式

         lambda接收数组元素的下标并返回放在数组下标位置的值。这里你把字符‘a’加上下标并把结果转换成字符串来计算出数组元素的值。为了清楚起见,这里显示了数组元素的类型,但在真实的代码中可以省略。因为编译器可以推导出它的类型。

        说到这里,kotlin代码中最常见的创建数组的情况之一是需要调用参数为数组的java方法时,或是调用带有vararg参数的kotlin函数在这种情况下,通常已经将数组存储在集合中,只需要将其转换为数组即可。可以使用toTypedArray方法来执行此操作

        和其他类型一样,数组类型的类型参数始终会变成对象类型。因此,如果你声明了一个Array<Int>,它将会是一个包含装箱整形的数组(它的java类型是Integer[])。如果你需要创建没有装箱的基本类型的数组,必须使用一个基本数据类型数组的特殊类。 

        为了表示基本数据类型的数组,kotlin提供了若干独立的类,每一种基本数据类型都对应一个。例如Int类型值的数组较多IntArray。kotlin还提供了ByteArray、CharArray、BooleanArray等给其他类型。所有这些类型都被编译成普通的java基本数据类型数组,比如int[]、byte[]、char[]等。因此这些数组中的值存储时并没有装箱,而是使用了可能的最高效的方式

        或者,假如你有一个持有基本数据类型装箱后的值的数组或者集合,可以用对应的转换函数把它们转换成基本数据类型的数组,比如toIntArray

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值