Aengus 的技术小筑

Aengus | Blog

Kotlin 1.4.30 的新特性预览

2172
2021-03-03

Kotlin 1.4.30是Kotlin 1.4的最后一个版本,其中包含了Kotlin 1.5中的即将发布的特性,包括inline value classes的稳定、JVMrecord类的实验性支持以及sealed interface的实验性支持,如果想要体验这些特性,需要特别声明版本:

compileKotlin {
    kotlinOptions {
        languageVersion = "1.5"
        apiVersion = "1.5"
    }
}

inline value classes的稳定

在Kotlin 1.3中inline class已经是Alpha状态,而在1.4.30版本中变为了Beta状态。在Kotlin 1.5中将确定inline classes的概念并为了更一般的特性,将其变为value class,我们将在下面提到。

inline class允许有一个并且只有一个val属性,编译器会自动将内联类替换为其属性,并且将使用内联类的函数的名称进行修改,如下:

inline class Color(val rgb: Int)

fun changeBackground(color: Color)
changeBackground(Color(255))

// 编译后
fun changeBackground-euwHqFQ(color: Int) 
changeBackground-euwHqFQ(255) 

修改函数名称的原因是防止由于JVM中类似方法的重载导致方法冲突。若在Java中使用Kotlin中定义的内联类,只能调用其空的构造函数,无法对内联类中包裹的属性进行赋值,但是可以定义接收内联类为参数的方法:

// Kotlin
inline class Color(val rgb: Int)

// Java
Color a = new Color();
a.getRgb() // OK
a.setRgb(1) // Error

类消除只有在将内联类传给普通方法的时候才会发生,当传给泛型方法或者将内联类存储在Collection中时并不会立刻进行类消除,这有些类似Java中的装箱,只有在进行使用时才会进行拆箱,这些都是自动的。

对于Java调用修改JVM name

从1.4.30开始,可以给调用内联类的方法修改其Java调用时的名字,默认由编译器进行修改以防止Java重载冲突,用法如下:

// Kotlin declarations
inline class Timeout(val millis: Long)

val Int.millis get() = Timeout(this.toLong())
val Int.seconds get() = Timeout(this * 1000L)

@JvmName("greetAfterTimeoutMillis")
fun greetAfterTimeout(timeout: Timeout)

// Kotlin usage
greetAfterTimeout(2.seconds)

// Java usage
greetAfterTimeoutMillis(2000);

@JvmName()不会对Kotlin生效,因为Kotlin传入的类型是内联类。

初始化代码块

从1.4.30开始可以对内联类添加init代码块了:

inline class Name(val s: String) {
    init {
        require(s.isNotEmpty())
    }
}

**注意:**内联类的init代码块只有调用构造方法的时候才会调用

Inline value classes

Kotlin 1.5为内联类带来更具体的概念并且引入了更多的特性,其语法也变为了value class

对于JVM来说,内联类是对只有一个参数的类的特别优化。value class代表了更一般的概念并且会带来更多的优化:当前的内联类、Valhalla项目原始类。

由于内联类是value class的一种优化,所以必须要用和以往不同的方式声明:

@JvmInline
value class Color(val rgb: Int)

原来的语法inline class还可以继续使用一段时间,但是在1.5中使用会得到一个警告并且将来会被标记为错误。

Value classes

Value class代表了不可变的数据实体,现在(Kotlin 1.5)为了支持inline classvalue class同样也只允许一个参数,但在之后的版本中将可以接收多个只读(val)参数:

value class Point(val x: Int, val y: Int)

Value class完全用来存储数据,没有”标识符“:===操作符不可以被调用,==操作符会比较其中所有的属性;在Valhalla项目引入到JVM后,没有”标识符“这一特性将允许value class通过JVM原始类型来实现。

上面的特性也是value class不同于data class的一些点。

对JVM record类的支持

Java 14中引入了record class,其目的和Kotlin中的data class类似,都是作为数据的简单存储。

Java record并不遵循JavaBean的规范,在JavaBean中的Getter方法为getX()getY(),而record class中则变为了x()y()。现在Kotlin 1.4.30中也支持了这种语法,在Kotlin中调用record class和JavaBean类似:

// Java
record Point(int x, int y) { }
// Kotlin
fun foo(point: Point) {
    point.x // 属性调用
    point.x() // 也可以
}

同样也可以通过@JvmRecord注解将Kotlin中的data class转为record class来给Java调用,这样生成的Getter方法就变成x()而不是getX()

@JvmRecord
data class Point(val x: Int, val y: Int)

需要注意的是@JvmRecord注解只有用JVM 15+的版本去编译Kotlin代码时才能够使用。

密封接口及密封类提升

当声明一个类为sealed时,将会限制其子类的继承结构,这将允许when表达式的分支检查。在1.4中,密封类有两个限制:顶层类不能是密封接口;继承密封类的所有的直接子类都必须在同一个文件中。

Kotlin 1.5移除了这两个限制:可以将接口声明为sealed,子类(包括密封类和密封接口)可以不在同一个文件中(但是需要和父类在相同的包下或者编译单元中)。

sealed interface Expr
data class Const(val number: Double) : Expr
data class Sum(val e1: Expr, val e2: Expr) : Expr
object NotANumber : Expr

fun eval(expr: Expr): Double = when(expr) {
    is Const -> expr.number
    is Sum -> eval(expr.e1) + eval(expr.e2)
    NotANumber -> Double.NaN
}

密封接口同样可以限制子类的继承结构,除此之外,另一个用法是禁止外部库实现或继承接口。

在未来使用JVM支持

sealed classes预览版支持已经被引入到Java 15中,在将来编译Kotlin sealed classes时会提供JVM的原生支持(可能是JVM 17或此特性稳定后)。在Java中,显式的列出密封类或接口的所有子类:

// Java
public sealed interface Expression
    permits Const, Sum, NotANumber { ... }

这些信息将使用新的PermittedSubclasses属性存储在类文件中,JVM会在运行时识别sealed classes并且阻止未授权的子类的扩展。

将来在使用最新的JVM编译Kotlin时,将启动新的sealed classes的JVM原生支持,编译器会在字节码中生成允许的子类列表来确保JVM支持以及格外的运行时检查:

// JVM 17+
Expr::class.java.permittedSubclasses // [Const, Sum, NotANunmber]

在Kotlin中并不用像Java一样声明所有继承的子类,编译器将会根据同包下的所有子类自动生成。

理论上在旧的JVM版本中也可以定义Kotlin密封接口的Java子类,但是并没有相关的限制,因为旧的JVM并没有相关功能。

原文地址

New Language Features Preview in Kotlin 1.4.30