容易被忽视的几个 Kotlin 细节, value class 执行效率竟然这么高

4,007 阅读5分钟

hi 大家好,我是 DHL。公众号:ByteCode ,专注分享最新技术原创文章,涉及 Kotlin、Jetpack、算法动画、数据结构 、系统源码、 LeetCode / 剑指 Offer / 多线程 / 国内外大厂算法题 等等。

在之前的文章中,分析过 Kotlin 1.5 宣布了一个重磅特性 value class 这是一个非常实用的特性,提高代码的可读性同时,还可以提高性能,因为编译器会对它进行更深层次的优化。主要包含了以下内容,没有看过的小伙伴可以前去查看

  • inline classvalue class 有什么区别
  • value class 不能被继承,但是可以实现接口
  • 当传递的对象为空时,value class 将会失去内联效果
  • value class 禁止使用 === 可以使用 ==

而今天这篇文章主要介绍 value classdata class 的区别,这可能是平时在做业务开发的时候,容易被忽视的几个细节。通过这篇文章,你将学习到以下内容。

  • 什么是 value class ?
  • 什么是 data class ?
  • value classdata class 的区别
    • value class 占用更少的内存,执行效率更高
    • value class 执行效率比 data class 快多少
    • value class 没有 copy() 方法
    • value class 构造函数只能传入一个参数
    • value class 为什么不能重写 equals()hashcode() 方法
    • value classdata class 都不能被继承

什么是 value class

value class 表示内联类,需要在主构造函数中传入一个参数,而且需要用 val 进行修饰, 编译成 Java 代码之后,会替换为传进去的值,代码如下所示。

@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 代码并没有创建额外的对象,而是将在 Kotlin 中创建的对象 User 替换为传进去的值 DHL

什么是 data class

data class 表示数据类,编译器会根据主构造函数声明的参数,自动生成 quals()hashCode()toString()componentN()copy()setXXX()getXXX() 等等模板方法,将我们从重复的劳动力解放出来了,专注于核心业务的实现。

data class User(val name: String)

// 编译之后
public final class User {
   private final String name;
   public final String getName() { return this.name; }
   ......
   public final String component1() { return this.name; }
   public final User copy(@NotNull String name) { return new User(name); }
   ......
}

value class 和 data class 的区别

Value Class 占用更少的内存,执行效率更高

为了保证相同的逻辑应用在各个地方,通常我们对于 model 中参数的验证都会封装在当前 model 中,比如在一个 data class User 中检查用户名是否为空。

data class User(var name: String? = null) {
    init {
        requireNotNull(name) { "name is not null" }
    }
}

当我们每次创建 User 对象的时候,都会在堆中分配对象,需要占用更多的内存,同时也会使我们的代码执行效率更低。因为对象创建过程是非常的慢。会经历两个过程:类加载过程、对象创建过程。

  • 类加载过程

    • 会先判断这个类是否已经初始化,如果没有初始化,会执行类的加载过程
    • 类的加载过程:加载、验证、准备、解析、初始化等等阶段,之后会执行 <clinit>() 方法,初始化静态变量,执行静态代码块等等
  • 对象创建过程

    • 如果类已经初始化了,直接执行对象的创建过程
    • 对象的创建过程:在堆内存中开辟一块空间,给开辟空间分配一个地址,之后执行初始化,会执行 <init>() 方法,初始化普通变量,调用普通代码块

value calss 的出现很好的帮助我们解决了这些问题,它使代码执行效率更高,占用更少的内存。这全都得益于 Kotlin 编译器对它进行大量的优化。

@JvmInline
value class User(val name: String)
fun login(user: User?): String = user?.name ?: ""
println(login(User("DHL")))

// 编译后的代码
String var0 = login-js0Jwf8("DHL");
System.out.println(var0);

正如你所见当我们在实例化 User 的时候,并没有在堆中分配对象,而是将传递给方法 login() 的参数 User 替换为传进去的值 DHL

Value class 执行时间比 data class 快多少

接下来我们用一个例子来感受一下 value classdata class 快多少,代码如下所示。

data class User1(val name: String)
fun printDataClass(user: User1) {}

@JvmInline
value class User2(val name: String)
fun printValueClass(user: User2) {}

@OptIn(ExperimentalTime::class)
fun main() {
    // data class
    val measureDataClass = measureTime {
        repeat(100) {
            User1("DHL")
        }
    }
    println("measure data class time  ${measureDataClass.toDouble(TimeUnit.MILLISECONDS)} ms")

    // value class
    val measureRunValueClass = measureTime {
        repeat(100) {
            User2("DHL")
        }
    }
    println("measure value class time ${measureRunValueClass.toDouble(TimeUnit.MILLISECONDS)} ms")
}

上述代码唯一的区别 User1data class 来声明的,User2value class 来声明的,最后的执行时间如下所示。

measure data class time  6.790241 ms
measure value class time 0.832866 ms

value class 执行效率远远高于 data class。当数据量很大时,它们的差距也会越来越大。

Value class 没有 copy () 方法

value calssdata class 一样可以有 init function,也可以有 internal function,方便我们封装业务逻辑。

但是 value calss 不会生成 copy() 方法,而 data class 编译后会生成 copy() 方法,如下所示。

data class User(val name: String, val pwd: String)

// 编译之后
public final class User {
    ......
   @NotNull
   public final User copy(@NotNull String name, @NotNull String pwd) {
      return new User(name, pwd);
   }
    ......
}

这也意味着通过 data calss 创建对象实例副本,我们不需要重写所有的参数,可以指定需要改变的参数。

user = user.copy(name = "hi-dhl")

value calss 只能通过构造函数去创建对象,需要显示指定所有的参数。

Value class 构造函数只能传入一个参数

现阶段 value class 只能在构造函数中传入一个参数,而且需要用 val 进行修饰,而 data calss 支持在构造函数中添加多个参数,参数可以用 val 或者 var 声明。不过在不久的将来 Kotlin 将会支持在 value class 构造函数中添加多个参数,如下图所示。

Value class 和 data class 都不能被继承

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

Value class 不能重写 equals () 、hashcode () 方法

value class 相比于 data class 不能重写 equals()hashcode() 方法, 如下图所示。

equals() 方法用于比较两个参数的内容是否相同,关于 Kotlin 中的 ===== 以及 eauals 方法的区别,可以查看我另外一篇文章 解密 Koltin 中的 == 和 === 以及 eauals

因为 value class 构造函数只能传入一个参数,而且必须用 val 进行修饰,所以不存在需要比较两个相同的参数场景,因此 Kotlin 不让重写 equals()hashcode() 方法。


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

代码不止,文章不停

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



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

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

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

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

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

近期必读热门文章