Kotlin 宣布一个重磅特性 value class

Kotlin 宣布一个重磅特性 value class

  • 如果评论区没有及时回复,欢迎来公众号:ByteCode 咨询
  • 公众号:ByteCode。致力于分享最新技术原创文章,涉及 Kotlin、Jetpack、算法、译文、系统源码相关的文章

Kotlin 1.5 宣布了一个重磅特性 value class,这是一个非常实用的特性,提高代码的可读性同时,还可以提高性能,因为编译器会对它进行更深层次的优化,减少对象的创建。

随着 Kotlin 不断的完善,出现了一系列的特性 inner classdata classsealed classsealed interfaceinline classvalue class 等等,之前写过几篇文章专门分析 sealed classsealed interface,可以点击下方链接前去查看。

而今天这篇文章主要分析 inline classvalue class

通过这篇文章你将学习到以下内容:

  • 什么是内联函数?
    • 什么情况下使用内联函数?
  • 什么是 inline class
  • 什么是 value class
    • value class 不能被继承
    • value class 可以实现接口
    • 当传递的对象为空时,会失去内联效果
    • value class 禁止使用 === 可以使用 ==
  • inline classvalue class 有什么区别?
  • Java 如何调用接受内联类的函数?

内联函数

在 Kotlin 中通过 inline-functions (内联函数) 实现函数内联,内联的作用:提升运行效率,调用被 inline 修饰符的函数,会把方法体内的代码放到调用的地方,其主要目的提高性能,减少对象的创建。内联函数代码如下所示。

fun testInline() {
printByteCode()
}

inline fun printByteCode() {
println("printByteCode")
}

// 编译之后
public static final void testInline() {
String var1 = "printByteCode";
System.out.println(var1);
}

inline 修饰的函数适用于以下情况

  • inline 修饰符适用于把函数作为另一个函数的参数,例如高阶函数​ filtermapjoinToString 或者一些独立的函数 repeat
  • inline 操作符适合和 reified 操作符结合在一起使用
  • 如果函数体很短,使用 inline 操作符提高效率

如果使用 inline 修饰符标记普通函数,Android Studio 会给一个大大大的警告,如下图所示,这是为了防止 inline 操作符滥用而带来的性能损失。

但是如果函数体很短,想通过 inline 操作符提高效率,又想消除掉警告,可以前往查看 为数不多的人知道的 Kotlin 技巧及解析(三) 文章中的 「Kotlin 注解在项目中的使用」。

代码示例仓库地址:https://github.com/hi-dhl/KtKit

内联类

内联类是一个被忽略,非常有用的特性。有时必要的业务逻辑,需要将基本数据类型、String 等等参数封装在一个 Model 中,然后在 Model 中封装一些方法,对这个参数做检查、验证等等操作。

参数被封装之后,需要创建包装对象,对象的创建在堆中进行分配,数据量很大的情况,对性能的损耗也非常大,例如:内存的占用,运行时的效率,频繁创建对象,导致 GC 回收大量对象带来的卡顿问题等等。

基本数据类型、String 等等运行时 JVM 会对它进行优化,但是如果将这些参数封装在一个类中,包装类不会做任何处理,依然会在堆中创建对象。

所以为了减少性能的损耗,避免对象的创建,因此 Kotlin 推出了一个内联类 inline-classes。内联类只能在构造函数中传入一个参数,参数需要用 val 声明,编译之后,会替换为传进去的值,代码如下所示。

inline class User(val name: String)

fun testInline() {
println(User("DHL"))
}

// 编译之后
public static final void testInline() {
System.out.println("DHL");
}

正如你所见,编译后的 Java 代码并没有创建额外的对象,在 Kotlin 中创建的对象被替换为传进去的值。

Java 如何调用接受内联类的函数

在 Kotlin 中声明一个函数,并且将 inline class 作为参数传递,编译成 Java 代码之后,函数名称会被打乱,代码如下所示。

inline class User(val name: String) 

fun login(user: User) {
}

// 编译后的代码
public static final void login_FY_U7ig/* $FF was: login-FY_U7ig*/(@NotNull String user) {
}

编译后的函数名称会被打乱,例如 login-FY_U7ig。 但是这样存在一个问题, Java 无法调用接受内联类的函数,代码如下所示。

inline class User(val name: String) 

fun login(user: User) {
}

fun login(passwd: String) {
}

// Kotlin 编译正常
login(User("DHL"))
login("ByteCode")

// Java 编译失败
MainKt.login("DHL");

在 Kotlin 中调用,可以正常编译,因为内联的原因,导致 Java 调用就会失败,如下图所示。

为了能够在 Java 中正常调用,因此添加了注解 @JvmName 更改函数名称,来解决这个问题,代码如下所示。

inline class User(val name: String) 

@JvmName("loginWithName")
fun login(user: User) {
}

fun login(passwd: String) {
}

// Kotlin 编译正常
login(User("DHL"))
login("ByteCode")

// Java 编译正常
MainKt.loginWithName("DHL");

所以无论是 Inline classes 还是 Value classes 如果没有添加 @JvmName 注解,都会存在这个问题。将生成的函数名称打乱,是为了防止方法重载冲突,或者 Java 意外调用。

Inline classes 是在 Kotlin 1.3 引入的 ,在 Kotlin 1.5 时进入了稳定版本,废弃了 inline 修饰符,引入了 Value classes

什么是 Value classes

Inline classesValue classes 的子集, Value classesInline classes 会得到更多优化,现阶段 Value classesInline classes 一样,只能在构造函数中传入一个参数,参数需要用 val 声明,将来可以在构造函数中添加多个参数,但是每个参数都需要用 val 声明,官方说明如下图所示。

将来如果支持添加多个参数,那么它的使用范围会越来越广的。

