Kotlin
A concise multiplatform language developed by JetBrains
在贵公司中扩大 Kotlin 的采用
客座文章作者:Urs Peter,高级软件工程师兼 JetBrains 认证 Kotlin 培训师。对于希望以更系统的方式提升 Kotlin 技能的读者,Urs 还在 Xebia Academy 主办了 Kotlin 技能提升计划。
本文为在以 Java 为主的环境中成功采用 Kotlin 的终极指南系列的第三篇文章,系列将从一位开发者产生好奇心到公司范围转型这一发展过程,逐步讲述 Kotlin 采用在实际团队中如何增长。
本系列的所有内容:
广而告之:赢得开发者同僚的支持与认可
截至目前,您已组建起一支认可 Kotlin 优势的核心团队。现在进入关键阶段:您如何扩大其采用范围?
这个阶段的关键在于赢得持怀疑态度的 Java 开发者的认可与支持。硬性因素与软性因素将发挥关键作用:
硬性因素:代码本身
- 用代码证明实力
软性因素:为开发者提供支持并促进其协作交流
- 助力开发者轻松上手
- 提供自学资料
- 建立内部 Kotlin 社区
- 保持耐心…
用代码证明实力
充分利用通过 Kotlin(重新)编写应用程序所积累的经验,以具体易懂的方式展现其优势:
- 事实胜于雄辩:通过对比 Java 与 Kotlin 代码段,直观呈现 Kotlin 带来的简洁性优势。
- 跳出细节,聚焦核心设计范式:Kotlin 并非仅仅与 Java 存在差异,它构建于安全性、兼顾极致可读性与可维护性的简洁性、函数一等公民地位和可扩展性的基础之上,这些特性共同解决了 Java 的根本性缺陷,同时仍能与 Java 生态系统实现完全互操作。
以下是部分具体示例:
1. null 安全特性:告别“十亿美元级错误”
Java
//This is why we have the billion-dollar mistake (not only in Java…) Booking booking = null; //🤨 This is allowed but causes: booking.destination; //Runtime error 😱 Optional booking = null; //Optionals aren’t safe from null either: 🤨 booking.map(Destination::destination); //Runtime error 😱
Java 虽已逐步添加部分 null 安全特性(例如 Optional 类与注解,如 @NotNull 等),但始终未能解决这一根本性问题。此外,Valhalla 项目(null 限制类型与可为 null 类型)并不会为 Java 引入 null 安全机制,而是仅提供更多可供选择的选项。
Kotlin
//Important to realize that null is very restricted in Kotlin: val booking:Booking? = null //…null can only be assigned to Nullable types ✅ val booking:Booking = null //null assigned to a Non-nullable types yields a Compilation error 😃 booking.destination //unsafely accessing a nullable type directly causes a Compilation error 😃 booking?.destination //only safe access is possible ✅
Kotlin null 安全的一大优势在于,它不仅安全,还拥有极高的实用性。这正是“鱼与熊掌可以兼得”的经典范例:
假设我们有如下模型:
Kotlin
data class Booking(val destination:Destination? = null) data class Destination(val hotel:Hotel? = null) data class Hotel(val name:String, val stars:Int? = null)
Java
public record Booking(Optional destination) {
public Booking() { this(Optional.empty()); }
public Booking(Destination destination) {
this(Optional.ofNullable(destination));
}
}
public record Destination(Optional hotel) {
public Destination() { this(Optional.empty()); }
public Destination(Hotel hotel) {
this(Optional.ofNullable(hotel));
}
}
public record Hotel(String name, Optional stars) {
public Hotel(String name) {
this(name, Optional.empty());
}
public Hotel(String name, Integer stars) {
this(name, Optional.ofNullable(stars));
}
}
构造对象
Java
//Because Optional is a wrapper, the number of nested objects grows, which doesn’t help readability
final Optional booking = Optional.of(new Booking(
Optional.of(new Destination(Optional.of(
new Hotel("Sunset Paradise", 5))))));
Kotlin
//Since nullability is part of the type system, no wrapper is needed: The required type or null can be used.
val booking:Booking? = Booking(Destination(Hotel("Sunset Paradise", 5)))
遍历嵌套对象
Java
//traversing a graph of Optionals requires extensive unwrapping
final var stars = "*".repeat(booking
.flatMap(Booking::getDestination)
.flatMap(Destination::getHotel)
.map(Hotel::getStars).orElse(0)); //-> "*****"
Kotlin
//Easily traverse a graph of nullable types with: ‘?’, use ?: for the ‘else’ case. val stars = "*".repeat(booking?.destination?.hotel?.stars ?: 0) //-> "*****"
解包嵌套对象
Java
//extensive unwrapping is also needed for printing a leaf booking.getDestination() .flatMap(Destination::getHotel) .map(Hotel::getName) .map(String::toUpperCase) .ifPresent(System.out::println);
Kotlin
//In Kotlin we have two elegant options:
//1. we can again traverse the graph with ‘?’
booking?.destination?.hotel.?name?.uppercase()?.also(::println)
//2. We can make use of Kotlin’s smart-cast feature
if(booking?.destination?.hotel != null) {
//The compiler has checked that all the elements in the object graph are not null, so we can access the elements as if they were non-nullable types
println(booking.destination.hotel.uppercase())
}
Java 缺乏 null 安全支持是开发者面临的核心痛点,这不仅导致开发者需要采用防御性编程模式、不同的为 null 性构造(甚至完全缺失),还会增加代码冗长度。此外,NullPointerException 约占应用程序崩溃原因的三分之一(JetBrains 博客)。Kotlin 的编译时 null 检查可以彻底杜绝这类运行时故障。这也是时至今日,null 安全仍是向 Kotlin 迁移的首要驱动因素的原因所在。
2. 集合是您的得力助手,而非绊脚石
Spring 框架的创作者 Rod Johnson 在近期的一次采访中表示,促使他尝试 Kotlin 的并非可为 null 类型,而是过度复杂的 Java Streams API:Creator of Spring: No desire to write Java(Spring 的创作者:已无意愿编写 Java)。
以下示例将阐述 Java Streams API 之所以极为复杂的各类原因,以及 Kotlin 如何解决所有这些问题:
Java
public record Product(String name, int... ratings){}
List products = List.of(
new Product("gadget", 9, 8, 7),
new Product("goody", 10, 9)
);
Map maxRatingsPerProduct =
//🤨 1. Stream introduces indirection
products.stream()
//🤨 1. Always to and from Stream conversion
.collect(
//🤨 2. Lacks extension methods, so wrappers are required
Collectors.groupingBy(
Product::name,
//🤨 2. Again…
Collectors.mapping( groupedProducts ->
//🤨 3. (too) low-level types, arrays, and primitives cause extra complexity
//🤨 4. No API on Array, always wrap in stream
Arrays.stream(groupedProducts.ratings())
.max()
//🤨 5. Extra verbosity due to Optional
.orElse(0.0),
//🤨 6. No named arguments: what does this do?
Collectors.reducing(0, Integer::max)
)
));
Kotlin
//😃 rich and uniform Collection API - even on Java collections - due to extension methods
val maxRatingsPerProduct = products.
.groupBy { it.name }
.mapValues { (_, groupedProducts) -> //😃 destructuring for semantic precision
//😃 built-in nullability support, and the same API for
//arrays like other Collections
groupedProducts.flatMap { it.ratings }
.maxOrNull() ?: 0
}
}
借助 Kotlin 统一的集合框架,不同集合类型间的转换变得极为简洁:
Java
int[] numbers = {1, 3, 3, 5, 2};
Set unique = Arrays.stream(numbers).boxed().collect(Collectors.toSet());
Map evenOrOdd = unique.stream()
.collect(Collectors.toMap(
n -> n,
n -> n % 2 == 0));
Kotlin
val numbers = arrayOf(1, 3, 3, 5, 2)
val unique: Set = numbers.toSet() //😃 simply call to to do the conversion
val evenOrOdd: Map = unique.associateWith { it % 2 == 0 }
最终效果:
- 功能丰富且直观的集合 API:管道按从左到右的顺序阅读(跟英语习惯相同),无需嵌套在收集器调用中。
- 样板代码更少、复杂度更低:没有 Collectors.groupingBy、Stream、Optional 和 Arrays.stream。
- 统一的思维模型:无论您从
List、Set、Array还是基元数组入手,使用的都是相同的集合操作符。 - 高效无负担:编译器仅在不可避免时才插入装箱/拆箱操作,您只需编写常规代码即可。
- null 安全集成:集合 API 全面支持为 null 性,提供多种辅助方法,这类方法通常以
orNull(...)为后缀。 - 使用惯用包装器实现 Java 无缝互操作:得益于扩展方法,您可以获得二者的优势,在 Java 集合的基础上,获得功能丰富的集合体验。
简而言之,Kotlin 将筛选、映射、分组等日常集合操作提升为一等公民级别的可组合函数,让您专注于表达想要什么,而非纠结于如何实现的繁琐流程。
3. Kotlin 摒弃了 checked 异常,代码安全性却更胜一筹
Java 是为数不多仍支持 checked 异常的语言之一。尽管 checked 异常最初作为一项安全特性实现,但它并未兑现其承诺。它们的冗长、仅做无用功或直接将异常作为 RuntimeException 重新抛出的无意义 catch 块,以及与 lambda 的不一致,正是其非但无法提升代码安全性,反而造成阻碍的部分原因。
Kotlin 采用了几乎所有其他编程语言(如 C#、Python、Scala、Rust 和 Go)均验证有效的范式,仅在不可恢复的场景下使用异常。
以下示例凸显了 checked 异常给代码带来的阻碍,却并未带来任何安全性提升:
Java
public String downloadAndGetLargestFile(List urls) {
List contents = urls.stream().map(urlStr -> {
Optional optional;
try {
optional = Optional.of(new URI(urlStr).toURL());
//🤨 Within lambdas checked exceptions are not supported and must always be caught...
} catch (URISyntaxException | MalformedURLException e) {
optional = Optional.empty();
}
return optional;
}).filter(Optional::isPresent) //Quite a mouthful to get rid of the Optional...
.map(Optional::get)
.map(url -> {
try (InputStream is = url.openStream()) {
return new String(is.readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException e) {
//🤨… or re-thrown, which is annoying, I don’t really care about IOE
throw new IllegalArgumentException(e);
}
}).toList();
//🤨 An empty List results in a NoSuchElementException, so why is it not checked? The chance that the List is empty is as high as the other two cases above...
return Collections.max(contents);
}
Kotlin
//😃 safe return type
fun downloadAndGetLargestFile(urls: List): String? =
urls.mapNotNull { //😃 convenient utility methods to rid of null
//😃 try catch is possible, yet runCatching is an elegant way to convert an exception to null
runCatching { URI(it).toURL() }.getOrNull()
}.maxOfOrNull{ //😃 safe way to retrieve the max value
it.openStream().use{ it.reader().readText() } //😃 convenient extension methods to make java.io streams fluent
}
4. 函数作为一等公民
Kotlin 将函数视为一等公民,如果您此前使用的是 Java,可能会产生疑问:这究竟意味着什么?又为何至关重要?
核心差异在于 Java 的局限性,其函数式能力大多仅通过 lambda 体现在调用端,而声明端仍受限于冗长且不够直观的函数式接口。在 Kotlin 中,您无需编写任何样板代码即可定义、传递、返回与组合函数,这让函数式编程变得更具表达力和自然。
Java
public void doWithImage(
URL url,
//🤨 Function interfaces introduce an indirection: because we don’t see the signature of a Function we don’t know what a BiConsumer does, unless we look it up
BiConsumer f) throws IOException {
f.accept(url.getFile(), ImageIO.read(url));
}
//🤨 Same here
public void debug(Supplier f) {
if(isDebugEnabled()) {
logger.debug("Debug: " + f.get());
}
}
//🤨 calling no-argument lambdas is verbose
debug(() -> "expensive concat".repeat(1000));
Kotlin
fun doWithImage(
url: URL,
//😃 Kotlin has a syntax for declaring functions: from the signature, we see what goes in and what goes out
f:(String, BufferedImage) -> Unit) =
f(url.file, ImageIO.read(url))
//😃 same here: nothing goes in, a String goes out
fun debug(msg: () -> String) {
if(isDebugEnabled) {
logger.debug(msg())
}
}
//😃 convenient syntax to pass a lambda: {}
debug{"expensive concat".repeat(1000)}
Kotlin 提供了清晰简洁的函数声明语法,仅通过签名,您就能立刻知晓输入与输出,无需导航至外部函数式接口。
Java 的函数式编程特性需要通过大量 java.util.function.* 接口实现,这些接口的签名冗长复杂,这使得函数式编程的使用颇为繁琐。相比之下,Kotlin 将函数视为一等公民:这些接口对开发者完全隐藏,却仍能与 Java 的方式保持完全互操作:

因此,在 Kotlin 中使用函数变得格外简洁直观,这将大幅降低您在代码中运用这一强大编程理念的门槛。
5. 利用协程实现无负担并发
如果您需要高吞吐量、单个请求内的并行处理或流式传输,Java 中唯一的选择便是响应式库(例如,Reactor 和 RxJava),这类库可以在 Spring WebFlux、Vert.X、Quarkus 等框架中使用。
这类库的问题在于其复杂度众所周知,还会强制您使用函数式编程。因此,它们的学习曲线十分陡峭,且极易出现错误,而这些错误在应用程序承受负载时,可能会引发严重后果。这很可能就是响应式编程始终未能成为主流的原因。
注意:尽管虚拟线程与响应式库存在一定功能重叠,但它并不能替代响应式库。虚拟线程支持非阻塞 I/O,但不提供并行处理或响应式流等特性。一旦主流框架提供支持,结构化并发与作用域值也将具备并行处理能力。对于响应式流,您始终需要依赖响应式库。
假设您是一名使用 Spring Boot 的 Java 开发者,并且想要在单个请求内发起并行调用。最终您会得到这样的代码:
@PostMapping("/users")
@ResponseBody
@Transactional
public Mono storeUser(@RequestBody User user) {
Mono avatarMono = avatarService.randomAvatar();
Mono validEmailMono = emailService.verifyEmail(user.getEmail());
//🤨 what does ‘zip’ do?
return Mono.zip(avatarMono, validEmailMono).flatMap(tuple ->
if(!tuple.getT2()) //what is getT2()? It’s the validEmail Boolean…
//🤨 why can I not just throw an exception?
Mono.error(new InvalidEmailException("Invalid Email"));
else personDao.save(UserBuilder.from(user)
.withAvatarUrl(tuple.getT1()));
);
}
尽管从运行时角度来看,这段代码的执行表现完全没问题,但它引入的偶发复杂性却极为显著:
- 在整个调用链中,代码完全被
Mono/Flux主导,这就迫使您必须对所有模型对象进行包装。 - 代码中随处可见
zip、flatMap等众多复杂操作符。 - 您无法使用抛出异常等标准编程构造
- 代码的业务意图大打折扣:代码重心全放在
Mono和flatMap上,这就从业务视角掩盖了实际要实现的核心逻辑。
好消息是,Kotlin 协程为此提供了一种强大的解决方案。协程可被视为语言层面的响应式实现。因此,它们兼具两者的优势:
- 您可以像以往一样编写顺序代码。
- 代码在运行时会以异步/并行方式执行。
上述 Java 代码转换为协程后的写法如下:
@GetMapping("/users")
@ResponseBody
@Transactional
suspend fun storeUser(@RequestBody user:User):User = coroutineScope {
val avatarUrl = async { avatarService.randomAvatar() }
val validEmail = async { emailService.verifyEmail() }
if(!validEmail.await()) throw InvalidEmailException("Invalid email")
personRepo.save(user.copy(avatar = avatarUrl.await()))
}
Kotlin 的 suspend 关键字以清晰简洁的方式实现了结构化、非阻塞执行。借助 async{} 和 await(),它可以促进并行处理,无需深度嵌套的回调,也无需 Mono 或 CompletableFuture 这类复杂构造。
因此,代码复杂度会降低,开发者体验和可维护性会提升,同时保持完全一致的性能特征。

注意:并非所有基于 Java Web 的主流框架都同样支持协程。Spring 对此支持良好,Micronaut 亦是如此。Quarkus 目前对协程的支持较为有限。
6. 不过话说回来,Java 也在不断发展!
Java 持续向前发展,推出了 records、模式匹配等特性,同时还有 Amber、Valhalla 和 Loom 等后续项目在推进中。这种稳步发展不仅增强了 JVM 的能力,也让整个生态系统受益。
但关键问题在于:这些“新”的 Java 特性大多已被 Kotlin 开发者使用多年。null 安全、值类、顶层函数、默认实参、简洁的集合以及一等函数均是 Kotlin 设计之初就内置的特性,且以更统一、对开发者更友好的方式呈现。这也是 Kotlin 代码往往更简洁、更安全,且开发效率大幅提升的原因所在。
而且优势不止于此:Kotlin 还能从 Java 的创新中获益。JVM 层面的进步,如虚拟线程和 Loom,或 Valhalla 的性能提升,同样可以无缝应用于 Kotlin。
简而言之:Java 不断演进,但 Kotlin 从设计之初就致力于为开发者提供所需的现代化工具,这使其成为构建未来系统的安全、现代化且具有前瞻性的选择。

7. Kotlin 的演进优势
与现代编程语言相比,老牌编程语言不可避免地背负着历史包袱。在支持海量现有代码库的同时对语言进行更新,这给语言设计者带来了独特的挑战。Kotlin 拥有两项关键优势:
站在巨人的肩膀上:Kotlin 的初始设计团队并未重复造轮子,而是从主流编程语言中汲取了经过验证的编程范式,并将其整合为一个连贯统一的整体。这种设计思路充分吸收了整个编程社区的演进经验,实现了学习成果的最大化。
从 Java 的不足中吸取经验:Kotlin 的设计者得以观察到 Java 的缺陷,并从一开始就制定了完善的解决方案。
想要深入了解 Kotlin 的演进历程,Kotlin 初始设计团队的 Andrey Breslav 曾在阿姆斯特丹的 Kotlin 开发者大会上发表过一场精彩演讲:Shoulders of Giants: Languages Kotlin Learned From – Andrey Breslav
软性因素:支持开发者顺畅踏上 Kotlin 采用之路,并构建开发者联结
1. 助力开发者轻松上手
这些直观对比 Java 与 Kotlin 的代码段的目的是激发开发者对 Kotlin 的兴趣。不过,仅凭代码本身并不足以赢得 Java 开发者的青睐与认同。为加快 Kotlin 的采用并确保开发者顺利入门,需要提供以下支持:
- 示例项目:一个可以直接运行的项目,包含并列呈现的 Java 与 Kotlin 代码,在迁移过程中为团队提供实用参考。
- 内置质量检查:预配置了 SonarQube、ktlint、detekt 等工具,从一开始就帮助开发者编写整洁、一致且易于维护的代码。这将帮助您应用一致的 lint 规则、测试框架、库和 CI 管道,从而减少团队间的协作阻力。
- 指导与支持:资深 Kotlin 工程师随时为新团队提供指导,解答疑问,并在开发初期阶段提供实操建议。
- 这一点尤为重要:只需来自其他团队、曾经历过这些阶段的资深开发者提供数小时的指导,就能避免大量麻烦与技术债务。
少量的支持与指导是培养开发者对 Kotlin 持久热情的最有效方式。
2. 提供自学资料
尤其是对于从 Java 转型的开发者而言,自主学习 Kotlin 的基础知识完全可行。提前提供一些资源,既能为开发者顺畅掌握 Kotlin 并提升工作效率铺平道路,也能降低入门门槛。
- JetBrains “Tour of Kotlin”(入门指南)
Kotlin 官方网站上的一款基于浏览器的简短教程。 - Kotlin Koans
由 JetBrains 维护的一系列轻量化编程挑战。 - Udacity “Kotlin Bootcamp for Programmers”
与 Google 合作打造的完整视频课程。
注意:尽管自学对于掌握基础知识十分有益,但它也存在一些弊端。弊端之一在于自主性带来的弹性空间:当陷入日常工作的忙碌节奏时,人们很容易放弃自学。此外,您将无法获得来自从业者的反馈,这些从业者深谙 Kotlin 惯用写法的精妙细节,知晓如何正确应用这些写法。自学完成后,您很可能会写出 Java 风格的 Kotlin 代码,这种写法虽能带来一定收益,却无法充分发挥 Kotlin 语言的全部潜力。
除非缺乏优质指导,否则传统课程会极具价值。参与这类课程十分必要:您可以与同水平的同行交流,还能获得资深专业人士的答疑,这将帮助您在初期转型阶段更快上手,同时减少非地道的 Kotlin 代码。
3. 建立内部 Kotlin 社区
在公司内部提升全员 Kotlin 技术水平的最有效方式之一,就是建立并重点培育一个内部技术社区。
- 启动内部 Kotlin 社区
- 首先寻找一支至少由 3 至 6 名开发者组成的核心团队,这些开发者需要愿意为 Kotlin 社区投入精力。同时确保他们的管理者为其这项工作提供时间支持并给予认可。
- 团队组建完成后,邀请 Kotlin 社区的知名演讲者,组织一场全公司范围的启动大会。这将点燃大家对 Kotlin 的热情,为社区发展注入强劲动力。
- 安排定期聚会(每月一次或每两周一次),确保发展势头不中断。
- 创建一个共享聊天频道/维基页面,用于存放问题、代码段和活动笔记。
- 邀请(外部)演讲者
- 邀请已将 Kotlin 投入生产环境的工程师,分享实践经验与心得。
- 穿插开展深度技术演讲(协程、KMP、函数式编程)与高阶案例研究(迁移策略、工具实操技巧)。
- 分享内部其他项目的经验教训
- 请项目负责人分享他们在 Kotlin 应用中的经验,包括有效做法、失败教训以及可衡量的成果。
- 这些洞察可以整理成一份《Kotlin 实践指南》,供新团队直接参考使用。
- 为公司内部开发者提供展示平台
- 组织闪电演讲环节,任何人都可以在 5 至 10 分钟内演示实用技巧、推荐相关库,或分享自己攻克的技术难题。
- 公开表彰贡献者 – 在全员会议或内部简报中公开致谢可以提升参与度与知识共享氛围。
- 保持高效的反馈闭环
- 每次活动结束后,通过快速问卷收集关于内容清晰度和实用性的反馈,再据此调整后续的活动议程。
- 轮换组织职责,确保社区不依赖单一核心人物,实现长期稳健发展。
注意:上述诸多建议看似简单直接。不过,维持社区活力与存续所需付出的努力不容小觑。
4. 保持耐心…
文化转型需循序渐进,不可急于求成。当您对一款能产生实际价值的工具充满热情时,需要警惕急于推进的倾向,这往往会适得其反。有效的做法是借助上述各类活动培育采用流程,始终遵循示范而非说教的核心原则。
系列博文后续内容
我们将工作重心从说服开发者,转移到争取决策者的认可与支持。下一篇文章将介绍如何基于真实数据和可衡量成果,为 Kotlin 的采用构建有说服力的业务用例。您会学习如何将开发者的实践成果转化为打动管理层的论据,如何将开发效率的提升与成本节约直接关联,以及如何阐明 Kotlin 绝非单纯的技术升级 – 对于团队与公司而言,它更是一项具备战略意义的举措。
本博文英文原作者:
