第6章Kotlin的类型系统

Kotlin的类型系统着重防止NullPointerException,通过可空性、安全调用运算符(?.)、Elvis运算符(?:)、非空断言(!!)等工具确保安全。类型参数默认是可空的,可以通过as?进行安全转换。延迟初始化属性(lateinit)用于避免初始化的繁琐。平台类型允许与Java代码交互时处理可空性不确定性,而Any和Unit分别代表所有类型的超类型和无返回类型。Kotlin的集合与数组支持只读和可变,提供多种创建和操作方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第6章Kotlin的类型系统.png

前述

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

6.1、可空性

可空性是Kotlin类型系统中帮助你避免NullPointerException错误的特性,Kotlin解决这类问题的方法是把运行时的错误变成编译器的错误

1、可空类型标识符?

  • Kotlin和Java的类型系统之间第一条也可能是最重要的一条区别是,Kotlin对可空类型的显式支持

  • 这是一种指出程序中哪些变量和属性允许为null的方式

  • 如果一个变量可以为null,对变量的方法的调用就是不安全的,因为这样会导致nullPointException,Kotlin不允许这样的调用,因而可以阻止许多可能的异常

  • 例子

    de2ff0878631900ef94fd08fc67d391116efbcaf8032dade221819a3cab06ea3.png

    • 使用可能为null的实参调用strLen是不允许的,在编译器就会被标记成错误
      image.png

    • 这个函数中的参数被声明成String类型,在Kotlin中这表示它必须包含一个String实例。这一点由编译器强制实施,所以不能传一个包含null的实参,这样就保证了strLen函数永远不会在运行时抛出nullPointerException

    • 如果允许调用这个方法的时候传给它所有可能的实参,包括那些可以为null的实参,需要显式地在类型名称后面加上问号来标记它:
      712cc08a182eda1185021bb24fabccec532299bd3bfc9291c33e8f0c25b4e8b1.png

    • 问号可以加在任何类型的后面来表示这个类型的变量可以存储null引用:String?、Int?、MyCustomType?等等

      image.png

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

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

    • 也不能把它赋值给非空类型的变量

      23a2601e45e5f04e51b260890b93bcad7a5c92963bd69dec40cd0ab829c3fb5c.png

    • 也不能把可空类型的值传给拥有非空类型参数的函数

      f8b6dbef9e09fbfd7d9035c3f711ae4dec8a8eeaca991948078fa3a2ad5c834e.png

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

      • 例子

        e7de5edf1395da86aa521873a195042eb5c6e95c467ddee1cb8d05a380d02488.png
        fb7b1e49a77c6d9be9b18a05893277c1a3dd960a01cd603da4df57da082dc983.png

2、类型的含义

  • 维基百科关于类型的定义:类型就是数据的分类…决定了该类型可能的值,以及该类型的值上可以完成的操作

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

  • ?.安全运算符允许把一次null检查和一次方法调用合并成一个操作

  • 例如表达式 s?.toUpperCase()等同于下面这种烦琐的写法:if(s!=null) s.toUpperCase() else null

  • 换句话说,如果你试图调用一个非空值的方法,这次方法调用会被正常地执行

  • 如果值是null,这次调用不会发生,而整个表达式的值为null

    3b3170f3a34c1936807e7768a1721dd301abb2017c03e16f0b18ffcb647cfeee.png

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

    • 例子

      0e43249377f9dd203eee6270b94d8d607a1b7f35aaed0f509926b8274f371fab.png
      013ac096790be298cadb92501abe46f84c954fb18be1c23d82add64057ac8762.png
      bb5e1ede50a182b5e2334a5de9460cc0efb1d1d4298719c64770fbe03ab274fd.png

  • 安全调用不仅可以调用方法,也能用来访问属性

    • 例子

      • 一个具有可空属性的简单Kotlin类,以及访问这个属性时安全调用运算符
        e72001fce05e7638ed7b1e822dcd02a62eee33b0d78f2188973d19fccdacc154.png
        e58cfd2de1c0a4239e7b63754f33ac63a8fc468f2fe13c6e5e0294055268f67b.png
        d68d9d1c412f4cf6c1ba3050024f4c968db882217f29212c39c8814d8d2a2de8.png
  • 如果对象图中有多个可空类型的属性,通常可以在同一个表达式中方便地使用多个安全调用

    • 例子

      • 链接多个安全调用
        be80b75345274586720fad89d1253a97bb12a081460926d7ba7d61927d44d833.png
        3de69837899347ecd129b18343c01ff3a85b5ebeae713001ef478b53fe25cc67.png
        8dd8acb0fb8be8dfc2a5067c2092f72ae06594e7f3bf56847bfdb9c8fead2461.png

4、Elvis运算符:“?:”

