13.6.9.3 Kotlin 1.5.0 新变化

原文链接: https://kotlinlang.org/docs/whatsnew15.html

13.6.9.3 Kotlin 1.5.0 新变化

阅读 Kotlin 1.5.0 发行说明,了解新的语言特性,以及 Kotlin Multiplatform、JVM、Native、JS 的更新和 Gradle、Maven 的构建工具支持。

发布时间:2021 年 5 月 5 日

Kotlin 1.5.0 引入了新的语言特性、稳定的基于 IR 的 JVM 编译器后端、性能改进,以及稳定实验性特性和弃用陈旧特性等演进性变更。

你也可以在发布博客文章中查看这些变更的概述。

提示: 有关 Kotlin 发布周期的信息,请参见 Kotlin 发布流程。

语言特性

Kotlin 1.5.0 带来了在 1.4.30 中预览的新语言特性的稳定版本:

这些特性的详细说明可在这篇博客文章以及 Kotlin 文档的相应页面中找到。

支持 JVM record

Java 在不断快速演进,为了确保 Kotlin 与它保持互操作,我们为其最新特性之一——record 类——提供了支持。

Kotlin 对 JVM record 的支持包含双向互操作:

  • 在 Kotlin 代码中,你可以像使用带属性的普通类那样使用 Java record 类。
  • 要在 Java 代码中把 Kotlin 类当作 record 使用,请把它声明为 data 类并用 @JvmRecord 注解标记。
1
2
@JvmRecord
data class User(val name: String, val age: Int)

进一步了解在 Kotlin 中使用 JVM record。

视频:Support for JVM Records in Kotlin 1.5.0

密封接口

Kotlin 接口现在可以带 sealed 修饰符,它对接口的作用方式与对类相同:密封接口的所有实现都在编译期已知。

1
sealed interface Polygon

例如,你可以依靠这一事实编写穷尽的 when 表达式。

1
2
3
4
5
fun draw(polygon: Polygon) = when (polygon) {
   is Rectangle -> // ...
   is Triangle -> // ...
   // 不需要 else —— 所有可能的实现都已覆盖
}

此外,密封接口可以形成更灵活的受限类层次结构,因为一个类可以直接继承多个密封接口。

1
class FilledRectangle: Polygon, Fillable

进一步了解密封接口。

视频:Sealed Interfaces and Sealed Classes Improvements

包级密封类层次结构

密封类现在可以在同一编译单元和同一包的所有文件中拥有子类。以前,所有子类都必须出现在同一个文件中。

直接子类可以是顶层的,也可以嵌套在任意数量的具名类、具名接口或具名对象中。

密封类的子类必须有正确限定的名称——它们不能是局部对象或匿名对象。

进一步了解密封类层次结构。

内联类

内联类是基于值的类的一个子集,只保存值。你可以把它们用作某种类型值的包装器,而不会带来使用内存分配的额外开销。

内联类可以在类名之前用 value 修饰符声明:

1
value class Password(val s: String)

JVM 后端还需要特殊的 @JvmInline 注解:

1
2
@JvmInline
value class Password(val s: String)

inline 修饰符现在已被弃用并会产生警告。

进一步了解内联类。

视频:From Inline to Value Classes

Kotlin/JVM

Kotlin/JVM 获得了一些内部和面向用户的改进。以下是最值得注意的几项:

稳定的 JVM IR 后端

Kotlin/JVM 编译器的基于 IR 的后端现在已进入 Stable,并默认启用。

从 Kotlin 1.4.0 开始,基于 IR 的后端的早期版本就可以预览,现在它已成为语言版本 1.5 的默认后端。对于更早的语言版本,仍然默认使用旧后端。

你可以在这篇博客文章中找到关于 IR 后端优势及其未来发展的更多细节。

如果你需要在 Kotlin 1.5.0 中使用旧后端,可以在项目的配置文件中添加以下代码:

  • 在 Gradle 中:

Kotlin

1
2
3
 tasks.withType<org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompile> {
   kotlinOptions.useOldBackend = true
 }

Groovy

1
2
3
 tasks.withType(org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompile) {
  kotlinOptions.useOldBackend = true
 }
  • 在 Maven 中:
1
2
3
4
5
 <configuration>
     <args>
         <arg>-Xuse-old-backend</arg>
     </args>
 </configuration>

新的默认 JVM 目标:1.8

Kotlin/JVM 编译的默认目标版本现在是 1.8。1.6 目标已被弃用。

如果你需要为 JVM 1.6 构建,仍然可以切换到该目标。了解如何操作:

通过 invokedynamic 实现 SAM 适配器

Kotlin 1.5.0 现在使用动态调用(invokedynamic)来编译 SAM(单一抽象方法)转换:

新实现使用 LambdaMetafactory.metafactory(),并且编译期间不再生成辅助包装类。这会减小应用 JAR 的体积,从而改善 JVM 启动性能。

要回退到基于匿名类生成的旧实现方案,请添加编译器选项 -Xsam-conversions=class。

请在 Gradle、Maven 和命令行编译器中了解如何添加编译器选项。

通过 invokedynamic 实现 lambda

警告: 把普通 Kotlin lambda 编译为 invokedynamic 是实验性的。它随时可能被放弃或更改。需要选择启用(详见下文),并且只应用于评估目的。我们欢迎你在 YouTrack 中提供反馈。

Kotlin 1.5.0 引入了对把普通 Kotlin lambda(未被转换为函数式接口实例)编译为动态调用(invokedynamic)的实验性支持。该实现通过使用 LambdaMetafactory.metafactory()在运行时有效地生成必要的类,从而产生更轻量的二进制文件。与普通的 lambda 编译相比,它目前有三个限制:

  • 编译为 invokedynamic 的 lambda 不可序列化。
  • 对此类 lambda 调用 toString() 会得到可读性较差的字符串表示。
  • 实验性的 reflect API 不支持用 LambdaMetafactory 创建的 lambda。

要试用该特性,请添加 -Xlambdas=indy 编译器选项。如果你能通过这个 YouTrack 议题分享反馈,我们将非常感激。

请在 Gradle、Maven 和命令行编译器中了解如何添加编译器选项。

弃用 @JvmDefault 和旧的 Xjvm-default 模式

在 Kotlin 1.4.0 之前,有 @JvmDefault 注解以及 -Xjvm-default=enable 和 -Xjvm-default=compatibility 模式。它们用于为 Kotlin 接口中特定的非抽象成员创建 JVM 默认方法。

在 Kotlin 1.4.0 中,我们引入了新的 Xjvm-default 模式,可以在整个项目范围内开启默认方法生成。

在 Kotlin 1.5.0 中,我们弃用 @JvmDefault 以及旧的 Xjvm-default 模式:-Xjvm-default=enable 和 -Xjvm-default=compatibility。

进一步了解 Java 互操作中的默认方法。

可空性注解处理的改进

Kotlin 支持通过可空性注解处理来自 Java 的类型可空性信息。Kotlin 1.5.0 为该特性引入了多项改进:

  • 它会读取作为依赖使用的已编译 Java 库中类型实参上的可空性注解。
  • 它支持具有 TYPE_USE 目标的可空性注解,适用于:
  • 数组
  • 可变参数
  • 字段
  • 类型参数及其上界
  • 基类和接口的类型实参
  • 如果某个可空性注解有多个适用于某个类型的目标,并且其中一个是 TYPE_USE,则优先使用 TYPE_USE。例如,如果 @Nullable 同时支持 TYPE_USE 和 METHOD 目标,那么方法签名 @Nullable String[] f() 会变成 fun f(): Array<String?>!。

对于这些新支持的情形,在从 Kotlin 调用 Java 时使用错误的类型可空性会产生警告。使用 -Xtype-enhancement-improvements-strict-mode 编译器选项可以对这些情形启用严格模式(报告错误)。

进一步了解空安全与平台类型。

Kotlin/Native

Kotlin/Native 现在性能更好、也更稳定。值得注意的变更包括:

性能改进

在 1.5.0 中,Kotlin/Native 获得了一组性能改进,既加快了编译速度,也加快了执行速度。

编译器缓存现在在调试模式下支持 linuxX64(仅限 Linux 主机)和 iosArm64 目标。启用编译器缓存后,除第一次以外的绝大多数调试编译都完成得更快。测量显示,在我们的测试项目上速度提升约 200%。

