Kotlin 开篇

这篇文章是为「码上开学」写的,首发于码上开学官网

引子

找到那把三叉戟,你便能号令整个海洋。 ——《海王》

为什么需要新语言

Java是当今世界最流行的工业级语言,有着非常成熟的生态和广泛的开发群体。当初Android选择Java作为开发语言,也是为了吸引Java程序员这个世界上最大的开发群体。最早的一批Android程序员可能已经用Java写了近十年Android程序了,各种实践方法也比较成熟了。那么今天我们为什么还需要一门新语言呢?这就要从Java的发行历史说起了。

Java历史

Java版本 发布日期 重要功能
JDK 1.0 Jan 1996 Initial release
JDK 1.1 Feb 1997 Reflection, JDBC, Inner Classes
J2SE 1.2 Dec 1998 Collection, JIT
J2SE 1.3 May 2000 HotSpot JVM, JNDI
J2SE 1.4 Feb 2002 assert, Regular, NIO
J2SE 5.0 Sep 2004 Generics, Annotations, Autoboxing/unboxing, Concurrency
Java SE 6 Dec 2006 JDBC 4.0, Java Compiler API, New GC
Java SE 7 July 2011 Strings in switch, Diamond operator, Resource management
Java SE 8 Mar 2014 Lambda, Functional interface, Optionals, New Date and Time API
Java SE 9 Sep 2017 Multi-gigabytes heaps, Java Module System

其实从Java 5开始,Java就是一门很完备的工业级语言了,生态也非常成熟,各种框架层出不穷。但是后续的Java 6和Java 7只是在Java 5基础上添加了一些功能和语法糖,根本算不上大版本更新,我觉得版本号定为5.1,5.2可能更合适。如果你稍微了解过同期的C#,看看Lambda和LINQ,你大概也会对Java怒其不争吧。

直到2014年Java 8正式发布,才算是Java语言的一次大更新,加入了呼声很高的Lambda和stream。可是等等,Android是哪一年发布的?2008年!所以Android是以Java 6来进行开发。虽然从Android Studio 3.0开始可以使用部分Java 8特性进行开发了,但是非常好用的stream只能在Android 7.0以上使用(即minSdkVersion = 24)。受困于Android版本碎片化,我们只能放弃。那么用Java 6写代码到底有什么问题呢?

从一道算法题说起

请听题:有一个英文小写单词列表List\<String>,要求将其按首字母分组(key为 ‘a’ - ‘z’),并且每个分组内的单词列表都是按升序排序,得到一个Map\<Character, List\<String>>。请尝试用10行以内Java 6.0代码完成。

1
2
3
4
5
6
7
8
9
10
11
12
List<String> keywords = ...;
Map<Character, List<String>> result = new HashMap<>();
for (String k: keywords) {
char firstChar = k.charAt(0);
if (!result.contains(firstChar)) {
result.put(firstChar, new ArrayList<String>());
}
result.get(firstChar).add(k);
}
for (List<String> list: result.values()) {
Collections.sort(list);
}

实际上已经超过10行了。我们再看看业界标杆C#是怎么写的

1
2
3
4
5
6
List<string> keywords = …;
var result = keywords
.GroupBy(k => k[0])
.ToDictionary(
g => g.Key,
g => g.OrderBy(k => k).ToList());

为了代码可读性,我添加了一些换行。如果你愿意的话,写成一行也行。可以明显看出,对比当今先进语言,Java 6已经无法让人愉快的写代码。用Java 6写代码时,我们脑子里想的不是要做什么,而是怎么做

Java平台

我们平时说Java,其实包含了两个不同的概念:一是Java语言本身,二是Java虚拟机,即JVM。虽然上面吐槽了Java语言本身的历史包袱,但是JVM还是非常优秀的,它有非常多的优点:

  • 跨平台
  • 自动垃圾回收
  • 运行速度快(你没看错。虽然Java程序速度没有C/C++快,但是在所有编程语言中Java属于速度快的那一拨。大数据框架Hadoop便是用Java开发,能顺利处理海量数据。)

我们知道Java代码编译后会生成字节码,然后字节码在JVM中运行注意Android中的虚拟机并不是JVM,而是Dalvik/ART,但也是编译成字节码。那么有没有可能新创造一门语言,编译的时候也生成字节码,然后在JVM中运行呢?这样既可以摆脱Java的历史包袱,又能享受到JVM和成熟Java框架的各种好处!答案当然是肯定的!实际上Java平台已经衍生出ScalaClojureGroovy等比较流行的语言了。而今天我们要讲的Kotlin则是Java平台中的新贵,出自大名鼎鼎的JetBrains公司。打开他们的官网你就会发现,很多著名的IDE(比如IntelliJRubyMineWebStorm)都是出自这家公司。IntelliJ是当今最主流的Java IDE,JetBrains公司在开发IntelliJ的过程中积累的经验令Kotlin的诞生显得水到渠成。而Google在2017年的I/O大会上宣布Kotlin成为Android开发的官方编程语言后,更是令Kotlin一夜之间成为最受瞩目的编程语言之一。那么我们来看看Kotlin会给我们带来哪些好处吧。