5b08c918e787cd48ce87032fa5e15a90daf63fd8de1cf0587752ce5acb52ee4a.png

  • Kotlin有方便的运算符来提供代替null的默认值,它被称为Elvis运算符(或者null合并运算符)

  • 使用

    c106feafe28476598e6d19cd0c17fd306e0dc3d778e810e30c0b83e3d9dff231.png

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

      image.png

    • 例子

      b972bca566208a7899f5b6e931807d1371084eb720950d118d9bc29dbd8100f7.png
      635bb26b6da7f2934e2117c94bff2df804741c588f28e8759b6e780f18763bb7.png
      ad34241f843624714d24bf8987f7b446e8f4b9ff00882a4e385de398eec6e0a6.png

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

    • 例子

      c61470b77c1f0f88166f6f43e632cbc16a80a92354b8db97d6638b5461cdb2a2.png
      abfea3d28cb1dd0e5ab802dea228695e651a4e49939b7d4ad1674f7d3cef6b64.png
      e80ed99204a435570ba1d394021efdd1aa0d65e7f12d821ae5ca855d04256cd2.png

  • 使用场景

    • return和throw这样的操作很适用

    • 如果Elvis运算符左边的值为null,函数就会立即返回一个值或者抛出一个异常。如果函数中需要检查先决条件,这个方式特别有用

    • 例子

      • 打印包含个人公司地址的出货标签函数
        8201e83691b2e415dbfb9012ce82ea440c5a86e3e6dbf28413539e02668546c2.png
        4c0180cf47b09c1f95cd3b6bb2a4571c6dea752baa9d6d1f975d7b7e2be8b45a.png
        40302fc1f1fd205fe08b623b1d3009ba4ef75002f76eb429704a425a9f95f038.png
      • 如果一切正常,函数打印标签,如果地址不存在,它会报告一个有意义的错误。留意前一章中见过的with函数如何被用来避免在这一行中重复四次address的

5、安全转换:“as?”

  • 和常规的Java类型转换一样,如果被转换的值不是你试图转换的类型,就会抛出ClassCastException异常。当然可以结合is检查来确保这个值拥有合适的类型

  • Kotlin有更优雅的解决方案

  • as?运算符尝试把值转换成指定的类型,如果值不是合适的就返回null

    f7fc71f9f27c412620bd965bbb40c67553d0850290e51a455334d4272ad135e9.png

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

    d4f9d463febfed099d5d2f9a68d48f95e0b093501180e170e2367243eab3335a.png
    5167085babc6af0b880c458772632b56dcd4e49a9b38e5161741fba542f346ab.png
    69555948728041d697ceb9ec4f7a00f7c16be660ef45e28571fa76a424f86e2a.png

    • 使用这种模式,可以非常容易地检查实参是否是适当的类型,转换它,并在它的类型不正确时返回false,而且这些操作全部在同一个表达式中
    • 当然,这种场景下智能转换也会生效:当你检查过类型并拒绝了null值,编译器就确定了变量otherPerson值的类型是Person2并让你能够相应地使用它
  • 安全调用、安全转换和Elvis运算符都非常有用,它们出现在Kotlin代码中的频率非常高

6、非空断言:“!!”

  • 非空断言是Kotlin提供的最简单直率的处理可空类型值的工具

  • 它使用双双感叹号表示,可以把任意值转换成非空类型。如果对null值做出非空断言,则会抛出异常

    64fed31b6f940588cbe307356c395308a6ea971dc63506d8c87b0dbdca8ef6f7.png

  • 例子

    d03cceef4daa1143b7cbb7d39cd53a8fdb8aab5b97baebbb14c17f768c7aa354.png
    16fca44085cd6d1562b0d2a95b81ce0bfa42d06a595d3d13a448b6337c21f166.png
    984c3e3b8e829b4be2123fb1c7f9f433aea406ed788c30dce148d445b3f1e50f.png

    • 函数中s为null的时候,Kotlin会在运行时抛出一个异常,但是注意异常抛出的位置是非空断言所在的那一行,而不是接下来试图使用那个值的一行
  • 注意

    • 可能双感叹号看起来有点粗暴:就像在冲着编译器咆哮。这是有意为之。Kotlin的设计者试图说服你思考更好的解决方案,这些方案不会使用断言这种编译器无法验证的方式
  • 某些问题适合用非空断言来解决。当你在一个函数中检查一个值是否为null,而在另一个函数中使用这个值时,这种情况下编译器无法识别这种用法是否安全。如果你确信这样的检查一定在其他某个函数中存在,你可能不想在使用这个值之前重复检查,这时就可以使用非空断言

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

    99b6682f58ddbf195b6d4216e6bcacc322f8ab7d193b0fbed8ca66052826b9e2.png

