1. 引言

Kotlin 比 Java 更加简洁和富有表达力,但它是否带来了性能上的代价?换句话说,选择 Kotlin 而不是 Java 是否会带来性能损失

从本质上讲,Kotlin 编译成的字节码与 Java 是等价的。类、方法、方法参数以及标准流程控制语句(如 iffor)在两者中是相同的。

不过,两者之间也存在一些差异。例如,Kotlin 支持 内联函数(inline functions)。如果一个函数接受 lambda 表达式作为参数,并且被声明为 inline,那么在字节码中将不会生成实际的 lambda 对象。相反,编译器会将 lambda 中的代码直接插入到调用位置。Kotlin 集合的转换函数(如 mapfilterassociatefirstfindany 等)都是内联函数

此外,在 Java 中使用函数式集合转换需要先将集合转换为 Stream,再通过 Collector 收集结果。当处理大型集合的多次转换时,这种开销是合理的。但如果只是对一个短集合做一次 map 操作,这种额外的对象创建成本就变得相对明显。

在本文中,我们将探讨如何衡量 Kotlin 与 Java 的性能差异,并分析这些差异的实际影响。

2. 使用 Java Microbenchmark Harness

由于 Kotlin 最终编译成与 Java 相同的 JVM 字节码,我们可以使用 Java Microbenchmark Harness (JMH) 来对 Kotlin 和 Java 的代码进行性能分析。

我们使用 Gradle 构建项目,并通过 jmh-gradle-plugin 快速接入 JMH 框架。

以下是我们测试中使用的一些 JMH 注解配置:

@Benchmark
@BenchmarkMode(Mode.Throughput)
@Fork(value = 5, warmups = 5)
@OutputTimeUnit(TimeUnit.MILLISECONDS)

我们使用 Apple M1 Pro、32GB 内存、macOS Monterey 12.1、OpenJDK 17.0.1+12-39 的环境进行测试。不同软硬件环境的结果可能不同。

我们将每个测试函数的返回值传入 Blackhole,以防止 JIT 编译器过度优化。同时我们使用合理的预热次数来获取平均值更稳定的测试结果。

3. 内联高阶函数性能对比

第一个测试案例是关于内联高阶函数的性能。

我们模拟一个事务操作:创建事务对象、打开事务、执行传入的动作、提交事务。

Java 中的 lambda 表达式会生成 invokedynamic 指令,JIT 编译器会为此生成一个调用点对象,实现一个函数式接口。虽然这个过程对开发者透明,但确实带来了额外开销。

public static <T> T inTransaction(JavaDatabaseTransaction.Database database, Function<JavaDatabaseTransaction.Database, T> action) {
    var transaction = new JavaDatabaseTransaction.Transaction(UUID.randomUUID());
    try {
        var result = action.apply(database);
        transaction.commit();
        return result;
    } catch (Exception ex) {
        transaction.rollback();
        throw ex;
    }
}

public static String transactedAction(Object obj) throws MalformedURLException {
    var database = new JavaDatabaseTransaction.Database(new URL("http://localhost"), "user:pass");
    return inTransaction(database, d -> UUID.randomUUID() + obj.toString());
}

Kotlin 的写法更简洁,而且在字节码层面完全不同。inline 函数会被直接复制到调用点,不会生成 lambda 对象:

inline fun <T> inTransaction(db: Database, action: (Database) -> T): T {
    val transaction = Transaction(id = UUID.randomUUID())
    try {
        return action(db).also { transaction.commit() }
    } catch (ex: Exception) {
        transaction.rollback()
        throw ex
    }
}

fun transactedAction(arg: Any): String {
    val database = Database(URL("http://localhost"), "user:pass")
    return inTransaction(database) { UUID.randomUUID().toString() + arg.toString() }
}

尽管在实际操作数据库时,网络 IO 会主导性能,但我们仍可以测试内联带来的收益:

Benchmark                            Mode  Cnt     Score       Error   Units
KotlinVsJava.inlinedLambdaKotlin     thrpt  25     1433.691 ± 108.326  ops/ms
KotlinVsJava.lambdaJava              thrpt  25      993.428 ±  25.065  ops/ms

✅ **结果表明,内联效率比 Java 的动态调用点高出 44%**。因此,对于关键路径上的高阶函数,使用 inline 是值得的。

4. 集合函数式转换性能对比

Java 的 Stream API 需要额外创建 StreamCollector 对象,而 Kotlin 的集合函数(如 map)则直接操作集合。

我们分别测试了 Java 和 Kotlin 的 map 方法:

public static List<String> transformStringList(List<String> strings) {
    return strings.stream().map(s -> s + System.currentTimeMillis()).collect(Collectors.toList());
}
fun transformStringList(strings: List<String>) =
    strings.map { it + System.currentTimeMillis() }

我们使用 currentTimeMillis() 确保每次操作结果不同,防止被 JIT 优化。

测试结果如下:

Benchmark                              Mode    Cnt   Score     Error    Units
KotlinVsJava.stringCollectionJava    thrpt   25    1982.486 ± 112.839 ops/ms
KotlinVsJava.stringCollectionKotlin  thrpt   25    1760.223 ± 69.072  ops/ms

❌ **结果表明 Java 竟然比 Kotlin 快了 12%**。这可能是因为 Kotlin 在 map 中加入了额外的空检查和容量预判逻辑。

我们又测试了小集合的情况:

fun mapSmallCollection() =
    (1..10).map { java.lang.String.valueOf(it) }

Java 版本如下:

public static List<String> transformSmallList() {
    return IntStream.range(1, 10)
      .mapToObj(String::valueOf)
      .collect(Collectors.toList());
}

结果如下:

Benchmark                              Mode    Cnt   Score     Error    Units
KotlinVsJava.smallCollectionJava     thrpt   25    15.135 ± 0.932     ops/us
KotlinVsJava.smallCollectionKotlin   thrpt   25    17.826 ± 0.332     ops/us

Kotlin 在处理小集合时更快。不过总体来看,两者差异非常小,在实际生产代码中几乎不会成为瓶颈

5. 可变参数与展开操作符性能对比

Java 的 varargs 本质上是数组的语法糖。如果已有数组,可以直接传入:

public static String concatenate(String... pieces) {
    StringBuilder sb = new StringBuilder(pieces.length * 8);
    for(String p : pieces) {
        sb.append(p).append(",");
    }
    return sb.toString();
}

public static String callConcatenate(String[] pieces) {
    return concatenate(pieces);
}

而 Kotlin 的 vararg 是一个特殊语法,传入数组时必须使用展开操作符 *

fun concatenate(vararg pieces: String): String = pieces.joinToString()

fun callVarargFunction(pieces: Array<out String>) = concatenate(*pieces)

我们测试了展开操作符的性能影响:

Benchmark                              Mode    Cnt   Score     Error    Units
KotlinVsJava.varargsJava               thrpt    25    14.653 ± 0.089    ops/us
KotlinVsJava.varargsKotlin             thrpt    25    12.468 ± 0.279    ops/us

Kotlin 的展开操作带来了约 17% 的性能损失。因此,在性能敏感的代码路径中,应避免使用展开操作符调用 vararg 方法。

6. 修改 Java Bean 与复制 Data Class(包含初始化)

Kotlin 引入了 data class,并鼓励使用不可变字段(val)。如果需要修改字段,必须调用 copy 方法创建新对象:

fun changeField(input: DataClass, newValue: String): DataClass = input.copy(fieldA = newValue)

我们对比了 Kotlin 的 copy 与 Java 的 setter 性能。测试的 DataClassPOJO 均只有一个字段:

data class DataClass(val fieldA: String)
public class POJO {
    private String fieldA;

    public POJO(String fieldA) {
        this.fieldA = fieldA;
    }

    public String getFieldA() {
        return fieldA;
    }

    public void setFieldA(String fieldA) {
        this.fieldA = fieldA;
    }
}

测试结果如下:

Benchmark                              Mode    Cnt   Score     Error    Units
KotlinVsJava.changeFieldJava           thrpt   25    337.300 ± 1.263     ops/us

KotlinVsJava.changeFieldKotlin         thrpt   25    351.128 ± 0.910     ops/us

✅ **Kotlin 的 copy 比 Java 的 setter 快了 4%**。

我们又测试了更复杂的类:

data class DataClass(
    val fieldA: String,
    val fieldB: String,
    val addressLine1: String,
    val addressLine2: String,
    val city: String,
    val age: Int,
    val salary: BigDecimal,
    val currency: Currency,
    val child: InnerDataClass
)

data class InnerDataClass(val fieldA: String, val fieldB: String)

结果如下:

Benchmark                        Mode   Cnt    Score     Error   Units
KotlinVsJava.changeFieldJava     thrpt   25    100,503 ± 1,047    ops/us
KotlinVsJava.changeFieldKotlin   thrpt   25    126,282 ± 0,232    ops/us

Kotlin 的 copy 在复杂类中快了近 3 倍。这是因为不可变类在构造时有性能优势,且 final 字段有时会带来小幅度的性能提升。

7. 修改 Java Bean 与复制 Data Class(排除初始化)

我们隔离了构造函数,只测试修改/复制操作:

@State(Scope.Thread)
public static class InputKotlin {
    public DataClass pojo;

    @Setup(Level.Trial)
    public void setup() {
        pojo = new DataClass(
                "ABC",
                "fieldB",
                "Baker st., 221b",
                "Marylebone",
                "London",
                (int) (31 + System.currentTimeMillis() % 17),
                new BigDecimal("30000.23"),
                Currency.getInstance("GBP"),
                new InnerDataClass("a", "b")
        );
    }

    public String string2 = "XYZ";
}

public void changeFieldKotlin_changingOnly(Blackhole blackhole, InputKotlin input) {
    blackhole.consume(DataClassKt.changeField(input.pojo, input.string2));
}

测试结果如下:

Benchmark                                     Mode    Cnt    Score     Error   Units
KotlinVsJava.changeFieldJava_changingOnly     thrpt   25     364,745 ± 2,470    ops/us
KotlinVsJava.changeFieldKotlin_changingOnly   thrpt   25     163,215 ± 1,235    ops/us

修改字段比复制快了 2.23 倍。但要注意,构造可变对象的成本更高,所以即使复制两次,也比构造一次 + 修改快。

如果你需要修改多个字段,Kotlin 的 copy 优势更明显,因为只需要一次调用,而 Java 需要多次调用 setter。

总体来看,这些性能差异在实际应用中几乎可以忽略,因为 IO 和业务逻辑才是真正的性能瓶颈。

8. 总结

我们通过 JMH 测试比较了 Kotlin 与 Java 的性能差异,得出以下结论:

Kotlin 的性能与 Java 相当,甚至在某些场景下略优

内联函数(inline)对性能提升显著,尤其是高阶函数

Kotlin 的 vararg 展开操作带来了约 17% 的性能损失

对于不可变数据类,copy 操作比构造 + 修改更快,尤其在多字段场景下

使用 Blackhole 和 JMH 可以有效防止编译器优化,帮助我们准确测量性能差异

⚠️ 所有测试结果仅供参考,实际生产环境中性能瓶颈往往在 IO 或业务逻辑中

如需查看完整代码,请访问:GitHub 项目地址


原始标题:Is Kotlin Faster Than Java?