如何避免Kotlin里的陷阱?

最近 Kotlin 特别流行,并且我也赞同 Kotlin 是一个经过深思熟虑后被设计出的语言,除了下面提到的缺点之外。我会在本文向你分析一些我在开发过程中遇到的陷阱,并且教你如何避免他们。

谜一样的 null

在 Kotlin 当中,你可以不用考虑在你的代码中如何处理 null 的问题,这会让你忘记 null 是无处不在的这个说法,只不过被隐藏了起来。看看下面这个表面看起来没有问题的类:


  1. class Foo { 
  2.     private val c: String 
  3.     init { 
  4.         bar() 
  5.         c = "" 
  6.     } 
  7.     private fun bar() { 
  8.         println(c.length) 
  9.     } 

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

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

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

来自 JDK 的 null

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

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


  1. val map = ConcurrentHashMap<String, String>() 
  2. map["foo"] = "bar" 
  3. val bar: String = map["foo"]!! 

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

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

考虑如下例子:


  1. val queue: Queue<String> = LinkedList() 
  2. queue.peek().toInt() 

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

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


  1. /** 
  2.   * Retrieves, but does not remove, the head of this queue, 
  3.   * or returns {@code null} if this queue is empty. 
  4.   * 
  5.   * @return the head of this queue, or {@code null} if this queue is empty 
  6.   */ 
  7.   E peek(); 

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


  1. val queue: Queue<String?> = LinkedList() 
  2. queue.peek()?.toInt() 

内部 it

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

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

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


  1. val list = listOf("foo.bar", "baz.qux") 
  2. list.forEach { 
  3.     it.split(".").forEach { 
  4.         println(it) 
  5.     } 

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


  1. list.forEach { item -> 
  2.     item.split(".").forEach { part -> 
  3.         println(part) 
  4.     } 

看起来是不是好多了!

隐藏的复制

注意观察下面的类:


  1. data class Foo(val bars: MutableList) 

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


  1. val bars = mutableListOf("foobar", "wombar") 
  2. val foo0 = Foo(bars) 
  3. val foo1 = foo0.copy() 
  4. bars.add("oops") 
  5. println(foo1.bars.joinToString()) 

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

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


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

内部方法暴露

仔细思考下面的例子:


  1. class MyApi { 
  2.     fun operation0() { 
  3.     } 
  4.     internal fun hiddenOperation() {             
  5.     } 

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


  1. interface MyApi { 
  2.     fun operation0() 
  3. class MyApiImpl: MyApi { 
  4.     override fun operation0() { 
  5.     } 
  6.     internal fun hiddenOperation() { 
  7.     } 

特殊的全局扩展

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


  1. fun String.extractCustomerName() : String { 
  2.     // ... 

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


  1. /** 
  2.  * Returns an element of this [List] wrapped in an Optional 
  3.  * which is empty if `idx` is out of bounds. 
  4.  */ 
  5. fun <T> List<T>.getIfPresent(idx: Int) = 
  6.         if (idx >= size) { 
  7.             Optional.empty() 
  8.         } else { 
  9.             Optional.of(get(idx)) 
  10.         } 
  11. /** 
  12.  * Negates `isPresent`. 
  13.  */ 
  14. fun <T> Optional<T>.isNotPresent() = isPresent.not() 

lambdas Unit 返回值 vs Java SAM 转换

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


  1. fun consumeText(text: String, fn: (String) -> Unit) { 
  2. // usage 
  3. consumeText("foo") { 
  4.     println(it) 

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


  1. consumeText("foo", (text) -> { 
  2.     System.out.println(text); 
  3.     return Unit.INSTANCE; 
  4. }); 

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


  1. nterface StringConsumer { 
  2.     fun consume(text: String) 
  3. fun consumeText(text: String, fn: StringConsumer) { 

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


  1. consumeText("foo", System.out::println); 

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


  1. consumeText("foo", object: StringConsumer { 
  2.     override fun consume(text: String) { 
  3.         println(text) 
  4.     } 
  5. }) 

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


  1. fun consumeText(text: String, fn: Consumer<String>) { 
  2. // usage from Kotlin 
  3. consumeText("foo", Consumer { 
  4.     println(it) 
  5. }) 
  6. // usage from Java 
  7. consumeText("foo", System.out::println); 

Java 中使用不可变集合

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


  1. fun createSet(): Set<String> = setOf("foo") 
  2. // ... 
  3. createSet().add("bar") // oops, compile error 

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


  1. createSet().add("bar"); // UnsupportedOperationException 

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

接口中没有重载

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


  1. interface Foo { 
  2.     @JvmOverloads // OUCH! 
  3.     fun bar(qux: String) 
  4. class FooImpl : Foo { 
  5.  
  6.     @JvmOverloads // YIKES! 
  7.     override fun bar(qux: String) { 
  8.     } 

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


  1. interface Foo { 
  2.     fun bar() 
  3.     fun bar(qux: String) 

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

总结

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

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

本文作者:佚名

来源:51CTO

时间: 2024-12-24 20:50:51

如何避免Kotlin里的陷阱?的相关文章

用Kotlin写一个基于Spring Boot的RESTful服务

Spring太复杂了,配置这个东西简直就是浪费生命.尤其在没有什么并发压力,随便搞一个RESTful服务 让整个业务跑起来先的情况下,更是么有必要纠结在一堆的XML配置上.显然这么想的人是很多的,于是就 有了Spring Boot.又由于Java 8太墨迹于是有了Kotlin. 数据源使用MySql.通过Spring Boot这个基本不怎么配置的,不怎么微的微框架的Spring Data JPA和Hibernate 来访问数据. 处理依赖 这里使用Gradle来处理依赖. 首先下载官网给的初始项

使用Kotlin&amp;Anko, 扔掉XML开发Android应用

尝鲜使用Kotlin写了一段时间Android.说大幅度的减少了Java代码一点不夸张.用Java的时候动不动就new一个OnClickListener()匿名类,动不动就类型转换的地方都可以省下很多.更不用说特殊的地方使用data class更是少些不知道多少代码. Jetbrains给Android带来的不仅是Kotlin,还有Anko.从Anko的官方说明来看这是一个雄心勃勃的要代替XML写Layout的新的开发方式.Anko最重要的一点是引入了DSL(Domain Specific La

谷歌大牛说:为什么 Kotlin 比你们用的那些垃圾语言都好

为什么说 Kotlin 比你们用的那些垃圾语言都好 说真的,其实我不想去攻击你们的语言信仰--至少不会"大大地"想.毕竟你喜欢的语言大放异彩的时期,可能要追溯到冰川时代了,对不?如果你喜欢的语言到今天还没有死,那只能说明这门语言在逐渐地改进和更新,保持与时俱进. 但改进的速度呢?好吧--假设你现在用的语言碰巧是 Java,并且你也沉溺于 Java 曾经是一门非常优秀的语言的想法,那么你就完蛋了.而且是早就完蛋了.尽管人类都不太喜欢思考终极命运问题,但是相比于在 20 多年前刚刚问世,J

为什么你该摒弃 Java ,全面转向 Kotlin 语言?

我想告诉你一个名为 Kotlin 的新的编程语言,以及为什么你要开始考虑使用它来开发你的下一个项目.我以前喜欢 Java ,但是去年我发现了 Kotlin ,只要有可能我就会用 Kotlin 来写代码.现在我实在无法想象有什么地方只有 Java 能做,而 Kotlin 不能的. Kotlin 是 JetBrains 开发的,这是一家开发了一整套 IDEs 的公司,诸如 IntelliJ 和 ReSharper, 还有正在闪耀光芒的 Kotlin.这是一个务实而且简洁的编程语言,真正让人感觉身心愉

Getting started with Kotlin on Android

2017年3月26日,ThoughtWorks高级咨询师张帅.王智勇在"Mobile Open Day-小步构建移动开发知识网络"进行<Getting started with Kotlin on Android>演讲分享.IT大咖说作为独家视频合作方,经主办方和讲者审阅授权发布. 嘉宾分享视频地址:http://t.cn/RKwZwbZ Java VS Kotlin 在Java的使用中会遇到很多问题.它的语法繁琐,API低级:随时可能出现null pointer问题:有各

Kotlin, Android的Swift

苹果已经用Swift代替Objective-C,一种古老的语言,来进行iOS的开发了.明显Android开发也有这个趋势. 虽然现在已经可以选择Scala或者Groovy等基于JVM的语言开发Android应用来尝尝鲜,但是弊端却显而易见. 要引入一个全新的开发语言,那么就意味着需要引入这个语言的全部的运行时.这简直就是噩梦.因为这会给 极大的增加应用包的大小,还不说65535方法问题.小的应用还可以,但是这不是一个合适的替代语言应该有的问题. Kotlin 介绍一下Kotlin--一个基于JV

金山邀百万手机用户揭秘手机安全陷阱

本报讯 近日,中央电视台3·15晚会对一些手机存在"吸费"软件的现象进行了 曝光,诸多的山寨手机以及少数国内品牌手机,在手机中内置"后门",用户在不知不觉中被扣除话费.而扣除的方式,便是在手机中内置"收费"代码,每次点击便是扣费短信,对用户进行扣费.手机"吸费"软件问题引发了 网友的极大关注. 为了最大程度的维护手机消费者权益,国内知名网络安全厂商金山安全宣布即日起开展"你的手机里有扣费软件吗?全民曝光手机安全陷阱大

Android自定义视图二:如何绘制内容

这个系列是老外写的,干货!翻译出来一起学习.如有不妥,不吝赐教! Android自定义视图一:扩展现有的视图,添加新的XML属性 Android自定义视图二:如何绘制内容 Android自定义视图三:给自定义视图添加"流畅"的动画 Android自定义视图四:定制onMeasure强制显示为方形 有的时候自持扩展一个标准的Android视图是不够的.你需要在视图上绘制你自己的内容才行.本文将会讲述如何使用Canvas类来绘制一个折线图,并会讲述如何处理尺寸和padding. 如果你还没

BP怎么写才能吸引投资人注意?

摘要: 1.BP怎么写才能吸引投资人注意? 2.去哪找懂我的投资人? 3.都说好,为什么就是不投我? 4.凭啥说我的项目就值这么点钱? 5.TS 长啥样儿? 6.TS 怎么看?签还是不签? 7.怎样 1.BP怎么写才能吸引投资人注意? 2.去哪找懂我的投资人? 3.都说好,为什么就是不投我? 4.凭啥说我的项目就值这么点钱? 5.TS 长啥样儿? 6.TS 怎么看?签还是不签? 7.怎样识别投资人藏在协议里的"陷阱"? 8.多久钱才能真正到账? 9.VIE结构是什么? 10.谈判技巧很