1. 概述

Kotlin 中的字符串插值(String Interpolation)让我们可以优雅地将常量字符串和变量拼接成一个新的字符串。

在本文中,我们将通过查看生成的字节码,来深入了解插值机制背后的实现原理。同时,我们也会探讨当前实现可能的优化方向。

2. 字符串插值

我们从一个简单且熟悉的例子开始:

class Person(val firstName: String, val lastName: String, val age: Int) {
    override fun toString(): String {
        return "$firstName $lastName is $age years old"
    }
}

如上所示,我们在 toString() 方法中使用了字符串插值。为了查看 Kotlin 编译器是如何实现这个特性的,我们可以先使用 kotlinc 编译该类:

>> kotlinc interpolation.kt

然后使用 javap 工具查看生成的字节码:

>> javap -c -p com.baeldung.interpolation.Person
// truncated
public java.lang.String toString();
    Code:
       0: new           #9   // class StringBuilder
       3: dup
       4: invokespecial #13  // Method StringBuilder."<init>":()V
       7: aload_0
       8: getfield      #17  // Field firstName:LString;
      11: invokevirtual #21  // Method StringBuilder.append:(LString;)LStringBuilder;
      14: bipush        32
      16: invokevirtual #24  // Method StringBuilder.append:(C)LStringBuilder;
      19: aload_0
      20: getfield      #27  // Field lastName:LString;
      23: invokevirtual #21  // Method StringBuilder.append:(LString;)LStringBuilder;
      26: ldc           #29  // String  is
      28: invokevirtual #21  // Method StringBuilder.append:(LString;)LStringBuilder;
      31: aload_0
      32: getfield      #33  // Field age:I
      35: invokevirtual #36  // Method StringBuilder.append:(I)LStringBuilder;
      38: ldc           #38  // String  years old
      40: invokevirtual #21  // Method StringBuilder.append:(LString;)LStringBuilder;
      43: invokevirtual #40  // Method StringBuilder.toString:()LString;
      46: areturn

这段字节码做了什么?它创建了一个 StringBuilder 实例,并通过 append() 方法逐个拼接字符串的各个部分。本质上,这段字节码等价于如下 Java 代码:

new StringBuilder()
  .append(firstName)
  .append(' ') // ASCII 码 32,即空格
  .append(lastName)
  .append(" is ")
  .append(age)
  .append(" years old")
  .toString()

结论:Kotlin 的字符串插值在底层是通过 StringBuilder 实现的。

2.1. 优缺点分析

优点:

  • 实现方式简单直观,Java 和 Kotlin 开发者都很容易理解。

缺点:

  • 随着模板中变量数量的增加,生成的字节码也会变得更长。
  • 启动时 JVM 需要处理更多字节码,影响启动性能。
  • 拼接策略在编译时就已固定,即使未来有更高效的实现方式,也需要重新编译代码才能生效。

那有没有更好的方式?Java 9 和 Kotlin 1.4.20 提供了新方案:InvokeDynamic(简称 Indy)。

3. InvokeDynamic(Indy)

InvokeDynamicJSR 292 的一部分,旨在增强 JVM 对动态类型语言的支持。从 Java 9 开始,字符串拼接默认使用了 invokedynamic 指令。

使用 Indy 的主要优势在于:拼接策略可以在不修改字节码的前提下动态调整。这意味着即使不重新编译代码,也能享受新的优化。

从 Kotlin 1.4.20 开始,Kotlin 编译器也支持使用 Indy 来进行字符串拼接。但需要满足两个条件:

  • 使用 Java 9 或更高版本作为 JVM target
  • 使用 -Xstring-concat=indy-with-constants 编译器参数启用 Indy 拼接

重新编译上面的例子:

>> kotlinc -jvm-target 9 -Xstring-concat=indy-with-constants interpolation.kt

再看字节码变化:

>> javap -c -p com.baeldung.interpolation.Person
public java.lang.String toString();
    Code:
       0: aload_0
       1: getfield      #11       // Field firstName:LString;
       4: aload_0
       5: getfield      #14       // Field lastName:LString;
       8: aload_0
       9: getfield      #18       // Field age:I
      12: invokedynamic #30,  0   // InvokeDynamic #0:makeConcatWithConstants:(LString;LString;I)LString;
      17: areturn

可以看到,无论模板中有多少变量,生成的字节码都是一样的简洁。更少的指令意味着更小的类体积和更快的加载速度。

⚠️ 注意: 这种方式仅适用于 Java 9+ 和 Kotlin 1.4.20+,并且 JVM target 需设为 9 或更高。Kotlin 1.5 已计划将 Indy 设为默认实现。

4. 总结

本文我们对比了 Kotlin 中字符串插值的两种实现方式:

  • StringBuilder:兼容性好,实现简单,适合 Java 8 及以下项目
  • InvokeDynamic(Indy):性能更优,字节码更简洁,适合 Java 9+ 和 Kotlin 1.4.20+ 项目

如果你在开发高性能或大型项目,建议开启 Indy 支持,以提升运行效率和类加载速度。

如需查看完整示例代码,欢迎访问 GitHub 仓库


原始标题:How Does String Interpolation Work in Kotlin?

« 上一篇: Kotlin 与 Scala 对比