对比Java学Kotlin内联类
Posted 陈蒙_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了对比Java学Kotlin内联类相关的知识,希望对你有一定的参考价值。
什么是内联类?
Kotlin 在1.2.30 版本开始试验版本的内联类,写法是:
inline class Duration(val value: Long)
从1.5版本开始,上面这种写法被废弃了,转而启用稳定版的写法:
@JvmInline
value class Duration(val value: Long)
即,由关键字 inline 转为注解 @JvmInline + 关键字 value,但是二者在构造函数里面都是有且仅有一个成员变量。
之所以要这么改变,是因为:
- inline class 有歧义,容易让人联想其跟内联函数的关系:好比抽象类里面有抽象方法,那内联类里面的都是内联方法?
- Kotlin 在下“一盘大棋”(value classes, which are classes without identity and hold values only),内联类正在转变成棋局的一部分;
为什么要有内联类?
内联类是用类的外衣包裹基本类型数据。
看起来像是一个类,但是实际上是基本类型。这样既具有代码的可读性(对开发者),又拥有基本类型的高效性(对 JVM),因为 JVM 底层会对基本类型做很多优化,而真正的类却没有这种待遇从而带来额外开销。
以两个时刻间的间距 Duration 为例,我们有几种实现方式:基本类型、常规类、内联类。我们分别来看下。
【基本类型】
greetAfterTimeout(millis: Long)
,但是我们在实际使用容易产生歧义,即 greetAfterTimeout(2)
里面的2单位是秒还是毫秒?虽然我们可以通过后缀的方法来解决这个问题greetAfterTimeoutMs(millis: Long)
、greetAfterTimeoutSec(secs: Long)
,但同时也产生了函数的冗余。
【常规类】
比如我们新建一个名为 Duration 的类:
class Duraton(val millis: Long)
fun seconds(value: Int) = Duration(value * 1000)
然后这样使用:
greetAfterTimeout(duration: Duration)
greetAfterTimeout(seconds(2))
虽然可读性不错,但是跟基本类型一样,同样有了额外的存储开销。
【内联类】
@JvmInline
value class Duration(val value: Long)
fun seconds(value: Int) = Duration(value * 1000)
greetAfterTimeout(value: Duration)
// 上述代码在 JVM 看来等价于:
greetAfterTimeout__(value: Long)
// 即
greetAfterTimeout(seconds(2))
// 等价于:
greetAfterTimeout(2000L)
这种方式既具有良好的可读性又没有产品额外的存储开销。
kotlin.time.Duration 是 Kotlin 内置的内联类,大家感兴趣的话可以自行查看其源码。除了 Duration 之外,还有一组内联类形式的无符号数,包括 UInt
、UShort
、ULong
等。
当前 value class 只允许主构造函数中含有一个 val 型的成员变量,虽然其长远目标是不限制变量个数:
@JvmInline
value class Person(val name: String) // ok
@JvmInline
value class Person(var name: String) // 编译报错
除了构造函数中的成员变量,我们也可以在内联类中声明其他的“成员变量”,但是注意不能有 Backing fields,同时我们也可以给内联类添加 @Serializable 注解:
@Serializable
@JvmInline
value class Person(val name: String) {
val length: Int
get() = name.length
}
在Java中如何使用?
我们以 greetAfterTimeout(value: Duration)
为例,JVM 在实际运行时会把该方法名进行重命名成 greetAfterTimeout-R44lxco(value: Long)
,之所以这么做,是为了避免重名,比如同时存在两个内联类:
@JvmInline
value class Person(val name: String)
@JvmInline
value class Name(val s: String)
fun record(p: Person)
fun record(n: Name)
// 实际编译后的代码是:
fun record-7qJ_djg(p: String)
fun record-GWat88I(n: String)
从而避免重名的问题。
同时我们在 Java 代码中引用上述方法时,如果入参是内联类,也需要使用带后缀的方法:
// 引用入参是基本类型的 fun greetAfterTimeout(value: Long) 的方法:
ExampleKt.greetAfterTimeout(2000L)
// 引用入参是内联类的 fun greetAfterTimeout(value: Duration) 的方法:
ExampleKt.greetAfterTimeout-R44lxco(2000L)
如果不想使用带后缀的方法,我们可以在 Kotlin 代码中加上 @JvmName 注解:
@JvmName
fun greetAfterTimeout(value: Duration)
这样我们就可以在 Java 代码中引用不带后缀的方法名了:
ExampleKt.greetAfterTimeout(2000L)
跟数据类有啥区别?
内联类可以实现接口,但是不能和其他类存在继承关系,既不能当基类也不能当子类。
因为内联类实际上并不是一个类,所以其并没有示例的引用存在,也就无法通过 === 来比较。我们可以通过一个对比来更加直观的记忆。
data class DataPoint(val x: Int, val y: Int)
@JvmInline
value class ValuePoint(val x: Int, val y: Int)
当我们把 DataPoint 和 ValuePoint 分别作为参数传递时,它们在栈上的存储格式一个是引用、一个是值:
对于 DataPoint
和 ValuePoint
的数组,它们在栈上的存储格式是这样的:
其他事项
编译器对待内联类,并不是总是将其当做基本类型对待,也有可能持有一个 wrapper,这一点跟 Int、int、Integer 的封箱和开箱有点类似。
interface I
@JvmInline
value class Foo(val i: Int) : I
fun asInline(f: Foo) {}
fun <T> asGeneric(x: T) {}
fun asInterface(i: I) {}
fun asNullable(i: Foo?) {}
fun <T> id(x: T): T = x
fun main() {
val f = Foo(42)
asInline(f) // unboxed: used as Foo itself
asGeneric(f) // boxed: used as generic type T
asInterface(f) // boxed: used as type I
asNullable(f) // boxed: used as Foo?, which is different from Foo
// below, 'f' first is boxed (while being passed to 'id') and then unboxed (when returned from 'id')
// In the end, 'c' contains unboxed representation (just '42'), as 'f'
val c = id(f)
}
参考文档
以上是关于对比Java学Kotlin内联类的主要内容,如果未能解决你的问题,请参考以下文章