Kotlin最佳工程实践
写在前面
自从Kotlin被官宣为Android开发正式语言,这门语言也越来越流行。相信大家也对Kotlin这门语言有过了解或者学习,例如类型推断、空安全、lambda表达式、高阶函数,甚至Kotlin协程之类的。但听了很多大道理,还是过不好这一生;对很多java程序员来说,学了很多kotlin知识,但真到自己写的时候还是无从下手,写法还是照搬java那一套。
本文就是为了解决这个问题,希望提供在工程实践具体场景下如何去有效的利用kotlin的语言特性,接受kotlin的编程思想,来提高开发效率和优化代码结构。此外,本文中提到的工程实践限定在Java和Kotlin混合工程,对于纯Kotlin工程下一些最优实践不作讨论。最后,本文目标受众是学习过Kotlin的Java程序员,很多东西会建立在Java和kotlin语言基础上讨论,不会介绍kotlin语言的基础知识。
主要场景
如何实现懒汉式单例模式
众所周知,Kotlin中不在提供static关键字,而是提供了object关键字,可以更加方便的去声明一个对象,而Java中的单例模式也可以简化成一句简单的声明,示例代码:
object SimpleSington {
fun test() {}
}
//在Kotlin里调用
SimpleSington.test()
//在Java中调用
SimpleSington.INSTANCE.test();
这种声明单例的方法,等价于如下java代码:
public final class SimpleSington {
public static final SimpleSington INSTANCE;
private SimpleSington() {
}
static {
INSTANCE = new SimpleSington();
}
}
而这是饿汉式的实现,有可能造成一定的性能浪费。如果对于性能开销没有特别严格的要求,这个也满足日常开发,但如果因为业务、性能要求需要实现一个懒汉式的加载,就需要改进我们的写法。这里可以用上kotlin的另一个语言特性:lazy。具体语法的介绍不是本文重点,如果需要了解可以查看:Kotlin的lateinit和lazy,下面是示例代码:
class LazySingleton private constructor() {
companion object {
val instance: LazySingleton by lazy { LazySingleton() }
}
}
这个写法利用lazy关键字,非常简单的实现了一个懒汉式的单例模式。那么这段代码等效于哪种java的写法;以及lazy的更高级用法,包括3中LazyThreadSafetyMode的含义,读者感兴趣可以阅读上面的链接并自己探究。
使用顶层函数代替静态工具类
Java中static另一个典型应用场景,就是静态工具类,而在Kotlin中,我们应该如何去写一些工具类?对于Kotlin对static的替代,第一反应是object或者companion object,那是不是应该写成如下这样?
// Don't
object StringUtil {
fun countAmountOfX(string: String): Int{
return string.length - string.replace("x", "").length
}
}
StringUtil.countAmountOfX("xFunxWithxKotlinx")
但这样的写法我们并不推荐,这里应该用Kotlin支持的顶层函数,写法如下:
// Do
fun countAmountOfX(str: String): Int {
return str.length - str.replace("x", "").length
}
countAmountOfX("xFunxWithxKotlinx")
使用扩展函数代替工具方法
其实上面的代码还可以进一步优化,kotlin中可以使用扩展函数来扩展一个类的方法,这种写法运用在代码中,最终结果就成了这样:
// Do
fun String.countAmountOfX(): Int {
return length - replace("x", "").length
}
"xFunxWithxKotlinx".countAmountOfX()
这样,这个函数就变成了像String自带的函数一样,用起来非常自然;但在Kotlin/Java混合工程中,这样用了顶层函数和扩展函数语言特性的方法,要如何在Java里中使用呢?这就需要加上一个注解,示例代码如下:
@file:JvmName("StringUtil")
package com.liye.utils
// In Kotlin, remove the unnecessary wrapping util class and use top-level functions instead
// Often, you can additionally use extension functions, which increases readability ("like a story").
fun String.countAmountOfX(): Int {
return length - replace("x", "").length
}
// Java code
StringUtil.countAmountOfX("xFunxWithxKotlinx")
这里面涉及到两个知识点:1. 通过@file:JvmName注解可以让java中使用到kotlin的顶层函数;2. kotlin中的扩展函数在java中调用时,会把扩展对象作为第一个参数传入;
此外,根据Kotlin编码规约的推荐,应该把单行表达式的函数使用=来简化,同时省去了返回值声明,代码如下:
// Do
fun String.countAmountOfX() = length - replace("x", "").length
对于Kotlin的扩展函数,还需了解的是Kotlin的标准库中利用扩展函数提供了大量基于Java类的扩展,包括:String、Collection、Array等,这些都可以在实践中帮助我们更方便的编码;而且Kotlin开发中也推荐尽量使用这些方法,替代掉之前很多我们自己写的各种工具类,例如:
// Do
if (str.isNullOrEmpty()) {
// ....
}
arrayOf(1, 2, 3, 4)
使用默认字段代替重载
Kotlin中的函数支持默认参数,这在代码声明时可以替代掉原来需要靠重载编写的大量代码,示例如下:
// Don't
fun foo() = foo("a")
fun foo(a: String) { /*……*/ }
// Do
fun foo(a: String = "a") { /*……*/ }
但是Java并不支持默认参数,所以在混合工程中,Kotlin中使用了默认参数的方法必须使用@JvmOverloads注解来支持java调用:
// Do
@JvmOverloads
fun foo(a: String = "a") { /*……*/ }
使用命名字段来增加可读性
Kotlin支持在方法调用时,使用命名参数,示例如下:
val config2 = SearchConfig2(
root = "~/folder",
term = "game of thrones",
recursive = true,
followSymlinks = true
)
可以发现,命名字段结合默认字段,有点类似Java中使用了Builder模式或者流式调用的代码,如下:
// Don't
val config = SearchConfig()
.setRoot("~/folder")
.setTerm("game of thrones")
.setRecursive(true)
.setFollowSymlinks(true)
上述代码是不推荐的,在Kotlin中,合理使用命名字段和默认字段,可以省去大量的这样的set方法;
使用lambda替代callback
在java中,经常需要使用callback接口来实现回调、观察者模式等逻辑,在kotlin中更推荐使用lambda来替代callbcak,省去callback接口的声明与定义;
// Don't
interface GreeterCallback {
fun greetName(name: String): Unit
}
fun sayHi(callback: GreeterCallback) = /* … */
// Do
fun sayHi(callback: (String) -> Unit) = /* … */
// Caller
greeter.sayHi { Log.d("Greeting", "Hello, $it!") }
其中关于lambda的使用以及简化可以参考文章:细说 Kotlin 的 Lambda 表达式
合理使用Kotlin的内置函数(let、apply等)
提到Kotlin的lambda,不得不提kotlin中内置了几个以lambda作为参数传入的内置函数,这几个函数结合kotlin的语言特性,可以大大简化我们的代码,下面给出几个工程实践中的例子。
替代if-null检测
在java中为了运行时避免NPE,我们经常需要做非空检测;而Kotlin一个重要的语言特性就是空安全,将非空检测作为了语言机制的一部分,那么如何在kotlin中优雅的去使用一些nullable对象呢?比如下面这样的代码:
// Java
if (data != null && data.publishTime != null && viewHolder != null && viewHolder.commentTimeTxt != null) {
viewHolder.commentTimeTxt.text = DateUtil.getRecommentDate(data.publishTime/1000)
}
在kotlin中就不需要再写这么多if了,直接使用?.结合let即可搞定:
// Kotlin
data?.publishTime?.let {
viewHolder?.commentTimeTxt?.text = DateUtil.getRecommentDate(it/1000)
}
替代if-type检测
了解Kotlin的基础语法,就会知道kotlin中提供了is和as关键字来实现类似Java中经常会使用的instanceOf和类型转换。但很多时候,结合内置函数,我们应该更进一步简化我们的代码,例如:
// Java
for (Animal animal : animals) {
if (animal instanceOf Dog) {
((Dog) animal).bark();
}
}
转成Kotlin的话,可以写成如下这样:
// kotlin
animals.forEach {
if (it is Dog) {
it.bark()
}
}
但其实,还可以通过使用as?关键字进一步简化成如下代码:
// kotlin
animals.forEach {
(it as? Dog)?.bark()
}
整合对象初始化代码
//Don't
val dataSource = BasicDataSource()
dataSource.driverClassName = "com.mysql.jdbc.Driver"
dataSource.url = "jdbc:mysql://domain:3309/db"
dataSource.username = "username"
dataSource.password = "password"
dataSource.maxTotal = 40
dataSource.maxIdle = 40
dataSource.minIdle = 4
这种Java中的常见写法,在kotlin中就不推荐了,可以用apply来大大简化:
//Do
val dataSource = BasicDataSource().apply {
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://domain:3309/db"
username = "username"
password = "password"
maxTotal = 40
maxIdle = 40
minIdle = 4
}
结语
基于Kotlin的新语法和编程思想,还有很多的工程实践可以探讨如何从原来Java的写法之上演进,这里探讨的只是很少一部分,更不用说还有类似Kotlin-reflect,kotlin-coroutine, RxKotlin这样的库,以及越来越多的支持库。相信Kotlin作为新一代的安卓官方语言,会越来越摆脱Java的桎梏,发展的越来越好,也希望大家可以尽快应用到自己的工程中。最后,欢迎讨论与指正!
参考文献
Kotlin的lateinit和lazy
Kotlin-Java interop guide
Kotlin习惯用法
Idiomatic Kotlin. Best Practices.
简述Kotlin中let, apply, run, with的区别
细说 Kotlin 的 Lambda 表达式