Kotlin带来的好处

Data Class

假设你在做一个金融交易系统,需要定义一个Class来表示每一笔支付,包含金额和币种。这里有一个简单的例子:

1
2
3
4
public class Purchase {
public String currency;
public int price; //为了便于演示,这里将价格设为整数类型
}

看起来不错。但是作为一个经验丰富的程序员,你一眼就发现了潜藏的问题。一个金融系统,一定是要保证每笔交易的正确性的,你肯定不希望object中的currency和price的值在某个模块里面被粗心的人修改了。所以你需要一个Immutable Class,即不可更改的类。于是你做了如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
public class Purchase {
private String currency;
private int price;

public Purchase(String currency, int price) {
this.currency = currency;
this.price = price;
}

public String getCurrency() { return this.currency; }
public int getPrice() { return this.price; }
}

你将currency和price定义为private field,通过构造方法来赋值,并且只暴露get方法。这样就不用担心数据被其他人误修改了。完美!可是等等,这就够了吗?熟读《Java编程思想》的你立刻想起,一个完备的Class还需要重写equals()hashCode()toString()方法!于是你又添加了一些代码:

1
2
3
4
5
6
7
public class Purchase {
...

public boolean equals(Object o) {}
public hashCode() {}
public String toString() {}
}

我就问你烦不烦:) 仅仅是表示金额和币种,怎么要写那么多代码?如果有十几个字段,那还不得上百行代码呀?!请看Kotlin中优雅的实现:

1
data class Purchase(val currency: String, val price: int)

没了。

一行代码搞定:)

关键字val会自动为currency和price创建不可更改的field,而data则会帮我们自动生成equals()、hashCode()和toString()方法!这还不是全部,看下面的代码:

1
2
val iPhone8 = Purchase(“CNY”, 5888)
val iPhoneX = iPhone8.copy(price = 8888)

看到copy方法了吗?是不是巨好用?这也是data class为我们自动生成的,方便吧:)

Extension Functions

Kotlin中的Extension类似于Objective-C中的Category,可以帮助我们扩展已有的Class,而无需继承这个Class,无论我们能不能访问源码。这对于系统Class以及一些第三方library中的Class特别有帮助。

随着项目规模的扩大,你的代码里肯定少不了一系列Utils类(比如FileUtils.javaDateUtils.java)来封装一些繁琐但常用的功能。比如在Android开发中,如果我们要动态的调整一个View的宽高,必须要先将dp转为px,所以我们会有这样一个方法:

1
2
3
4
5
public class ScreenUtil {
public static int dip2px(float dipValue) {
return (int) (dipValue * density + 0.5f);
}
}

然后这样使用:

1
layoutParams.width = ScreenUtil.dip2px(16F)

这样看起来没什么不对,只是不太符合人的阅读习惯。看看用Extension能帮我们做些什么:

1
2
3
fun Int.toPx(): Int {
return ScreenUtil.dip2px(this.toFloat())
}

我们给整数类型添加了一个Extension Function,叫做toPx,用来实现dp到px的转换。然后优雅的调用:

1
layoutParams.width = 16.toPx()

可读性是不是好多了🙂。你可能会想,Extension一个一个自己写也挺麻烦的,有没有大神把常用的Extension Functions封装成一个library啊?有的,Google爸爸已经帮我们考虑到了😘。请参看Android KTX项目。这个项目是Android Jetpack的一部分,而Jetpack也是我们接下来要分享的内容。

Null Safety

这是一个值得吹上三天三夜的革新。在Quora上有这样一个问题:为什么空指针异常被称为10亿美金的错误?相信每一个Java程序员都对这个问题深有体会。看看我们为了避免程序崩溃,不得不写多么丑陋的代码:

1
2
3
if (a != null && a.b != null && a.b.c != null) {
println(a.b.c.toString());
}

这样写有两个问题,一是代码很丑陋,二是本少爷很容易忘记做空指针检查啊😤!!!Java 8引入了Optional来解决这个问题,比自己检查空指针要稍微优雅一点,但仍然有许多不必要的代码包装。

Kotlin的类型系统从一开始就致力于避免空指针异常。我们在Kotlin中定义变量时可以指定它为可空类型(Nullable Type)和不可空类型(Non-Null Type)。

1
2
3
4
5
6
7
8
var a: String = "abc" // 不可空
a = null // 编译报错
print(a.length) // 没问题

var b: String? = "abc" // 可空
b = null // 没问题
print(b.length) // 编译报错
print(b?.length) // 没问题