要为新的目标使用编译器缓存,请在项目的 gradle.properties 中添加以下代码以选择启用:

  • 对于 linuxX64:kotlin.native.cacheKind.linuxX64=static
  • 对于 iosArm64:kotlin.native.cacheKind.iosArm64=static

如果启用编译器缓存后遇到任何问题,请在我们的问题跟踪器 YouTrack 中报告。

其他改进加快了 Kotlin/Native 代码的执行速度:

  • 平凡的属性访问器会被内联。
  • 字符串字面量上的 trimIndent() 会在编译期间求值。

停用内存泄漏检查器

Kotlin/Native 内置的内存泄漏检查器已默认禁用。

它最初是为内部使用而设计的,只能在有限的情况下(而非所有情况)发现泄漏。此外,后来发现它还存在可能导致应用崩溃的问题。因此我们决定关闭内存泄漏检查器。

在某些情况下(例如单元测试),内存泄漏检查器仍然有用。对于这些情况,你可以通过添加以下代码行来启用它:

1
Platform.isMemoryLeakCheckerActive = true

请注意,不建议在应用运行时启用该检查器。

Kotlin/JS

Kotlin/JS 在 1.5.0 中迎来演进性变更。我们继续推动 JS IR 编译器后端走向稳定,并发布其他更新:

升级到 webpack 5

Kotlin/JS Gradle 插件现在对浏览器目标使用 webpack 5 而不是 webpack 4。这是一次重大的 webpack 升级,带来了不兼容的变更。如果你使用自定义的 webpack 配置,请务必查看 webpack 5 发行说明。

进一步了解使用 webpack 打包 Kotlin/JS 项目。

IR 编译器的框架与库

警告: Kotlin/JS IR 编译器处于 Alpha 阶段。它可能会以不兼容的方式发生变化,并在将来需要手动迁移。我们欢迎你在 YouTrack 中提供反馈。

在开发 Kotlin/JS 编译器基于 IR 的后端的同时,我们鼓励并帮助库作者以 both 模式构建项目。这意味着他们可以为两种 Kotlin/JS 编译器生成制品,从而壮大新编译器的生态。

许多知名框架和库已经支持 IR 后端:KVision、fritz2、doodle 等。如果你在项目中使用它们,现在就已经可以用 IR 后端构建,并体验它带来的好处。

如果你在编写自己的库,请以 “both” 模式编译,这样你的使用者也能在新编译器中使用它。

Kotlin Multiplatform

在 Kotlin 1.5.0 中,为每个平台选择测试依赖已被简化,现在由 Gradle 插件自动完成。

多平台项目中现在可以使用获取字符类别的新 API。

标准库

标准库经历了大量变更和改进,从稳定实验性部分到添加新特性:

你可以在这篇博客文章中进一步了解标准库的变更。

视频:New Standard Library Features

稳定的无符号整数类型

UInt、ULong、UByte、UShort 无符号整数类型现在已进入 Stable。对这些类型的运算、区间和数列也是如此。无符号数组及对它们的运算仍处于 Beta。

进一步了解无符号整数类型。

稳定的与区域无关的文本大小写 API

此版本带来了新的与区域无关的文本大小写转换 API。它为对区域敏感的 toLowerCase()、toUpperCase()、capitalize() 和 decapitalize() API 函数提供了替代方案。新 API 帮助你避免因不同区域设置而导致的错误。

Kotlin 1.5.0 提供以下完全 Stable 的替代方案:

  • 对于 String 函数:

|早期版本|1.5.0 替代方案|

|String.toUpperCase()|String.uppercase()| |String.toLowerCase()|String.lowercase()| |String.capitalize()|String.replaceFirstChar { it.uppercase() }| |String.decapitalize()|String.replaceFirstChar { it.lowercase() }|

  • 对于 Char 函数:

|早期版本|1.5.0 替代方案|

|Char.toUpperCase()|Char.uppercaseChar(): Char
Char.uppercase(): String| |Char.toLowerCase()|Char.lowercaseChar(): Char
Char.lowercase(): String| |Char.toTitleCase()|Char.titlecaseChar(): Char
Char.titlecase(): String|

注意: 对于 Kotlin/JVM,还有带显式 Locale 参数的 uppercase()、lowercase() 和 titlecase() 重载。