运算符小结
运算符含义例子其他说明
可空类型标识符val s:String?=nullimage.png
?.安全调用运算符,先检查非空,然后再进行方法调用s?.toUpperCase()等同于if(s!=null)s.toUpperCase()3b3170f3a34c1936807e7768a1721dd301abb2017c03e16f0b18ffcb647cfeee.png
?:Elvis运算符,代替null的默认值image.pngimage.png
as?安全转换运算符,as?尝试把值转换成指定的类型,如果值不是合适的就返回nullimage.pngf7fc71f9f27c412620bd965bbb40c67553d0850290e51a455334d4272ad135e9.png
!!非空断言,简单直率处理可空类型,如果值为null,显示抛出异常image.png64fed31b6f940588cbe307356c395308a6ea971dc63506d8c87b0dbdca8ef6f7.png

7、“let”函数

  • let函数让处理可空表达式变得更加容易

  • 和安全调用运算符一起,它允许你对表达式求值,检查求值结果是否为null,并把结果保存为一个变量。所有这些动作都在同一个简洁的表达式中

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

    • 例子

      • 比如说有一个sendEmailTo函数,它接收一个String类型的参数并向这个地址发送一封邮件
        32192d12f96d3e5956e3be4fd72c015397ac069f3c8b2c9478e1ef2f7367b8fd.png

      • 不能把可空类型的值传给这个函数
        b1588dda3c8fa0daa18d789f3a7eb3571543cdbe91bd1e4c59297bd6323acf50.png

      • 必须显式地检查这个值不为null
        4ec5a27eae1f606d4a81a3a93ac20bd5dd931f815b972f932c51588a11e953e2.png

      • 但还有另外一种处理方式:使用let函数,并通过安全调用来调用它

      • let函数做的所有事情就是把一个调用它的对象编程lambda表达式的参数。如果结合安全调用语法,它能有效地把调用let函数的可空对象,转变成非空类型

        10543d38630c6357a2b7860fe5d0722ea5db79f40973d563eb9583564c931cbc.png

      • let函数只在email的值非空时才被调用,所以能在lambda中把email当做非空的实参使用

        22ad62606ac243309a9be64d3e0b3c8073a3dcba2e2cc1f21518ac0add9eda91.png

      • 使用自动生成的名字it这种简明语法之后,代码可变成

        c10788c579ae07ed3ac5b699ed5fc97ae1fff6247a9f5327bbdbb762ba248b2f.png

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

        6073f1e7e7fcb34727723915de615d576d70c1b79ec87eaf25bb10cb89467297.png
        d942e87c5e4fa5ef2f88063c85cc4650e0e34d41985d0755d891bfddd64ab08c.png

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

8、延迟初始化属性

  • 很多框架在对象实例实例创建之后用专门的方法来初始化对象

  • 但是你不能在构造方法中完全放弃非空属性的初始化器,仅仅在一个特殊的方法里初始化它

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

  • 例子

    • 使用非空断言访问可空属性

      5c215d8bee8389d73e1db9a752da08c863679ba35a8bd49507643aa364227cc9.png

      • 反复使用这个属性的时候,需要反复检查可空性
  • 为了解决这个问题,可以把myService属性声明成可以延迟初始化的

  • 使用lateinit修饰符来完成这样的声明

  • 例子

    35304602aa35700da2620a801f45803e016d820903cc59249d88e5413b73ebbc.png

    • 延迟初始化的属性都是var,因为需要在构造方法外修改它的值,而val属性会被编译成必须在构造方法中初始化的final字段
    • 尽管这个属性是非空类型,但是不需要再构造方法中初始化
    • 如果在属性被初始化之前就访问了它,会得到这个异常“lateinit propery myService has not been initialized”
    • lateinit属性常见的一种用法是依赖注入。在这种情况下,lateinit属性的值是被依赖注入框架从外部设置的。为了保证和各种Java(依赖注入)框架的兼容性,Kotlin会自动生成一个和lateinit属性具有相同可见性的字段。如果属性的可见性是public,生成字段的可见性也是public

9、可空类型的扩展

  • 为可空类型定义扩展函数是一种更强大的处理null值的方式。可以允许接收者为null的(扩展函数)调用,并在该函数中处理null,而不是在确保质量不为null之后再调用它的方法。

  • 只有扩展函数才能做到这一点,普通成员方法的调用是通过对象实例来分发的,因此实例为null的(成员方法)永远不能被执行

  • Kotlin标准库中定义的String的两个扩展函数isEmpty和isBlank就是这样的例子

  • 例子

    de2df63e3c3105773bdfbb246b35049bee3315d82500ac336b371be661056551.png
    a1043c127d7c1e11a38d6bdab168c664b9180fb973f0e11f19715e802fab2e8a.png
    bd78277a084f6d11c16b1a1801496e2854b1d6a4381782fd77eafcb7df4f2c20.png

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

      646073ec3ac95943d06391e68b633f7dd5d1b60226541f041021d37cb7049f29.png

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

    • 当你为一个可空类型(以?结尾)定义扩展函数时,这意味着你可以对可空的值调用这个函数;并且函数体中的this可能为null,所以你必须显式地检查。

    • 在Java中,this永远是非空的,因为它引用的是当前你所在的类的实例。而在Kotlin中,这并不永远成立:在可空类型的扩展函数中,this可以为null

  • 注意

    • 当你定义自己的扩展函数时,需要考虑该扩展是否需要为可空类型定义。默认情况下,应该把它定义成非空类型的扩展函数。如果发现大部分情况下需要再可空类型上使用这个函数,你可以稍后再安全地修改它(不会破坏其他代码)

10、类型参数的可空性

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

  • 例子

    • 处理可空类型参数

      68156e56f3d9a6fcd350877ac831a59931852e950464b56ef68da6e27592c855.png
      19f4d451300bf03d20b2e638e51a2f5b06c9661088112e0d6c274df574409f4b.png
      de98cb4e4a2e21f8dfc4918023486e6068994320d060dac5f5c15f39cf44d6ca.png

      • 在printlnHashCode调用中,类型参数t推导出的类型是可空类型Any?。因此,尽管没有用问号结尾,实参t依然允许持有null
      • 要使类型参数非空,必须要为它指定一个非空的上界,那样泛型会拒绝可空值作为实参
    • 为类型参数声明非空上界

      c5d5c9404770a8c060019727178eb7d91e97c010244224bc26e7086b100ac1c5.png
      f8163b43aba8992f0fa7292f4374809fae1c20506b1db8876e06546cab9a9fd2.png

  • 注意

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

11、可空性和Java

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

    5b656c3573b64355faf2590f29ea54baaa183084912e1c22fda2875435f5d27b.png

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

  • 如果这些注解不存在会发生什么?这种情况下,Java类会变成Kotlin中的平台类型

  • 平台类型

    • 平台类型本质上就是Kotlin不知道可空性信息的类型。既可以把它当做可空类型处理,也可以当做非空类型处理

    • 这意味着,你要像在Java中一样,对你在这个类型上做的操作负有全部责任。编译器将会允许所有的操作,它不会把这些值的空安全操作高亮成多余的,但它平时却是这样对待非空类型值上的空安全操作的。如果你认为这个值为null,在使用它之前可以用它和null比较,如果你认为它不为null,就直接使用它。就像在Java中一样,如果你错误地理解了这个值,使用的时候就会遇到NullPointerException

      fe8df7766f10e08ea65016d459c3b518036c29300f8b715a3887321cc06dfe34.png

  • 事实上,对于公有的Kotlin函数,编译器会生成对每个非空类型的参数(和接收者)的检查,所以,使用不正确的参数的调用尝试都会立即被报告为异常

  • 注意,这种值检查在函数调用的时候就执行了,而不是等到这些参数被使用的时候

  • 为什么需要平台类型?

    • 对Kotlin来说,把来自Java的所有值都当成可空的是不是更安全?这种设计也许可行,但是这需要对永远不为空的值做大量冗余的null检查,因为Kotlin解释器无法了解到这些信息
    • 涉及泛型的话这种情况就更糟糕了,例如,在Kotlin中,每个来自Java的ArrayList都被当作ArrayList<String?>?,每次访问或者转换类型都需要检查这些值是否为null,这将抵消掉安全性带来的好处。编写这样的检查非常令人厌烦,所以Kotlin的设计者做出了更实用的选择,让开发者负责正确处理来自Java的值
  • 在Kotlin中不能声明一个平台类型的变量,这些类型只能来自Java代码

  • 创建混合的Kotlin和Java类层级关系时会遇到的一些陷阱

    • 继承

      • 在Kotlin中重写Java的方法时,可以选择把参数和返回类型定义成可空的,也可以选择把它们定义成非空的。例如我们来看一个Java中的StringProcessor接口

        8f3b35b977a23bd2f4dcbabab00df8a7cff94052f5bfc9b4fe375c148e245797.png
        bc65d0e9fee10be895e4a063679aff4baa64b3c4a8e010c48ac002947b341f79.png

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

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

这一节讲讨论Kotlin中的基本类型,例如Int、Boolean和Any

与Java不一样的是,Kotlin并不区分基本数据类型和它们的包装类

