Kotlin 类型系统详解:基本数据类型、集合与数组
详细讲解了 Kotlin 类型系统的核心组成部分,涵盖基本数据类型及其可空性、数字转换规则、Any/Unit/Nothing 特殊类型的作用。重点阐述了集合与数组的区别,包括只读与可变接口的分离设计、可空性与集合元素的关系、以及与 Java 集合的互操作性。文章还介绍了数组的创建方式及基本类型数组的性能优势,并结合最佳实践总结了如何在实际开发中利用类型系统提升代码安全性和效率。

详细讲解了 Kotlin 类型系统的核心组成部分,涵盖基本数据类型及其可空性、数字转换规则、Any/Unit/Nothing 特殊类型的作用。重点阐述了集合与数组的区别,包括只读与可变接口的分离设计、可空性与集合元素的关系、以及与 Java 集合的互操作性。文章还介绍了数组的创建方式及基本类型数组的性能优势,并结合最佳实践总结了如何在实际开发中利用类型系统提升代码安全性和效率。

Kotlin 并不区分基本数据类型和包装类型,你使用的永远是同一个类型。在运行时,数字类型会尽可能地使用最高效的方式来表示。Int 类型会被编译成 Java 基本数据类型 int。不可行的例外是泛型类,比如集合。用作泛型类型参数的基本数据类型会被编译成对应的 Java 包装类型。
整数类型
浮点数类型
字符类型
布尔类型
Kotlin 中的可空类型不能用 Java 的基本数据类型表示,因为 null 只能被存储在 Java 的引用类型的变量中。只要使用了基本类型的可空版本,它就会被编译成对应的包装类型。JVM 不支持用基本数据类型作为类型参数,所以泛型类必须始终使用类型的包装表示。
例如:List<Int?> 表示一个包含可空整数的列表,而 List<Int> 表示一个不包含 null 的整数列表。
Kotlin 不会自动地把数字从一种类型转换成另一种类型,必须进行显式转换。每一种基本数据类型(Boolean 除外)都定义有转换函数:toByte()、toShort()、toInt()、toLong()、toFloat()、toDouble()、toChar()。这些函数支持双向转换。
为了避免意外情况,Kotlin 要求转换必须是显式的,尤其是在比较装箱值的时候。比较两个装箱值的 equals 方法不仅会检查它们存储的值,还要比较装箱类型。在 Java 中 new Integer(42).equals(new Long(42)) 会返回 false,而在 Kotlin 中由于类型系统的严格性,这种混淆更容易被避免。
Kotlin 支持以下这些在代码中书写数字字面值的方式:
当你书写数字字面值的时候一般不需要使用转换函数,即使你没有用上面的这些语法,当你使用数字字面值去初始化一个类型已知的变量时,又或是把字面值作为实参传给函数时,必要的转换会自动发生。此外算术运算符也被重载了,它们可以接收所有适当的数字类型。
Any 是 Kotlin 所有非空类型的超类型。但在 Java 中,Object 只是所有引用类型的超类型,而基本数据类型并不是类层级结构的一部分。和 Java 一样,把基本数据类型的值赋给 Any 类型的变量时会自动装箱。Any 是非空类型,所以 Any 类型的变量不能持有 null 值。在 Kotlin 中如果你要持有任何可能值的变量,包括 null 就要使用 Any? 类型。在底层 Any 类型对应 Object。所有 Kotlin 类都包含 toString、equals、hashCode,这些方法都继承自 Any。但 Any 不能使用其他 Object 的方法,比如 wait 和 notify,但可以通过手动把值转换成 Object 来调用。
val anyValue: Any = 1
val nullableAny: Any? = null
println(anyValue.toString()) // 输出 "1"
Unit 类型完成了 Java 中 void 一样的功能,而且可以省略。大多情况下你不会留意到 void 和 Unit 之间的区别。如果你的 Kotlin 函数使用 Unit 作为返回类型并且没有重写泛型函数,在底层它会被编译成旧的 void 函数。它们的不同之处在于 Unit 是一个完备的类型,可以作为类型参数,而 void 不行。只存在一个值是 Unit 类型,这个值也叫做 Unit,并且在函数中会被隐式返回。当你在重写返回泛型参数的函数时非常有用,只需让方法返回 Unit 类型的值。
fun printHello(): Unit {
println("Hello")
}
// 等价于
fun printHello() {
println("Hello")
}
许多测试库都有一个叫做 fail 的函数,它通过抛出带有特定消息的异常来让当前测试失败,一个包含无限循环的函数也永远不会成功结束。
Nothing 类型没有任何值,只有被当作函数返回值使用,或者被当作泛型函数返回值的类型参数使用才会有意义。
注意!返回 Nothing 的函数可以放在 Elvis 运算符的右边来做先决条件检查。
fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
}
val name: String?
if (name == null) {
fail("Name cannot be null")
} else {
// 此处 name 已被推断为非空 String
println(name.uppercase())
}
对前后一致的类型系统来说有一点十分关键:知道集合是否可以持有 null 元素,和知道变量值是否可以为 null 同等重要。
List<Int?> 是能持有 Int? 类型值的列表:换句话说,可以持有 Int 或者 null。如果一行文本可以被解析,那么就向 result 列表中添加一个整数,否则添加 null。从 Kotlin 1.1 开始可以使用函数 String.toIntOrNull 来简化例子。
注意!变量自己类型的可空性和用作类型参数的可空性是有区别的,一个包含可空 Int 的列表和包含 Int 的可空列表是有区别的。List<Int?>是列表中的单个值是可空的,列表本身不为 null,但列表的每个值可以为 null,而 List?是整个列表是可空的。我们要小心决定什么是可空的,是集合元素还是集合本身。
遍历一个包含可空值的集合并过滤掉 null 是一个非常常见的操作,因此 Kotlin 提供了 filterNotNull 函数来完成它。这种过滤也影响了集合的类型,此时 List<Int?> 会变成 List<Int>,因为过滤保证了集合不会再包含任何为 null 的元素。
val numbers: List<Int?> = listOf(1, null, 3, null, 5)
val nonNullNumbers: List<Int> = numbers.filterNotNull()
与 Java 有别,Kotlin 把访问集合数据的接口和修改集合数据的接口分开了。这种区别存在于最基础的使用集合的接口之中:Collection。使用这个接口,可以遍历集合中的元素、获取集合大小、判断集合中是否包含某个元素,以及执行其他从该集合中读取数据的操作,但这个接口没有任何添加或移除元素的方法。使用 MutableCollection 接口可以修改集合中的数据。它继承了 Collection 接口,还提供了方法来添加和移除元素、清空集合等。
一般的规则是在代码的任何地方都使用只读接口,只在代码需要修改集合的地方使用可变接口的变体。
使用集合接口时牢记只读集合不一定是不可变的。如果你使用的变量有一个只读接口类型,他可能只是同一个集合的众多引用中的一个,任何其他的引用都可能拥有一个可变接口类型,所以只读集合并不总是线程安全的。
每一种 Java 集合接口在 Kotlin 中都有两种表示:一种是只读的,一种是可变的。
可变接口直接对应 java.util 包中的接口,而它们的只读版本中缺少了所有产生改变的方法。
注意!setOf 和 mapOf 返回的是 Java 标准类库中类的实例 (1.0 中是这样),在底层它们都是可变的,Kotlin 的未来版本可能会使用真正不可变的实现类作为它们两个的返回值。
当你需要调用一个 Java 方法并把集合作为实参传给它时,可以把任意只读或可变接口的值作为实参传递。因为 Java 并不会区分只读与可变集合,即使 Kotlin 声明为只读,Java 代码也能修改这个集合。所以,如果你写了一个 Kotlin 函数,使用了集合并传递给了 Java,你有责任使用正确的参数类型,这取决于你调用的 Java 代码是否会去修改集合。同时也要注意包含非空类型元素的集合类,如果你向 Java 方法传递了这样的集合,该方法就可能在其中写入 null 值,Kotlin 没有办法在不影响性能的情况下禁止它的发生。
我们提到过 Kotlin 把那些定义在 Java 代码中的类型看成平台类型,Kotlin 没有任何关于平台类型的可空型信息,所以编译器允许 Kotlin 代码将其视为可空或非空。同样 Java 中声明的集合类型的变量也被视为平台类型。一个平台类型的集合本质上就是可变性未知的集合,Kotlin 代码将其视为只读或者可变的,实际上你想要执行的所有操作都能正常工作。
当你重写或者实现签名中有集合类型的 Java 方法时这种差异才变得重要,你需要决定使用哪一种 Kotlin 类型来表示这个 Java 类型:
Kotlin 中的一个数组是一个带有类型参数的类,其元素类型被指定为相应的类型参数。
在 Kotlin 中创建数组:
lambda 接收数组元素的下标并返回放在数组下标位置的值,这里可以省略数组元素的类型。
Kotlin 代码中最常见的创建数组的情况之一是需要调用参数为数组的 Java 方法时,或是调用带有 vararg 参数的 Kotlin 函数时。在这些情况下通常已经将数据存储在集合中,只需将其转换成数组即可,可使用 toTypedArray 方法来执行此操作。
和其它类型一样,数组类型的类型参数始终会变成对象类型。如果你需要创建没有装箱的基本数据类型的数组,必须使用一个基本数据类型数组的特殊类。
Kotlin 提供了若干独立的类,如 Int 类型值的数组叫做 IntArray。还有 CharArray 等其他类型。所有这些类型都被编译成普通的 Java 基本数据类型数组,比如 int[] 等。因此这些数组中的值存储时并没有装箱,而是使用了可能的最高效的方式。
要创建一个基本数据类型的数组:
假如你有一个持有基本数据类型装箱后的值的数组或者集合,可以用对应的转换函数把它们转换成基本数据类型的数组,比如 toIntArray。
Kotlin 中也有一套和集合相同的用于数组的扩展函数,lambda 编程中的绝大部分函数也适用于数组,包括基本数据类型的数组,注意这些方法的返回值是列表不是数组。
在使用 Kotlin 类型系统时,遵循以下最佳实践可以提高代码质量和安全性:
?:) 或安全调用 (?.) 来处理 null 值,防止 NullPointerException。@JvmField 或显式注解来控制行为。通过掌握这些类型系统的基础知识,你可以编写出更安全、更高效且易于维护的 Kotlin 代码。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online