免费下载!由 O’Reilly 出版的《NGINX 完全指南》中文版已正式上线 了解详情
写点什么

Kotlin 的陷阱以及如何避免

  • 2017-10-18
  • 本文字数:3612 字

    阅读完需:约 12 分钟

最近 Kotlin 特别流行,并且我也赞同 Kotlin 是一个经过深思熟虑后被设计出的语言,除了下面提到的缺点之外。

我会在本文向你分析一些我在开发过程中遇到的陷阱,并且教你如何避免他们。

谜一样的 null

当你使用 Kotlin 编写程序的时候,无需考虑 null 的处理。这会让你忘记 null 其实是无处不在的,只不过被隐藏了起来。看看下面这个表面看起来没有问题的类:

复制代码
class Foo {
private val c: String
init {
bar()
c = ""
}
private fun bar() {
println(c.length)
}
}

如果你尝试初始化这个类,那么代码就会抛出一个 NullPointerException。因为 bar 方法尝试在 c 变量初始化之前就访问它。

尽管这个代码本身就是有问题的,才导致异常抛出。但是更糟糕的是你的编译器不会发现这一点。

Kotlin 可以帮你在绝大部分情况下避免 null,但是你不能因此而忘记 null 的存在。否则迟早有一天你会碰上类似的问题。

来自 JDK 的 null

Kotlin 的标准库能够很好地处理 null。但是如果你使用了 JDK 中的类,你需要自己处理关于 JDK 方法调用可能产生的空指针。

大部分情况下 Kotlin 的标准库就足够了,但是有时你需要使用到 ConcurrentHashMap:

复制代码
val map = ConcurrentHashMap<string string="">()
map["foo"] = "bar"
val bar: String = map["foo"]!!
</string>,>

这时,你需要使用 !! 操作符。但某些情况下你还可以使用像 (?) 这样的对 null 安全的操作符来替换它。尽管如此,当你使用 !! 或者 ? ,或者编写了一个适配器来使用 Java 类库的时候,你会发现代码因为这些修改而变的混乱。这是你无法避免的问题。

你还可能会碰上更多更可怕的问题。当你使用 JDK 类中的方法的时候,返回值可能是 null,而且没有什么像 Map 访问一样的语法糖。

考虑如下例子:

复制代码
val queue: Queue<string> = LinkedList()
queue.peek().toInt()
</string>

这种情况下,你使用了可能返回 null 值的 peek 方法。但是 Kotlin 编译器不会提示你这个问题,所以当你的 Queue 是空队列的的时候,可能会触发 NullPointerException 异常。

问题在于我们使用的 Queue 是 JDK 的一个接口,并且当你查看 peek 方法的文档时:

复制代码
/**
* Retrieves, but does not remove, the head of this queue,
* or returns {@code null} if this queue is empty.
*
* @return the head of this queue, or {@code null} if this queue is empty
*/
E peek();

文档中说 peek 方法会返回一个 E 类型的对象,但是 Kotlin 认为 E 是不可空的。在接下来的 Kotlin 版本中可能会解决这个问题,但是现在当你在你的工程中使用类似接口的时候,一定要注意:

复制代码
val queue: Queue<string> = LinkedList()
queue.peek()?.toInt()
</string>?>

内部 it

当一个 lambda 表达式只有一个参数的时候,你可以在你的代码中将其省略,并用 it 代替。

it:单参数的内部名称。当你表达式只有一个参数的时候,这是一个很有用的特性,声明的过程可以省略(就像 ->),并且参数名称为 it。

问题是,当你的代码中存在向下面例子一样的嵌套函数的时候:

复制代码
val list = listOf("foo.bar", "baz.qux")
list.forEach {
it.split(".").forEach {
println(it)
}
}

it 参数会混淆。解决方法就是像下面这样显示的声明:

复制代码
list.forEach { item ->
item.split(".").forEach { part ->
println(part)
}
}

看起来是不是好多了!

隐藏的复制

注意观察下面的类:

复制代码
data class Foo(val bars: MutableList<string>)
</string>

data 类提供了一系列的方法,并且你可以通过拷贝得到其镜像。猜猜下面的代码会输出什么?

复制代码
val bars = mutableListOf("foobar", "wombar")
val foo0 = Foo(bars)
val foo1 = foo0.copy()
bars.add("oops")
println(foo1.bars.joinToString())