旧的 API 函数已被标记为弃用,并将在未来的版本中移除。

请在 KEEP 中查看文本处理函数变更的完整列表。

稳定的字符到整数转换 API

从 Kotlin 1.5.0 开始,新的字符到编码、字符到数字的转换函数已进入 Stable。这些函数取代了当前常常与类似的字符串到 Int 转换相混淆的 API 函数。

新 API 消除了这种命名混淆,使代码行为更透明、更明确。

此版本引入了 Char 转换,它们被划分为以下几组命名清晰的函数:

  • 用于获取 Char 的整数编码以及从给定编码构造 Char 的函数:
1
2
3
 fun Char(code: Int): Char
 fun Char(code: UShort): Char
 val Char.code: Int
  • 用于把 Char 转换为它所表示数字的数值的函数:
1
2
 fun Char.digitToInt(radix: Int): Int
 fun Char.digitToIntOrNull(radix: Int): Int?
  • 一个 Int 扩展函数,用于把它所表示的非负个位数转换为对应的 Char 表示:
1
 fun Int.digitToChar(radix: Int): Char

旧的转换 API 现在已被弃用,包括带各种实现的 Number.toChar()(除 Int.toChar() 之外的所有实现)以及用于转换为数值类型的 Char 扩展,例如 Char.toInt()。

在 KEEP 中进一步了解字符到整数的转换 API。

稳定的 Path API

带有 java.nio.file.Path 扩展的实验性 Path API现在已进入 Stable。

1
2
3
4
5
6
// 使用 div (/) 运算符构造路径
val baseDir = Path("/base")
val subDir = baseDir / "subdirectory"

// 列出目录中的文件
val kotlinFiles: List<Path> = Path("/home/user").listDirectoryEntries("*.kt")

进一步了解 Path API。

向下取整除法与 mod 运算符

标准库中新增了用于模运算的运算:

  • floorDiv() 返回向下取整除法的结果。它可用于整数类型。
  • mod() 返回向下取整除法的余数(模)。它可用于所有数值类型。

这些运算看起来与现有的整数除法和 rem() 函数(或 % 运算符)非常相似,但它们对负数的行为不同:

  • a.floorDiv(b) 与普通 / 的区别在于 floorDiv 把结果向下取整(朝向更小的整数),而 / 把结果截断为更接近 0 的整数。
  • a.mod(b) 是 a 与 a.floorDiv(b) * b 之间的差。它要么为零,要么与 b 同号,而 a % b 可能与其异号。
1
2
3
4
5
6
7
fun main() {
    println("Floored division -5/3: ${(-5).floorDiv(3)}")
    println( "Modulus: ${(-5).mod(3)}")

    println("Truncated division -5/3: ${-5 / 3}")
    println( "Remainder: ${-5 % 3}")
}

Duration API 变更

警告: Duration API 是实验性的。它随时可能被放弃或更改。请仅将其用于评估目的。我们欢迎你在 YouTrack 中提供反馈。

有一个实验性的 Duration 类,用于以不同时间单位表示时长。在 1.5.0 中,Duration API 有以下变更:

  • 内部值表示现在使用 Long 而不是 Double,以提供更好的精度。
  • 新增了把 Long 转换为特定时间单位的 API。它取代了旧的、以 Double 值运算的 API,后者现在已被弃用。例如,Duration.inWholeMinutes 返回以 Long 表示的时长值,并取代 Duration.inMinutes。
  • 新增了从数字构造 Duration 的伴生函数。例如,Duration.seconds(Int) 创建一个表示整数秒数的 Duration 对象。像 Int.seconds 这样的旧扩展属性现在已被弃用。
1
2
3
4
5
6
7
8
import kotlin.time.Duration
import kotlin.time.ExperimentalTime

@ExperimentalTime
fun main() {
    val duration = Duration.milliseconds(120000)
    println("There are ${duration.inWholeSeconds} seconds in ${duration.inWholeMinutes} minutes")
}

获取字符类别的新 API 现已在多平台代码中可用

Kotlin 1.5.0 引入了在多平台项目中按照 Unicode 获取字符类别的新 API。若干函数现在在所有平台和通用代码中都可用。

用于检查字符是字母还是数字的函数:

1
2
3
4
5
6
fun main() {
    val chars = listOf('a', '1', '+')
    val (letterOrDigitList, notLetterOrDigitList) = chars.partition { it.isLetterOrDigit() }
    println(letterOrDigitList) // [a, 1]
    println(notLetterOrDigitList) // [+]
}

用于检查字符大小写的函数:

1
2
3
4
5
6
fun main() {
    val chars = listOf('Dž', 'Lj', 'Nj', 'Dz', '1', 'A', 'a', '+')
    val (titleCases, notTitleCases) = chars.partition { it.isTitleCase() }
    println(titleCases) // [Dž, Lj, Nj, Dz]
    println(notTitleCases) // [1, A, a, +]
}

其他一些函数:

属性 Char.category 及其返回类型枚举类 CharCategory(用于表示字符按 Unicode 的通用类别)现在也可在多平台项目中使用。

进一步了解字符。

新的集合函数 firstNotNullOf()

新的 firstNotNullOf() 和 firstNotNullOfOrNull() 函数把 mapNotNull() 与 first() 或 firstOrNull() 结合起来。它们用自定义的选择器函数映射原集合并返回第一个非 null 值。如果不存在这样的值,firstNotNullOf() 抛出异常,firstNotNullOfOrNull() 返回 null。

1
2
3
4
5
fun main() {
    val data = listOf("Kotlin", "1.5")
    println(data.firstNotNullOf(String::toDoubleOrNull))
    println(data.firstNotNullOfOrNull(String::toIntOrNull))
}

String?.toBoolean() 的严格版本

两个新函数引入了现有的 String?.toBoolean() 的大小写敏感严格版本:

1
2
3
4
5
fun main() {
    println("true".toBooleanStrict())
    println("1".toBooleanStrictOrNull())
    // println("1".toBooleanStrict()) // 异常
}

kotlin-test 库

kotlin-test 库引入了一些新特性:

多平台项目中简化的测试依赖用法

现在你可以使用 kotlin-test 依赖为 commonTest 源集添加测试依赖,Gradle 插件会为每个测试源集推断相应的平台依赖:

  • 对于 JVM 源集使用 kotlin-test-junit,请参见自动为 Kotlin/JVM 源集选择测试框架
  • 对于 Kotlin/JS 源集使用 kotlin-test-js
  • 对于通用源集使用 kotlin-test-common 和 kotlin-test-annotations-common
  • 对于 Kotlin/Native 源集不需要额外的制品

此外,你可以在任何共享源集或平台特定源集中使用 kotlin-test 依赖。

现有的带显式依赖的 kotlin-test 配置在 Gradle 和 Maven 中都能继续工作。

进一步了解设置对测试库的依赖。

自动为 Kotlin/JVM 源集选择测试框架

Gradle 插件现在会自动选择并添加对测试框架的依赖。你只需在通用源集中添加 kotlin-test 依赖即可。

Gradle 默认使用 JUnit 4。因此 kotlin("test") 依赖会解析为 JUnit 4 对应的变体,即 kotlin-test-junit:

Kotlin

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
kotlin {
    sourceSets {
        val commonTest by getting {
            dependencies {
                implementation(kotlin("test")) // 这会传递引入对 JUnit 4
                                               // 的依赖
            }
        }
    }
}

Groovy

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
kotlin {
    sourceSets {
        commonTest {
            dependencies {
                implementation kotlin("test") // 这会传递引入对 JUnit 4
                                              // 的依赖
            }
        }
    }
}

你可以在 test 任务中调用 useJUnitPlatform() 或 useTestNG() 来选择 JUnit 5 或 TestNG:

1
2
3
4
5
6
7
8
9
tasks {
    test {
        // 启用 TestNG 支持
        useTestNG()
        // 或者
        // 启用 JUnit Platform(即 JUnit 5)支持
        useJUnitPlatform()
    }
}

你可以在项目的 gradle.properties 中添加 kotlin.test.infer.jvm.variant=false 这一行来禁用自动选择测试框架。

进一步了解设置对测试库的依赖。

断言函数更新

此版本带来了新的断言函数,并改进了现有函数。

kotlin-test 库现在具有以下特性:

  • 检查值的类型

你可以使用新的 assertIs<T> 和 assertIsNot<T> 检查值的类型:

1
2
3
4
5
6
7
  @Test
  fun testFunction() {
      val s: Any = "test"
      assertIs<String>(s) // 断言失败时抛出 AssertionError,其中会提及 s 的实际类型
      // 由于 assertIs 中的契约,现在可以打印 s.length
      println("${s.length}")
  }

由于类型擦除,在下面的示例中该断言函数只检查 value 是否为 List 类型,并不检查它是否是元素类型为特定 String 的列表:assertIs<List<String>>(value)。

  • 比较数组、序列和任意可迭代对象的内容

新增了一组重载的 assertContentEquals() 函数,用于比较不实现结构相等性的不同集合的内容:

1
2
3
4
5
6
  @Test
  fun test() {
      val expectedArray = arrayOf(1, 2, 3)
      val actualArray = Array(3) { it + 1 }
      assertContentEquals(expectedArray, actualArray)
  }
  • 为 Double 和 Float 数字新增的 assertEquals() 和 assertNotEquals() 重载

assertEquals() 函数新增了重载,使得可以按绝对精度比较两个 Double 或 Float 数字。精度值作为函数的第三个参数指定:

1
2
3
4
5
6
7
8
9
   @Test
  fun test() {
      val x = sin(PI)

      // 精度参数
      val tolerance = 0.000001

      assertEquals(0.0, x, tolerance)
  }
  • 用于检查集合和元素内容的新函数

你现在可以使用 assertContains() 函数检查集合或元素是否包含某个内容。它可以用于具有 contains() 运算符的 Kotlin 集合和元素,例如 IntRange、String 等:

1
2
3
4
5
6
7
  @Test
  fun test() {
      val sampleList = listOf<String>("sample", "sample2")
      val sampleString = "sample"
      assertContains(sampleList, sampleString) // 集合中的元素
      assertContains(sampleString, "amp") // 字符串中的子串
  }
  • assertTrue()、assertFalse()、expect() 函数现在是内联的

从现在起,你可以把它们用作内联函数,因此可以在 lambda 表达式内调用挂起函数:

1
2
3
4
5
6
7
  @Test
  fun test() = runBlocking<Unit> {
      val deferred = async { "Kotlin is nice" }
      assertTrue("Kotlin substring should be present") {
          deferred.await() .contains("Kotlin")
      }
  }

kotlinx 库

随 Kotlin 1.5.0 一起,我们发布了 kotlinx 库的新版本:

协程 1.5.0-RC

kotlinx.coroutines 1.5.0-RC 发布,带来:

从 Kotlin 1.5.0 开始,实验性协程已被禁用,-Xcoroutines=experimental 标志不再受支持。

请在变更日志和 kotlinx.coroutines 1.5.0 发布博客文章中了解更多。

视频:kotlinx.coroutines 1.5.0

序列化 1.2.1

kotlinx.serialization 1.2.1 发布,带来:

  • JSON 序列化性能的改进
  • JSON 序列化中支持多个名称
  • 从 @Serializable 类生成实验性的 .proto schema
  • 以及更多内容

请在变更日志和 kotlinx.serialization 1.2.1 发布博客文章中了解更多。

视频:kotlinx.serialization 1.2.1

dateTime 0.2.0

kotlinx-datetime 0.2.0 发布,带来:

  • @Serializable 的 Datetime 对象
  • 规范化的 DateTimePeriod 和 DatePeriod API
  • 以及更多内容

请在变更日志和 kotlinx-datetime 0.2.0 发布博客文章中了解更多。

迁移到 Kotlin 1.5.0

一旦 1.5.0 可用,IntelliJ IDEA 和 Android Studio 就会建议把 Kotlin 插件更新到该版本。

要把现有项目迁移到 Kotlin 1.5.0,只需把 Kotlin 版本改为 1.5.0 并重新导入你的 Gradle 或 Maven 项目。了解如何更新到 Kotlin 1.5.0。

要使用 Kotlin 1.5.0 开始一个新项目,请更新 Kotlin 插件,然后从 File | New | Project 运行项目向导。

新的命令行编译器可在 GitHub 发布页面下载。

Kotlin 1.5.0 是一个特性版本,因此可能带来不兼容的语言变更。你可以在 Kotlin 1.5 兼容性指南中找到这些变更的详细列表。