Kotlin
A concise multiplatform language developed by JetBrains
在实际项目中评估 Kotlin
客座文章作者:Urs Peter,高级软件工程师兼 JetBrains 认证 Kotlin 培训师。 对于希望以更系统的方式提升 Kotlin 技能的读者,Urs 还在 Xebia Academy 主办了 Kotlin 技能提升计划。
本文为在以 Java 为主的环境中成功采用 Kotlin 的终极指南系列的第二篇文章,系列将从一位开发者产生好奇心到公司范围转型这一发展过程,逐步讲述 Kotlin 采用在实际团队中如何增长。
阅读第一篇:面向 Java 开发者的 Kotlin 使用入门
评估阶段:在演练场延续 Kotlin 采用
在测试场景中对使用 Kotlin 得心应手后,便可以着手开展更深入的评估。 有两种主要方式供您选择:
1. 使用 Kotlin 构建新的微服务/应用程序
从头开始开发新应用程序或微服务能让您不受遗留代码的限制,充分体验 Kotlin 的全部特性。 这种方式往往能带来最佳学习体验,也能最清晰地展现 Kotlin 的优势。
专家提示:此阶段可以寻求专家支持。 开发者固然对自身能力充满信心,但避免早期错误(例如编写 Java 风格的 Kotlin 代码、不使用 Kotlin 原生库),可以减少数月的技术债务。