控制台会输出 foobar, wombar, oops。问题出在 copy 方法并没有真正的复制一个完整的对象, 而是复制了对象的引用。当你忘记编写单元测试类,并且将你的 data 类按照不可变类来传递的时候,就可能出现这种问题。

解决方法就是当你使用 data 类的时候一定要多加小心,并且当你必须将其作为值对象的时候,像下面这样:

复制代码
data class Foo(val bars: List<string>)</string>

data 类还有一个问题:其 equals / hashCode 方法所用到的属性不可变。你只能通过手工重写这些方法的方式来修改返回值。谨记上面这一点。

内部方法暴露

仔细思考下面的例子:

复制代码
class MyApi {
fun operation0() {
}
internal fun hiddenOperation() {
}
}

当你在 Kotlin 的项目中引用这个类的时候,internal 关键字是生效的。但是当你从一个 Java 项目中使用的时候,hiddenOperation 就变成了一个公共方法!为了避免这种情况,我建议使用接口的方式来隐藏实现的细节:

复制代码
interface MyApi {
fun operation0()
}
class MyApiImpl: MyApi {
override fun operation0() {
}
internal fun hiddenOperation() {
}
}

特殊的全局扩展

毫无疑问,扩展函数的功能非常重要。但通常,能力越大责任越大。例如,你可以编写全局的 JDK 类扩展函数。但是当这个函数只在本地上下文中有意义,却是全局可见的时候,就会带来很多麻烦。

复制代码
fun String.extractCustomerName() : String {
// ...
}

每个跳转到你的方法的人都会不知所措。所以我认为在你编写这样的方法之前务必三思。下面就是一个建议:

复制代码
/**
* Returns an element of this [List] wrapped in an Optional
* which is empty if `idx` is out of bounds.
*/
fun <t> List<t>.getIfPresent(idx: Int) =
if (idx >= size) {
Optional.empty()
} else {
Optional.of(get(idx))
}
/**
* Negates `isPresent`.
*/
fun <t> Optional<t>.isNotPresent() = isPresent.not()
</t></t></t></t>

lambdas Unit 返回值 vs Java SAM 转换

如果你的函数参数是 lambdas 表达式,并且返回值类型是 Unit 的时候,你可以省略 return 关键字:

复制代码
fun consumeText(text: String, fn: (String) -> Unit) {
}
// usage
consumeText("foo") {
println(it)
}

这是一个很有趣的特性,但是当你在 Java 代码中调用该方法的时候会比较尴尬:

复制代码
consumeText("foo", (text) -> {
System.out.println(text);
return Unit.INSTANCE;
});

这对于 Java 端来说是不友好的,如果你想在 Java 中成功调用该方法,你需要定义如下接口:

复制代码
nterface StringConsumer {
fun consume(text: String)
}
fun consumeText(text: String, fn: StringConsumer) {
}

然后你就能使用 Java 的 SAM 转换。

复制代码
consumeText("foo", System.out::println);

但是在 Kotlin 这边看起来就很糟糕了:

复制代码
consumeText("foo", object: StringConsumer {
override fun consume(text: String) {
println(text)
}
})

问题关键点在于只有 Java 支持 SAM 转换,Kotlin 并不支持。我的建议是简单的场景中,只是用 Java 的 SAM 接口作为一个消费者:

复制代码
fun consumeText(text: String, fn: Consumer<string>) {
}
// usage from Kotlin
consumeText("foo", Consumer {
println(it)
})
// usage from Java
consumeText("foo", System.out::println);
</string>

Java 中使用不可变集合

Kotlin 提供了 JDK 集合类的不可变版本。

复制代码
fun createSet(): Set<string> = setOf("foo")
// ...
createSet().add("bar") // oops, compile error
</string>

这是一个很好的补充。但是当你在看 Java JDK 的 Set 类 API 的时候会发现:

复制代码
createSet().add("bar"); // UnsupportedOperationException

当你尝试修改这个 Set 的时候,就会抛出这个异常,就像你使用了 Collections.unmodifiableSet() 方法一样。我不知道这种情况是否合理,但是你在使用 Kotlin 不可变版本的 Java 集合类的时候,需要谨记这一点。

接口中没有重载

Kotlin 在接口上不支持使用 @JvmOverloads 注解,当然 override 也不行。