稍后你会了解到背后的原因和底层的运作机制

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

  • Kotlin并不区分基本数据类型和包装类型,使用的永远是同一个类型

    29349f8caae04b8ca74b5f62419a09b6bb7a810506ac95778b98b33f3ade0b97.png

  • 此外,还能对一个数字类型的值调用方法,例如下面这段代码中,使用了标准库的函数coerceIn来把值限制在特定范围内:

    0f757c34f5d8cba21be3b6021998f11555e3730534710d8775c1e8f31a9d25c1.png

  • 如果基本数据类型和引用类型是一样的,是不是意味着Kotlin使用对象来表示所有的数字?Kotlin并没有这样做

  • 在运行时,数字类型会尽可能地使用最高效的方式来表示。大多数情况下-对于变量、属性、参数和返回类型-Kotlin的Int类型会被编译成Java基本数据类型int

  • 唯一不可行的是例外是泛型类,比如集合。用作泛型类型参数的基本数据类型会被编译成对应的Java包装类型

    • 例如Int类型被用作集合类的类型参数时,集合类将会保存对应包装类型java.lang.Integer的实例
  • 对应到Java基本数据类型的类型完整列表如下

    • 整数类型-Byte、Short、Int、Long
    • 浮点数类型-Float、Double
    • 字符类型-Char
    • 布尔类型-Boolean

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

  • Kotlin中的可空类型不能用Java的基本数据类型表示,因为null只能被存储在Java的引用类型的变量中

  • 这意味着任何时候只要使用了基本数据类型的可空版本,它就会编译成对应的包装类型

  • 泛型类是包装类型应用的另一种情况。如果你用基本数据类型作为泛型类的类型参数,那么Kotlin会使用该类型的包装形式

    • 例如,下面这段代码创建了一个Integer包装类的列表,尽管你从来没有指定过可空类型或者用过null值
    • val listOfInts= listOf(1,2,3)
    • 这是由Java虚拟机实现泛型的方式决定实现的
    • JVM不支持用基本数据类型作为类型参数,所以泛型类(Java和Kotlin一样)必须始终使用类型的包装表示
    • 因此,假如要高效地存储基本数据类型元素的大型集合,要么使用支持这种集合的第三方库(如Trove4J)要么使用数组来存储

3、数字转换

  • Kotlin和Java之间一条重要的区别就是处理数字转换的方式

  • Kotlin不会自动地把数字从一种类型转换成另外一种,即使是转换成范围更大的类型,而是需要显示转换

    • 例如,Kotlin中下面这段代码不会编译
      bce04f98599d36fb8ccc8dca0f4c3e2386eec2e3be349a69a941f427544c8c5a.png
      07c2fff16c82d3ee728f401a72793b1a0696d0918c9ea49ac20e8d8601021d37.png
  • 每一种基本数据类型(Boolean除外)都定义有转换函数:toByte()、toShort()、toChar()等,这些函数支持双向转换:既可以把小范围的类型扩展到大范围,比如Int.toLong(),也可以把大范围的类型截取到小范围,比如Long.toInt()

  • 为了避免意外情况,Kotlin要求转换必须是显式的,尤其是在比较装箱值的时候。比较两个装箱值的equals方法不仅会检查它们存储的值,还要比较装箱类型

  • 所以,在Java中new Integer(42).equals(new Long(42))会返回false

  • 基本数据类型字面值

    • Kotlin除了支持简单的十进制数字之外,还支持下面这些在代码中书写数字字面值的方式

      • 使用后缀L表示Long类型(长整型)字面值:123L
      • 使用标准浮点数表示Double(双精度浮点数)字面值:0.12、2.0、1.2e10、1.2e-10
      • 使用后缀F表示Float类型(浮点数)字面值:123.4f、.456F、1e3f
      • 使用前缀0x或者0X表示十六进制字面值:0xCAFEBABA或者0xbcdl
      • 使用前缀0b或者0B表示二进制字面值:0b000000101
    • 注意,Kotlin1.1才开始支持数字字面值中的下划线。对于字符字面值来说,可以使用和Java几乎一样的语法。把字符写在单引号中,必要时还可以使用转义序列

      • 下面几个例子都是有效的Kotlin字符字面值:‘1’、‘\t’(制表符)、‘\u0009’(使用Unicode转义序列表示的制表符)
    • 注意,当书写数字字面值的时候,一般不需要使用转换函数。一种可能性是用这种特殊的语法来显式地标记常量的类型,比如42L或者42.0f。而且即使你没有用这种语法,当你使用数字字面值去初始化一个类型已知的变量时,又或者把字面值作为实参传给函数时,必要的转换会自动地发生

    • 此外,算数运算符也被重载了,它们可以接收所有适当的数字类型

      • 例如,下面这段代码并没有任何显式转换,但可以正确地工作
        1cbb3443b9df401bb27402b7fb9a1ef6f83a28519f544218aeb79b9a00510e0d.png
      • 注意,Kotlin算术运算符关于数值范围溢出的行为和Java完全一致:Kotlin并没有引入由溢出检查带来的额外开销
    • 字符串转换

      • Kotlin标准库提供了一套相似的扩展方法,用来把字符串转换成基本数据类型(toInt、toByte、toBoolean等)
        89614835aa44a94322fa41460d2212397c2724ddd3217fcc586a8b547deafd16.png
        e7ef77e8c24ef8fbd2b29637d674b4a64afebcaaef328004eb8aeff31a6efe62.png
      • 每个这样的函数都会尝试把字符串的内容解析成对应的类型,如果解析失败则抛出NumberFormatException

4、“Any”和“Any?”:根类型

  • 和Object作为Java类层级结构的根差不多,Any类型是Kotlin所有非空类型的超类型(非空类型的根)

  • 但是在Java中,Object只是所有引用类型的超类型(引用类型的根),而基本数据类型并不是类层级结构的一部分。这意味着当你需要Object的时候,不得不使用像java.lang.Integer这样的包装类型来表示基本数据类型的值

  • 而在Kotlin中,Any是所有类型的超类型(所有类型的根),包括像Int这样的基本数据类型

  • 和Java一样,把基本数据类型的赋值给Any类型的变量时会自动装箱

    87c3b1211a0c2474005d08e50a8870d1dab18326b42c9ca7f357ba674b78934f.png

    • 注意Any是非空空类型,所以Any类型的变量不可以持有null值,在Kotlin中如果你需要可以持有任何可能值的变量,包括null在内,必须使用Any?类型
  • 在底层,Any类型对应java.lang.Object。Kotlin把Java方法参数和返回类型中用到的Object类型看作Any(更确切地是当做平台类型,因为其可空性是未知的)。当Kotlin函数使用Any时,它会被编译成Java字节码中的Object

  • 所有Kotlin类都包含下面三个方法:toString、equals和hashCode。这些方法都继承自Any。Any并不能使用其他java.lang.Object的方法(比如外套和notify),但是可以通过手动把值转换成java.lang.Object来调用这些方法

5、Unit类型:Kotlin的“void”

  • Kotlin中的Unit类型完成了Java中的void一样的功能

  • 当函数没什么有意义的结果要返回时,它可以用作函数的返回类型

    7a4be38d007d291e01cc641204458b8c3d0362e4dc18976912c84a41b981a14b.png

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

  • Kotlin的Unit和Java的void到底有什么不一样呢?Unit是一个完备的类型,可以作为类型参数,而void却不行

  • 只存在一个值是Unit类型,这个值也叫作Unit,并且(在函数中)会被隐式地返回。当你在重写返回泛型参数的函数时这非常有用,只需要让方法返回Unit类型的值

    1f730eaedd19722d319935c33008c6d4a0574c0383b40b3853c4334b26f753a2.png

    • 接口要求process函数返回一个值,而且,因为Unit类型确实有值,所以从方法中返回它并没有问题。然后你不需要在NoResultProcessor.process函数中写上显式的return语句,因为编译器会隐式地加上return Unit
  • 和Java对比一下,Java中为了解决使用“没有值”作为类型参数的任何一种可能解法,都没有Kotlin的解决方案这样漂亮。一种选择是使用分开的接口定义来分别表示需要和不需要返回值的接口(如Callable和Runnable)。另一种是用特殊的java.lang.Void类型作为类型参数。即使你选择了后面这种方式,你还是需要假如一个 return null;语句来返回唯一能匹配这个类型的值,因为只要返回类型不是void,你就必须始终有显式的return语句

  • 你也许会奇怪为什么我们选择使用一个不一样的名字Unit而不是把它叫作Void,在函数式编程语言中,Unit这个名字习惯上被用来表示“只有一个实例”,这正是Kotlin的Unit和Java的Void的区别,我们本可以沿用Void这个名字,但是Kotlin还有一个叫作Nothing的类型,它有着完全不同的功能

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

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

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

  • 例子

    abedc7c1cb332cfbaee47768b0db1742ccdb26d4de231d1cd452f884b33103be.png
    2c1515cb09863590fd8db09d38d3561168496301c9f93d861e7571e899bc8f7f.png
    61acd6cf0a607ef28348ec726059ea07d43133656c33128e4c765ffa992c591a.png

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

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

    9d8053101ec9caed7d625afc38c0a85f89ce4935006047347228b58379499805.png

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

6.3、集合和数组

1、可空性和集合

  • 类型在被当作类型参数时也可以用同样的方式标记

  • 例子

    • 这个函数从一个文件中读取文本行的列表,并尝试把每一行文本解析成一个数字

      e95337bcd6b8cec4546d2f130c841444bbeff1e1161cd4a0753454e7107df8c8.png

  • 注意,变量自己类型的可空性用作类型参数的类型的可空性是有区别的。包含可空Int的列表和包含Int的可空列表之前的区别如下:

    image.png

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

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

  • 例子,计算列表中有效数字之和,并单独对无效数字计数

image.png

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

image.png