升级到 Kotlin 1.5 之后,Inline classes 将被弃用,如下图所示,编译器将会给出警告。

根据提示目前唯一需要改变的是语法 inline 替换为 value, 然后在添加 @JvmInline 注解即可。

@JvmInline
value class User(val name: String)

编译后的效果和 Inline classes 是一样的,因此后面的案例将会使用 Value classes

Value classes 不能被继承

因为 value class 编译后将会添加 fianl 修饰符,因此不能被继承,同样也不能继承其他的类,如下图所示。

Value classes 可以实现接口

虽然 value class 不能继承其他的类,但是可以实现接口,代码如下所示。

interface LoginInterface

@JvmInline
value class User(val name: String) : LoginInterface

fun testInline() {
println(User("DHL"))
}


// 编译后的代码
public static final void testInline() {
User var0 = User.box-impl(User.constructor-impl("DHL"));
System.out.println(var0);
}

public static final User box_impl/* $FF was: box-impl*/(String v) {
return new User(v);
}

正如代码所示,当 value class 实现接口时,失去了内联效果,依然会在堆中创建 User 对象,除了实现接口的时候,没有内联效果,当对象为空的时候,也会失去了内联效果。

当传递的对象为空的时候,会失去内联效果

当构造函数的参数为基本数据类型,且传递的参数 value class 的对象为空时,将失去内联效果,代码如下所示。

@JvmInline
value class User(val age: Int)

fun login(user: User?): Int = user?.age ?: 0

fun testInline() {
println(login(User(10)))
}

// 编译后的代码
public static final int login_js0Jwf8/* $FF was: login-js0Jwf8*/(@Nullable User user) {
return user != null ? user.unbox-impl() : 0;
}

public static final void testInline() {
int var0 = login-js0Jwf8(User.box-impl(10));
System.out.println(var0);
}

public static final User box_impl/* $FF was: box-impl*/(int v) {
return new User(v);
}

正如你所见,依然会在堆中创建 User 对象,失去了内联效果,这是因为 Int 不能直接转换为 null

当构造函数的参数为 String,且传递的参数 value class 的对象为空时,内联效果不会受到影响,代码如下所示。

@JvmInline
value class User(val name: String)

fun login(user: User?): String = user?.name ?: ""

fun testInline() {
println(login(User("DHL")))
}

// 编译后的代码
public static final String login_js0Jwf8/* $FF was: login-js0Jwf8*/(@Nullable String user) {
// ......
return var10000;
}

public static final void testInline() {
String var0 = login-js0Jwf8("DHL");
System.out.println(var0);
}

编译后的 Java 代码并没有创建对象,传递给方法 login 的参数 User 被替换为传进去的值 String

Value class 禁止使用 === 可以使用 ==

Kotlin 中的操作符 ===== 的区别,以及它们分别在什么场景下使用,更多信息可以前往查看 揭秘 Kotlin 中的 == 和 ===,这里简单介绍一下。

Kotlin 提供了两种方式用于对象的比较。

  • 比较对象的结构是否相等( == 或者 equals

    Kotlin 中的操作符 == 等价于 equals 用于比较对象的结构是否相等, 很多情况下使用的是 ==,因为对于浮点类型 Float 和 Double,其实现方法 equals 不遵循 IEEE 754 浮点运算标准。

  • 比较对象的引用是否相等 ( === )

    Kotlin 中的操作符 === 用于比较对象的引用是否指向同一个对象,运行时如果是基本数据类型 === 等价于 ==

其中 value class 比较特殊,禁止使用操作符 ===,如下图所示,编译器将会给出警告。

因为操作符 === 用于比较对象的引用是否相等,因为内联的原因,最终会替换为传进去的值。

但是操作符 == 是不受影响的,操作符 == 用于比较对象的结构是否相等,代码如下所示。

fun testInline() {
val u1 = User("DHL")
val u2 = User("DHL")
println(u1 == u2)
}

// 编译后的代码
public static final void testInline() {
String u1 = "DHL";
String u2 = "DHL";
boolean var2 = User.equals-impl0(u1, u2);
System.out.println(var2);
}


如果有帮助 点个赞 就是对我最大的鼓励

代码不止,文章不停

欢迎关注公众号:ByteCode,持续分享最新的技术



最后推荐长期更新和维护的项目:

  • 个人博客,将所有文章进行分类,欢迎前去查看 https://hi-dhl.com

  • KtKit 小巧而实用,用 Kotlin 语言编写的工具库,欢迎前去查看 KtKit

  • 计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,欢迎前去查看 AndroidX-Jetpack-Practice

  • LeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解,语言 Java 和 kotlin,包含多种解法、解题思路、时间复杂度、空间复杂度分析

近期必读热门文章

致力于分享一系列 Android 系统源码、逆向分析、算法、翻译、Jetpack 源码相关的文章,在技术的道路上一起前进

Android10 源码分析

正在写一系列的 Android 10 源码分析的文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,如果你同我一样喜欢研究 Android 源码,可以关注我 GitHub 上的 Android10-Source-Analysis

算法题库的归纳和总结

由于 LeetCode 的题库庞大,每个分类都能筛选出数百道题,由于每个人的精力有限,不可能刷完所有题目,因此我按照经典类型题目去分类、和题目的难易程度去排序。

  • 数据结构: 数组、栈、队列、字符串、链表、树……
  • 算法: 查找算法、搜索算法、位运算、排序、数学、……

每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路,如果你同我一样喜欢算法、LeetCode,可以关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin

精选国外的技术文章

目前正在整理和翻译一系列精选国外的技术文章,不仅仅是翻译,很多优秀的英文技术文章提供了很好思路和方法,每篇文章都会有译者思考部分,对原文的更加深入的解读,可以关注我 GitHub 上的 Technical-Article-Translation

评论