复制代码
interface Foo {
@JvmOverloads // OUCH!
fun bar(qux: String)
}
class FooImpl : Foo {
@JvmOverloads // YIKES!
override fun bar(qux: String) {
}
}

你只能像下面这样手动定义:

复制代码
interface Foo {
fun bar()
fun bar(qux: String)
}

要记住你可以使用 Kotlin 中的 KEEP (Kotlin Evolution and Enhancement Process) 来改善。KEEP 与 Java 中的 JEP 类似,但是与 JEP 相比要简洁许多。

总结

Kotlin 现下很流行,并且我也认为他是一个增强版的 Java。但是在使用 Kotlin 的时候你仍需要保持清醒,尤其是当你身处各种各样的关于 Kotlin 的宣传之中时。如果你要使用 Kotlin 的话,一定要注意我们在上面提到的 Kotlin 相关的缺陷。

最后我还是想说,上述提到的问题都比较容易解决,并且不会对语言的使用方面带来本质性的伤害。

也感谢您的阅读!可以在我的 Blog 上读到更多的文章。

原文链接: Kotlin pitfalls and how to avoid them


感谢覃云对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2017-10-18 19:007932

评论

发布
暂无评论
发现更多内容

react源码解析11.生命周期调用顺序

buchila11

React React Hooks

有人问你什么是CAP?你就把这篇文章发给他

李子捌

redis CAP理论 28天写作 12月日更

面试官:重写 equals 时为什么一定要重写 hashCode?

王磊

Prometheus Exporter (十四)MySQL Server Exporter

耳东@Erdong

MySQL Prometheus 28天写作 12月日更

架构训练营 Week1 作业

红莲疾风

「架构实战营」

react源码解析12.状态更新流程

buchila11

React React Hooks

模块五作业

panxiaochun

架构实战营

架构训练营 - 模块五作业

VegetableBird

架构实战营 架构师实战营 「架构实战营」

微信小程序开发:新建文件报错Error:pages/xxx/xxx.json Expecting ’STRING’,’NUMBER’,’NULL’,’TRUE’

三掌柜

28天写作 12月日更

Java本地事务失效了,线上应该如何排查?

JavaEdge

Spring Framework 签约计划第二季

模块五课后作业-设计微博系统中评论架构

断水风春

架构实战营

AOP+MybatisPlus 优化特殊的日志模块

4ye

Java spring 程序员 后端 签约计划第二季

Git进阶(五):git 分支管理策略

No Silver Bullet

git 学习 12月日更

模块五作业

小鹿

自定义规则删除过期文件(linux)

liuzhen007

28天写作 12月日更

生产环境的线程池出问题了,我到底该如何正确使用线程池?

JavaEdge

Java java 并发 签约计划第二季

数据也需要滴血认亲?

Justin

大数据 数据治理 28天写作

从deadline和被狗追说起(2/28)

赵新龙

28天写作

服务器宕机了,除了坐等,我还能做点什么?

JavaEdge

JVM 签约计划第二季

大厂算法面试之leetcode精讲17.栈

全栈潇晨

LeetCode 算法面试

听说过python协程没?听说过 asyncio 库没?都在这一篇博客了

梦想橡皮擦

12月日更

Rust 元宇宙 12 —— 序列化和存储

Miracle

rust 元宇宙

1.《重学JAVA》开篇

杨鹏Geek

Java Java 25 周年 28天写作 12月日更

架构实战营第五课作业微博评论高性能高可用架构

Geek_99eefd

架构实战营 「架构实战营」

大厂算法面试之leetcode精讲18.队列

全栈潇晨

算法 LeetCode

DDD 领域驱动设计落地实践系列:微服务拆分之道

慕枫技术笔记

后端 签约计划第二季

我的应用 OOM 了,我该做点什么?

JavaEdge

JVM 签约计划第二季

我的Spring AOP没生效,我该如何排查?

JavaEdge

Spring Framework 签约计划第二季

博客?Newsletter?付费订阅?Papyrus 都帮你搞定!

遇见

程序员 博客 Blog newsletter

微博评论的高性能高可用架构设计

zjluoyue

在线火星文转换器工具

入门小站

工具

Kotlin 的陷阱以及如何避免_Android_Adam Arold_InfoQ精选文章