2、只读集合与可变集合

  • Kotlin的集合设计和Java不同的另一项重要特质是,它把访问集合数据的接口和修改集合数据的接口分开了

  • 这种区别存在于最基础的使用集合的接口之中:Kotlin.collections.Collection。使用这个接口,可以遍历集合中的元素、获取集合的大小、判断集合中是否包含某个元素,以及执行其他从该元素中读取数据的操作。但这个接口没有任何添加或者移除元素的方法

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

    0d2ff4f1f2b00993a395549ce417f7d0e3e8c61b84ec479921cb955af4ff2824.png

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

  • 就像val和var之间的分离一样,只读集合接口和可变接口的分离能让程序中的数据发生的事情更容易理解。如果函数接收Collection而不是MutableCollection作为形参,你就知道它不会修改集合,而只是读取集合中的数据。如果函数要求你传递给它MutableCollection,可以认为它将会修改数据。如果你用了集合作为组件部状态的一部分,可能需要把集合先拷贝一份再传递给这样的函数(这种模式通常称为防御式拷贝)

  • 例如,可以清楚地看到,下面清单中的copyElements函数仅仅修改了target集合,而没有修改source集合

    47bed751a21b5b47a083c935ad728b0cdb9abda4b2cbdc3c47d68ce1d0d92b50.png

  • 使用集合接口时需要牢记的一个关键点是只读集合不一定是不可变的。如果你使用的变量拥有一个只读接口类型,它可能只是同一个集合的众多引用中的一个。任何其他的引用都可能拥有一个可变接口类型

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

3、Kotlin集合和Java

  • 每一个Kotlin接口都是其对应Java集合接口的一个实例,这种说法并没有错

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

    6b418bb84722c40969b43f6a0b66afc85e8f5ac6c96c4634dbd919af8dc1f7cd.png

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

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

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

    • 表6.1展示了可以用来创建不同类型集合的函数

      image.png

      • 注意,setOf()和mapOf()返回的是Java标准库中类的实例(至少在Kotlin1.0中是这样),在底层它们都是可变的。但你不能完全信赖这一点:Kotlin的未来版本可能会使用真正不可变的实现类作为setOf和mapOf的返回值

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

      • 这对集合的可变性有重要影响。因为Java并不区分只读集合和可变集合,即使Kotlin中把集合声明成只读的,Java代码也能够修改这个集合。Kotlin编译器不能完全分析Java代码到底对集合做了什么,因此Kotlin无法拒绝向可以修改集合的Java代码传递只读Collection。例如下面两段代码组成了一个跨语言兼容的Kotlin/java程序

        9687d908d80bd20f8e2e5c57888f37505bde95bca35931336a359feb75987cae.png

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

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

