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

5,317 阅读5分钟

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

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

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

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

  • 什么是 value class ?
  • 什么是 data class ?
  • value class 和 data class 的区别
    • value class 占用更少的内存,执行效率更高
    • value class 执行效率比 data class 快多少
    • value class 没有 copy() 方法
    • value class 构造函数只能传入一个参数
    • value class 为什么不能重写 equals() 、 hashcode() 方法
    • value class 和 data 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 class 比 data 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")
}

上述代码唯一的区别 User1 用 data class 来声明的,User2 用 value class 来声明的,最后的执行时间如下所示。

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

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

Value class 没有 copy () 方法

value calss 同 data 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 class 和 data 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,包含多种解法、解题思路、时间复杂度、空间复杂度分析

近期必读热门文章