(本站刚上线有些不稳定,文中代码偶尔会出现错乱窜行,刷新可以解决。我会尽快优化网站,同时尽快处理代码高亮问题。#脸红)

引子

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

为什么需要新语言

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

Java 历史

Java 历史

其实从 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\,要求将其按首字母分组(key 为 ‘a’ - ‘z’),并且每个分组内的单词列表都是按升序排序,得到一个 Map\>。请尝试用 10 行以内 Java 6.0 代码完成。

(本站刚上线有些不稳定,文中代码偶尔会出现错乱窜行,刷新可以解决。我会尽快优化网站,同时尽快处理代码高亮问题。#脸红)

List<String> keywords = ...;  
Map<Character, List<String>> result = new HashMap<>();  
for (String k: keywords) {  
    char firstChar = k.charAt(0);
    if (!result.containsKey(firstChar)) {
        result.put(firstChar, new ArrayList<String>());
    }
    result.get(firstChar).add(k);
}
for (List<String> list: result.values()) {  
    Collections.sort(list);
}

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

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 来表示每一笔支付,包含金额和币种。这里有一个简单的例子:

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

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

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() 方法!于是你又添加了一些代码:

public class Purchase {  
    ...

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

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

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

没了。

一行代码搞定:)

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

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,所以我们会有这样一个方法:

public class ScreenUtil {  
    public static int dip2px(float dipValue) {
        return (int) (dipValue * density + 0.5f);
    }
}

然后这样使用:

layoutParams.width = ScreenUtil.dip2px(16F)  

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

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

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

layoutParams.width = 16.toPx()  

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

Null Safety

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

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)。

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 ,那么就可以这样写:

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

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

Coroutines

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

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

networkRequest { result ->  
    databaseSave(result) { rows ->
        // Result saved
    }
}

而用Coroutines的写法是这样的:

val result = networkRequest()  
databaseSave(result)  
// Result saved

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

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

你还在等什么?


关于作者

本文作者是 赵元杰,以下是他的自我介绍:

我叫赵元杰,从事 Android 开发工作已有 7 年,目前就职于武汉智领云科技。我个人从 2015 年开始关注 Kotlin,非常喜欢这门语言,希望能借这次机会将 Kotlin 推广给更多 Java 程序员。

我的个人博客 https://qq157755587.github.io

我们公司的三位合伙人均是在美国拿到博士学位,然后在硅谷顶级公司(Twitter / EA / Apple)工作十多年的顶尖工程师,所以在我们公司没有诸如“程序员35岁要不要转行”这类问题。公司一直致力于用最前沿技术解决行业问题,技术栈包括但不限于

移动端:Kotlin / Swift / Tensor flow lite / Node.js / Python

大数据:Mesos / Hadoop / Spark

如果你想在移动互联网或大数据方面得到顶尖工程师的指导,欢迎联系我。对了,我们是国内少有的注重效率,提倡家庭高于工作的965公司😀

如果你想联系他,这是他的微信二维码:

如果你想感谢或者鼓励他,这是他的收款二维码:

另外,除了作者之外,以下几位也对这篇文章的产出做出了贡献(顺序不分先后):