4、作为平台类型的集合

  • Kotlin把那些定义在Java代码中的类型看成平台类型

  • Kotlin没有任何关于平台类型的可空性信息,所以编译器允许Kotlin代码将其视为可空或者非空。同样,Java中声明的集合类型的变量也被视为平台类型

  • 一个平台类型的集合本质上就是可变性未知的集合—Kotlin代码将其视为只读的或者可变的。通常这并不重要,因为,实际上你想要执行的所有操作都能正常工作

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

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

      • 集合是否为空?
      • 集合中的元素是否可空?
      • 你的方法会不会修改集合?
  1. 例子一

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

          import java.io.File;
          import java.util.List;
      
          interface FileContentProcessor {
            void processContents(File path, byte[] binaryContents, List<String> textContents);
          }
      
    • 这个接口的Kotlin实现需要作出下面的选择:

      • 列表将是可空的,因为有些文件是二进制格式,它们的内容不能被表示成文本
      • 列表中的元素将会是非空的,因为文件中的每一行都永远不为null
      • 列表将是只读的,因为它表示的是文件的内容,而且这些内容不会被修改
    • 下面是Kotlin的实现

          /**
           * FileContentProcessor的Kotlin实现
           *
           */
          class FileIndexer:FileContentProcessor {
              override fun processContents(path: File, binaryContents: ByteArray?, textContents: MutableList<String>?) {
              }
          }
      
  2. 例子二

    • 把它和另外一个数据转换接口对比。数据转换接口的实现是从文本表单中解析出数据并放到一个对象列表中。再把这些对象附加到输出列表中。当发现解析错误时,就把错误信息添加到另一个单独的列表中,作为错误日志

    • java中的实现

      /**
       * 另一个使用集合参数的java接口
       * @param <T>
       */
      public interface DataParser<T> {
        void parseData(String input, List<T> output, List<String> errors);
      }
      
      • 这种情况下的选择是不同的:

        • List将是非空的,因为调用者总是需要接收错误消息
        • 列表中的元素将是可空的,因为不是每个输出列表中的条目都有关联的错误信息
        • List将是可变的,因为实现代码需要向其中添加元素
    • Kotlin中的实现

          /**
           * DataParser的Kotlin实现
           */
         class PersonParser:DataParser<Person2> {
             override fun parseData(input: String,
                        output: MutableList<Person2>?,
                        errors: MutableList<String>?) {
             }
         }
      
      • 注意,同样的Java类型LIst如何表示成了两种不同的Kotlin类型:一种List?(包含字符串的可空列表,列表可空), 另一种是MutableList<String?>(包含可空字符串的可变列表,列表内容可空)

      • 为了做出正确的选择,你必须知道Java接口或类必须遵守的确切契约。基于你的实现要做的事情这通常很容易理解

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

  • 默认情况下,你应该优先使用集合而不是数组

  • Kotlin数组的语法出现在了每个例子中,因为数组是Java main函数标准签名的一部分

  • 使用数组

      //使用扩展属性array.indices在下标范围内迭代
      for(i in args.indices){
          //通过下标使用array[index]访问元素
          println("Argument $i is: ${args[i]}")
      }
    }
    
  • Kotlin的数组是一个带有类型参数的类,其元素类型被指定为相应的类型参数

  • 要在Kotlin中创建数组,有下面这些方法可供选择

    1. arrayOf函数创建一个数组,它包含的元素是指定为该元素的实参

    image.png

    1. arrayOfNulls创建一个给定大小的数组,包含的是null元素。当然,它只能用来创建包含元素类型可空的数组

    2. Array构造方法接收数组的大小和一个lambda表达式,调用lambda表达式来创建每一个数组元素。这就是使用非空元素类型来初始化数组,但不用显式地传递每个元素的方式

        val letters = Array<String>(26) { i -> ('a' + i).toString() }
       //joinToString函数是Kotlin标准库函数中的扩展函数
       println(letters.joinToString(""))
      

      b9aee65506b99a85c7606895484c4ab5687276054dee4ea65b2052a3a4d368d6.png

      • Lambda接收数组元素的下标并返回放在数组下表位置的值
      • 为了清楚起见,这里显示了数组元素的类型,但在真实的代码中可以省略。因为编译器可以推导出它的类型
    3. Kotlin代码中最常见的创建数组的情况之一是需要调用参数为数组的Java方法时,或是调用带有vararg参数的Kotlin函数时。在这些情况下,通常已经将数据存储在集合中,只需将其转换为数组即可。可以使用toTypedArray方法来执行此操作

      ff0d30fb4d598aa15188d6e6c11eaab42d32c87152238a0382255cec97e3769e.png
      f603506a7c62a5d47022c365dd25546a908437df6e4ab62df1ba6d190147f616.png

  • 和其他类型一样,数组类型的类型参数始终会变成对象类型。因此,如果你声明了一个Array,它将会是一个包含装箱整型的数组(它的Java类型将是java.lang.Integer[])

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

    • 要创建一个基本数据类型的数组,有以下几种方法:

      1. 该类型的构造方法接受size参数并返回一个使用对应基本数据类型默认值(通常是0)初始化好的数组
      2. 工厂函数(IntArray的intArrayOf,以及其他数组类型的函数)接收变长参数的值并创建存储这些值的数组
      3. 另一种构造方法,接收一个大小和一个用来初始化每个元素的lambda
  • 例子

    • 创建存储了5个0的整型数组的两种方式

    16b620f6513d90fda243eec6c7aa395643e891d1c6192669b7feb30eb3249992.png

    • 接收lambda的构造方法的例子

      cccefd43fb5dbc94bf55e38563f41914afcb267eacdf16452844146a6939d4f4.png
      169839a8421f544ecca152182a849c9bb4127f84abc9f82a2112a3e6196e9404.png

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

  • 对数组的操作,处理基本操作(获取数组的长度,获取或者设置元素)外,Kotlin标准库支持一套和集合相同的用于数组的扩展函数

    • 例子:使用forEachIndexed函数加上lambda来重写代码清单6.30中的代码

      06f9ee58dfcad6dcf9abbb2364c6dffb39019002c3ecc4f8713d4395a12a5915.png
      8f514b0e612710250f1b6202eda716a541be236cc38b4f3c9934402d669424b2.png

6.4、小结

Kotlin对可空类型的支持,可以帮助我们在编译器,检测出潜在的NullPointerException错误

Kotlin提供了像安全调用(?.)、Elvis运算符(?:)、非空断言(!!)及let函数这样的工具来简洁地处理可空类型

as?运算符提供了一种简单的方式来把值转换成一个类型,以及处理当它拥有不同类型时的情况

Java中的类型在Kotlin中被解释成平台类型,允许开发者把它们当做可空或者非空来对待

表示基本数字类型(如Int)看起来用起来都像普通的类,但通常会被编译成Java基本数据类型

可空的基本数据类型(如Int?)对应着Java中的装箱基本数据类型(如java.lang.Integer)

Any类型是所有其他类型的超类型,类似于Java的Object。而Unit类比于void

不会正常终止的函数使用Nothing类型作为返回类型

Kotlin使用标准Java集合类,并通过区分只读和可变集合来增强它们

当你在Kotlin中继承Java类或者实现Java接口时,你需要仔细考虑参数的可空性和可变性

Kotlin的Array类就像普通的泛型类,但它会被编译成Java数组

基本数据类型的数组使用像IntArray这样的特殊类来表示

附件:

第6章Kotlin的类型系统.svg

Kotlin学习之旅开始啦

第1章Kotlin:定义与目的

第2章Kotlin基础

第3章函数的定义和调用

第4章类、对象和接口

第5章Lambda编程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值