对于有 Java 背景的开发者,以下是使用 Kotlin 时可以规避的常见陷阱:
陷阱:选择与 Java 项目中不同的框架。
建议:坚持使用现有的框架。
如果您在 Java 项目中使用的是 Spring Boot,那么在 Kotlin 中继续使用即可。 Spring Boot 对 Kotlin 的支持堪称一流,因此换用其他框架并无额外益处。 此外,您不仅要学习新语言,还要额外学习新框架,这只会徒增复杂性,并无任何优势。
重要提示:Spring 会与 Kotlin 的“设计中继承”原则产生冲突,该原则要求类必须被显式标记为 open 才能进行扩展。
为避免给所有与 Spring 相关的类(如 @Configuration 等)都添加 open 关键字,可以使用以下构建插件:https://kotlinlang.org/docs/all-open-plugin.html#spring-support。 如果您使用知名的在线工具 Spring Initializr 创建 Spring 项目,此构建插件已为您预先配置。
陷阱:以 Java 风格编写 Kotlin 代码,依赖常见的 Java API 而非 Kotlin 标准库:
这种陷阱不胜枚举,下面我们来重点说说最常见的几种:
陷阱 1:使用 Java 流而非 Kotlin 集合
建议:始终使用 Kotlin 集合。
Kotlin 集合与 Java 集合可以完全互操作,同时配备了简洁且功能丰富的高阶函数,这样一来,就没有必要使用 Java 流了。
以下是一个示例,旨在按产品类别分组,选出营收(价格 * 销量)最高的前 3 款产品:
Java
record Product(String name, String category, double price, int sold){}
List products = List.of(
new Product("Lollipop", "sweets", 1.2, 321),
new Product("Broccoli", "vegetable", 1.8, 5);
Map<String, List> top3RevenueByCategory =
products.stream()
.collect(Collectors.groupingBy(
Product::category,
Collectors.collectingAndThen(
Collectors.toList(),
list -> list.stream()
.sorted(Comparator.comparingDouble(
(Product p) -> p.price() * p.sold())
.reversed())
.limit(3)
.toList()
)
)
);
Kotlin
val top3RevenueByCategory: Map<String, List> =
products.groupBy { it.category }
.mapValues { (_, list) ->
list.sortedByDescending { it.price * it.sold }.take(3)
}
Kotlin 与 Java 的互操作性让您可以像使用原生 Kotlin 类一样处理 Java 类和记录,不过您也可以改用 Kotlin (data) 类。
陷阱 2:持续使用 Java 的 Optional。
建议:善用可为 null 的类型。
Java 开发者转向 Kotlin 的核心原因之一就是 Kotlin 内置的为 null 性支持,这能彻底避免 NullPointerExceptions。 因此,尽量只使用可为 null 的类型,不要再用 Optional。 如果您的接口中还存在 Optional, 只需将其转换为可为 null 的类型即可轻松移除,代码如下:
Kotlin
//Let’s assume this repository is hard to change, because it’s a library you depend on
class OrderRepository {
//it returns Optional, but we want nullable types
fun getOrderBy(id: Long): Optional = …
}
//Simply add an extension method and apply the orElse(null) trick
fun OrderRepository.getOrderByOrNull(id: Long): Order? =
getOrderBy(id).orElse(null)
//Now enjoy the safety and ease of use of nullable types:
//Past:
val g = repository.getOrderBy(12).flatMap { product ->
product.goody.map { it.name }
}.orElse("No goody found")
//Future:
val g = repository.getOrderByOrNull(12)?.goody?.name ?: "No goody found"
陷阱 3:继续使用静态包装器。
建议:善用扩展方法。
扩展方法能带来诸多好处:
- 相比包装器,扩展方法能让代码更流畅、可读性更强。
- 扩展方法可以通过代码补全功能找到,而包装器则不能。
- 由于扩展方法需要导入,您可以在应用程序的特定部分有选择地使用扩展功能。
Java
//Very common approach in Java to add additional helper methods
public class DateUtils {
public static final DateTimeFormatter DEFAULT_DATE_TIME_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public String formatted(LocalDateTime dateTime,
DateTimeFormatter formatter) {
return dateTime.format(formatter);
}
public String formatted(LocalDateTime dateTime) {
return formatted(dateTime, DEFAULT_DATE_TIME_FORMATTER);
}
}
//Usage
formatted(LocalDateTime.now());
Kotlin
val DEFAULT_DATE_TIME_FORMATTER: DateTimeFormatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
//Use an extension method, including a default argument, which omits the need for an overloaded method.
fun LocalDateTime.asString(
formatter: DateTimeFormatter = DEFAULT_DATE_TIME_FORMATTER): String =
this.format(formatter)
//Usage
LocalDateTime.now().formatted()
需要注意的是,Kotlin 提供了顶层方法和变量。 这意味着我们可以直接将 DEFAULT_DATE_TIME_FORMATTER 之类的元素声明为顶层,而无需像 Java 那样必须绑定到某个对象。
陷阱 4:依赖(蹩脚的)Java API
建议:使用 Kotlin 更简洁流畅的替代方案。
Kotlin 标准库通过扩展方法让 Java 库的使用体验变得更友好,尽管其底层实现依然是 Java。 几乎所有主流的第三方库和框架(如 Spring)都采取了同样的做法。
标准库示例:
Java
String text;
try (
var reader = new BufferedReader(
new InputStreamReader(new FileInputStream("out.txt"),
StandardCharsets.UTF_8))) {
text = reader
.lines()
.collect(Collectors.joining(System.lineSeparator()));
}
System.out.println("Downloaded text: " + text + "n");
Kotlin
//Kotlin has enhanced the Java standard library with many powerful extension methods, like on java.io.*, which makes input stream processing a snap due to its fluent nature, fully supported by code completion
val text = FileInputStream("path").use {
it.bufferedReader().readText()
}
println("Downloaded text: $textn");
Spring 示例:
Java
final var books = RestClient.create()
.get()
.uri("http://.../api/books")
.retrieve()
.body( new ParameterizedTypeReference<List>(){}); // ⇦ inconvenient ParameterizedTypeReference
Kotlin
import org.springframework.web.client.body
val books = RestClient.create()
.get()
.uri("http://.../api/books")
.retrieve()
.body<List>() //⇦ Kotlin offers an extension that only requires the type without the need for a ParameterizedTypeReference
陷阱 5:为每个 public 类分别创建一个文件
建议:将相关的 public 类合并到一个文件中。
这样一来,无需浏览数十个文件,即可清晰了解某个(子)领域的结构。
Java

Kotlin
//For domain classes consider data classes - see why below
data class User(val email: String,
//Use nullable types for safety and expressiveness
val avatarUrl: URL? = null,
var isEmailVerified: Boolean)
data class Account(val user:User,
val address: Address,
val mfaEnabled:Boolean,
val createdAt: Instant)
data class Address(val street: String,
val city: String,
val postalCode: String)
陷阱 6:依赖可变编程范式
建议:主动采用不可变性 – 这是 Kotlin 的默认原则
许多编程语言(包括 Java)的趋势已十分明显:不可变性正逐渐取代可变性。
原因很简单:不可变性可以避免意外的副作用,让代码更安全、更可预测,也更易于理解。 它还能简化并发,因为不可变数据可以在多个线程间自由共享,且不存在竞争条件的风险。
正因为如此,大多数现代语言(包括 Kotlin 在内)要么默认强调不可变性,要么大力倡导不可变性。 在 Kotlin 中,不可变性是默认选择,不过在确实需要时,仍然可以选择可变性。
以下是关于 Kotlin 不可变性利器的快速指南:
1. 优先使用 val 而非 var
优先选择 val 而非 var。 如果您使用了本可替换为 val 的 var,IntelliJ IDEA 会通知您。
2. 使用(不可变的)data 类与 copy(...)
对于领域相关的类,请使用 data 类与 val。 Kotlin data 类常被拿来与 Java records 作比较。 尽管二者存在一定重叠,但 data 类提供了一个杀手锏级特性 copy(...)。缺少这一方法会导致 records 的转换操作(这在业务逻辑中经常需要)变得十分繁琐:
Java
//only immutable state
public record Person(String name, int age) {
//Lack of default parameters requires overloaded constructor
public Person(String name) {
this(name, 0);
}
//+ due to lack of String interpolation
public String sayHi() {
return "Hello, my name is " + name + " and I am " + age + " years old.";
}
}
//Usage
final var jack = new Person("Jack", 42);
jack: Person[name=Jack, age=5]
//The issue is here: transforming a record requires manually copying the identical state to the new instance ☹️
final var fred = new Person("Fred", jack.name);
Kotlin
//also supports mutable state (var)
data class Person(val name: String,
val age: Int = 0) {
//string interpolation
fun sayHi() = "Hi, my name is $name and I am $age years old."
}
val jack = Person("Jack", 42)
jack: Person(name=Jack, age=42)
//Kotlin offers the copy method, which, due to the ‘named argument’ feature, allows you to only adjust the state you want to change 😃
val fred = jack.copy(name = "Fred")
fred: Person(name=Fred, age=42)
此外,应尽可能为领域相关的类使用 data 类。 它们的不可变特性可以确保在处理应用程序核心逻辑时,提供安全、简洁、无忧的体验。
建议:优先使用不可变集合而非可变集合
不可变集合在线程安全方面具有显著优势,不仅可以安全地传递,而且更易于理解。 尽管 Java 集合为集合提供了一些不可变特性,但其使用存在风险,因为这很容易在运行时引发异常:
Java
List.of(1,2,3).add(4); ❌unsafe 😬! .add(...) compiles, but throws UnsupportedOperationException
Kotlin
//The default collections in Kotlin are immutable (read-only) listOf(1,2,3).add(4); //✅safe: does not compile val l0 = listOf(1,2,3) val l1 = l0 + 4 //✅safe: it will return a new list containing the added element l1 shouldBe listOf(1,2,3,4) //✅
使用 Collections.unmodifiableList(...) 也是如此,它们不仅不安全,还需要额外分配内存:
Java
class PersonRepo {
private final List cache = new ArrayList();
// Java – must clone or wrap every call
public List getItems() {
return Collections.unmodifiableList(cache); //⚠️extra alloc
}
}
//Usage
personRepo.getItems().add(joe) ❌unsafe 😬! .add(...) can be called but throws UnsupportedOperationException
Kotlin
class PersonRepo {
//The need to type ‘mutable’ for mutable collections is intentional: Kotlin wants you to use immutable ones by default. But sometimes you need them:
private val cache: MutableList = mutableListOf()
fun items(): List = cache //✅safe: though the underlying collection is mutable, by returning it as its superclass List, it only exposes the read-only interface
}
//Usage
personRepo.items().add(joe) //✅safe:😬! Does not compile
在并发场景中,应优先使用不可变数据结构,包括集合。 在 Java 中,这需要额外费力使用一些特殊的集合,这些集合提供的 API 要么不同,要么受限,例如 CopyOnWriteArrayList。 另一方面,在 Kotlin 中,只读的 List 几乎能满足所有用例的需求。
如果需要可变且线程安全的集合,Kotlin 提供了持久化集合(persistentListOf(...)、persistentMapOf(...)),它们都共用同一个强大的接口。
Java
ConcurrentHashMap persons = new ConcurrentHashMap();
persons.put("Alice", 23);
persons.put("Bob", 21);
//not fluent and data copying going on
Map incPersons = new HashMap(persons.size());
persons.forEach((k, v) -> incPersons.put(k, v + 1));
//wordy and data copying going on
persons
.entrySet()
.stream()
.forEach(entry ->
entry.setValue(entry.getValue() + 1));
Kotlin
persistentMapOf("Alice" to 23, "Bob" to 21)
.mapValues { (key, value) -> value + 1 } //✅same rich API like any other Kotlin Map type and not data copying going on
陷阱 7:持续使用构建器(更糟的是:尝试使用 Lombok)
建议:使用命名实参。
构建器在 Java 中十分常见。 尽管构建器使用起来很方便,但它们会增加额外代码、存在安全隐患,还会增加复杂性。 在 Kotlin 中,构建器毫无用武之地,因为一个简单的语言功能就使其沦为多余:命名实参。
Java
public record Person(String name, int age) {
// Builder for Person
public static class Builder {
private String name;
private int age;
public Builder() {}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Person build() {
return new Person(name, age);
}
}
}
//Usage
new JPerson.Builder().name("Jack").age(36).build(); //compiles and succeeds at runtime
new JPerson.Builder().age(36).build(); //❌unsafe 😬: compiles but fails at runtime.
Kotlin
data class Person(val name: String, val age: Int = 0) //Usage - no builder, only named arguments. Person(name = "Jack") //✅safe: if it compiles, it always succeeds at runtime Person(name = "Jack", age = 36) //✅
2. 扩展/转换现有的 Java 应用程序
如果没有从零开始尝试 Kotlin 的条件,不妨在现有 Java 代码库中添加新的 Kotlin 功能或完整的 Kotlin 模块。 得益于 Kotlin 与 Java 的无缝互操作性,您编写的 Kotlin 代码对 Java 调用者而言可以像 Java 代码一样。 这种方式可以实现以下目的:
- 无需大规模重写即可逐步迁移
- 在特定场景中对 Kotlin 进行实际测试
- 通过生产级 Kotlin 代码建立团队信心
与其漫无目的地开始,不妨考虑以下这些不同方式:
由外而内:
从应用程序的“叶子”部分(例如,控制器、批处理作业等)开始,然后逐步向核心领域推进。 这种方式具有以下优势:
- 编译时隔离。叶子类很少被其他组件依赖,因此您可以将其转换为 Kotlin,而系统其余部分仍能保持不变并正常构建。
- 减少连锁编辑。 借助无缝的互操作性,转换后的 UI/控制器调用现有 Java 领域代码时几乎无需更改。
- 更小的拉取请求,更简单的审查。 可以按文件或按功能逐步迁移。
由内而外:
从核心部分着手,再逐步推进到外层,这种方式往往风险更高,因为它会削弱前文提到的“由外而内”方式的优势。 不过,对于以下情况,这仍是一种可行的选择:
- 核心部分非常小或自成体系。 如果领域层只有少量 POJO 和服务,尽早转换可能成本较低,还能立即解锁符合习惯的构造(data 类、值类、密封层次结构)。
- 本身就要进行架构重构。 如果计划在迁移的同时重构不变量或引入 DDD 模式(值对象、聚合),有时先在 Kotlin 中重新设计领域层会更整洁。
- 严格的 null 安全约定。 将 Kotlin 置于核心位置,可以将领域层打造成一座“ null 安全堡垒”;外层的 Java 代码仍可能传入 null 值,但边界会变得清晰明确,也更易于管控。
逐个模块进行
- 如果您的架构是按功能而非按层组织的,且模块规模可控,则不妨逐个转换模块。
用于将 Java 转换为 Kotlin 的语言功能
Kotlin 提供了多种功能(主要是注解),能让您的 Kotlin 代码表现得如同原生 Java 代码一般。 在 Kotlin 与 Java 共存于同一代码库的混合环境中,这一点的价值尤为突出。
Kotlin
class Person @JvmOverloads constructor(val name: String,
var age: Int = 0) {
companion object {
@JvmStatic
@Throws(InvalidNameException::class)
fun newBorn(name: String): Person = if (name.isEmpty())
throw InvalidNameException("name not set")
else Person(name, 0)
@JvmField
val LOG = LoggerFactory.getLogger(KPerson.javaClass)
}
}
Java
//thanks to @JvmOverloads an additional constructor is created, propagating Kotlin’s default arguments to Java
var john = new Person("John");
//Kotlin automatically generates getters (val) and setters (var) for Java
john.setAge(23);
var name = ken.getName();
//@JvmStatic and @JvmField all accessing (companion) object fields and methods as statics in Java
//Without @JvmStatic it would be: Person.Companion.newBorn(...)
var ken = Person.newBorn("Ken");
//Without @JvmField it would be: Person.Companion.LOG
Person.LOG.info("Hello World, Ken ;-)");
//@Throws(...) will put the checked Exception in the method signature
try {
Person ken = Person.newBorn("Ken");
} catch (InvalidNameException e) {
//…
}
Kotlin
@file:JvmName("Persons")
package org.abc
@JvmName("prettyPrint")
fun Person.pretty() =
Person.LOG.info("$name is $age old")
Java
//@JvmName for files and methods makes accessing static fields look like Java: without it would be: PersonKt.pretty(...) Persons.prettyPrint(ken)
IntelliJ IDEA 的 Java 转 Kotlin 转换器
IntelliJ IDEA 提供了 Java 转 Kotlin 转换器,理论上,该工具可以自动完成转换工作。 不过,转换生成的代码往往不够理想,因此建议仅将其作为起点。 在此基础上,还需进一步将代码打磨成更地道的 Kotlin 风格。 本博文系列的最后一篇大规模采用 Kotlin 的成功因素将深入探讨此主题。
如果以 Java 为起点,写出的 Kotlin 代码很可能带着 Java 的影子。 这类代码虽能带来一些好处,但无法充分释放 Kotlin 的潜力。 因此,我更倾向于编写新应用程序这种方式。
系列博文后续内容
在在以 Java 为主的环境中成功采用 Kotlin 的终极指南系列博文的这一篇中,我们介绍了 Kotlin 实验如何逐步发展为生产级代码。 我们的下一篇博文将聚焦于 Kotin 采用的人文层面:说服同事。 文中将介绍如何用清晰且以代码为依据的论据进行阐述、如何指导新加入的开发者,以及如何在团队内部建立一个规模虽小却能持续发展的 Kotlin 社区。