上例的a是Non-Null,而b是Nullable。他们的唯一区别是定义的时候b的String后面加了个问号?,代表它是Nullable的。只有Nullable类型可以赋值为null。调用Nullable对象的方法时,需要加一个问号,像b?.length。如果b为null,那么print(b?.length)这行代码不会被执行。这样设计有什么好处呢?回到我们上一个例子,如果我们将aa.ba.b.c都定义为Nullable,那么就可以这样写:

1
println(a?.b?.c?.toString())

如果它们中间任意一个为null,那么 a?.b?.c?.toString() 这个 chain 就会返回 null 。是不是很强大很方便?理论上讲,只要我们的类型定义合理,那么90%的空指针异常都是能避免。为什么我不敢说100%?因为有时候我们看设计文档确定某个值绝对不可能为null,于是给它定义为Non-Null,结果程序运行时偏偏就传过来一个null……😂

Courtines

Android开发中多线程处理一直是一个难点,稍微不小心就容易出问题。你可能已经学习过RxJava,并在项目中成功使用RxJava来处理线程问题。这非常好。但是如果你的业务逻辑并没有复杂到必须用RxJava来解决,你应该看看Kotlin中的Courtines。Courtines通常翻译成_协程,在Lua等程序语言中已经有着广泛的应用。它的概念稍微有些复杂,我们可以暂时认为它是一种无需锁_并且没有线程切换开销的轻量级线程。

我们看一个例子,假设我需要从网络取回来一些数据,然后保存到数据库,那么传统的异步回调写法是这样的:

1
2
3
4
5
networkRequest { result ->
databaseSave(result) { rows ->
// Result saved
}
}

而用Courtines的写法是这样的:

1
2
3
val result = networkRequest()
databaseSave(result)
// Result saved

你可能已经发现了,这段代码根本不关心线程如何切换,只关心我到底要实现什么功能。实际上Courtines背后远比这要复杂,想要熟练使用需要经历一些学习曲线,但好在曲线并不算陡峭。我们后面会有专门文章来讲解如合使用Courtines。如果你已经激动的等不及了(和第一次知道Courtines时的我一样),可以先跟着Google Codelabs中的教程来练练手。

与Java的交互

虽然前面我们讲到,Java平台上的语言都会编译成字节码,但这并不代表所有语言都能与Java无缝交互。比如Scala和Java的交互就非常复杂。幸运的是,JetBrains从一开始就将与Java的交互性作为Kotlin的设计目标之一。无论是从Java调用Kotlin,还是从Kotlin调用Java,都非常自然,没有什么障碍。这意味着我们可以任意使用Java丰富的第三方库,也可以在现有的Java工程基础上用Kotlin添加新的功能。万一你遇到某些特殊情况,有一段逻辑用Kotlin搞不定(我写这篇文章时就遇到一个),可以把这段逻辑抽出来用Java写。有Java兜底,我们就能放心的为项目引入Kotlin了。

无障碍升级

我们前面讲到,要想在Android开发中使用Java 8中的stream,需要设定minSdkVersion = 24。如果将来要使用Java 9的新特性,恐怕又得等Android版本更新了。Kotlin则不同,你可以把它当作一个集成到项目中的第三方library,可以随时升级到最新版本!这意味着Kotlin将来推出的更多新特性都能应用到所有Android项目中!

迁移到Kotlin会遇到什么问题

前面讲了Kotlin的这么多好处,但要知道世上没有十全十美的语言。就Kotlin而言,目前社区反映比较多的问题是可见性修饰符(Visibility Modifiers)。我们知道Java中有四种访问权限:publicprotectedprivatepackage-private。其中package-private是指在同一个包名下可见,在library开发中非常方便。而在Kotlin中没有了package-private,取而代之的是internal,即在同一个模块(Module)内可见。这样我们在设计library时必须对可见性控制有更周全的考虑。为Android开发library,不可避免的要重写系统方法。如果你用重写了一个Java中的package-private方法,那么不好意思,这个方法会变成public🤣,原本并不想暴露出来的方法暴露了……

如果你主要做应用开发,那么目前已经没有什么坑了。Google已经逐渐用Kotlin重写Android文档中的所有例子,Github上用Kotlin开发的项目也在飞快增长。社区方面,在StackOverflow做的2018年度调查中,Kotlin更是一举登上最受欢迎语言榜第二名!就像前面讲的,万一有问题还有Java兜底。所以你唯一需要考虑的可能就是团队学习成本。好在Kotlin是一门非常简易、现代的语言,相信做这个决定并不困难。

回到最开始

我想肯定有好事的人要问,最开始那个算法题用Kotlin怎么写呢?

1
2
3
4
val keywords = arrayOf("apple", "app", "alpha", ...)
val result = keywords
.groupBy { it[0] }
.mapValues { it.value.sorted() }

你还在等什么?