Kotlin 宣布一个重磅特性

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);
}

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


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

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

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

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

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

image

近期必读热门文章

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,036评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,046评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,411评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,622评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,661评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,521评论 1 304
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,288评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,200评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,644评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,837评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,953评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,673评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,281评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,889评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,011评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,119评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,901评论 2 355

推荐阅读更多精彩内容