找到那把三叉戟,你便能号令整个海洋。 ——《海王》
Java是当今世界最流行的工业级语言,有着非常成熟的生态和广泛的开发群体。当初Android选择Java作为开发语言,也是为了吸引Java程序员这个世界上最大的开发群体。最早的一批Android程序员可能已经用Java写了近十年Android程序了,各种实践方法也比较成熟了。那么今天我们为什么还需要一门新语言呢?这就要从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代码完成。
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#是怎么写的
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虚拟机,即JVM。虽然上面吐槽了Java语言本身的历史包袱,但是JVM还是非常优秀的,它有非常多的优点:
我们知道Java代码编译后会生成 字节码 ,然后字节码在JVM中运行注意Android中的虚拟机并不是JVM,而是Dalvik/ART,但也是编译成字节码。那么有没有可能新 创造 一门语言,编译的时候也生成字节码,然后在JVM中运行呢?这样既可以摆脱Java的历史包袱,又能享受到JVM和成熟Java框架的各种好处!答案当然是肯定的!实际上Java平台已经衍生出 Scala 、 Clojure 、 Groovy 等比较流行的语言了。而今天我们要讲的 Kotlin 则是Java平台中的新贵,出自大名鼎鼎的 JetBrains 公司。打开他们的官网你就会发现,很多著名的IDE(比如 IntelliJ 、 RubyMine 、 WebStorm )都是出自这家公司。IntelliJ是当今最主流的Java IDE,JetBrains公司在开发IntelliJ的过程中积累的经验令Kotlin的诞生显得水到渠成。而Google在2017年的I/O大会上 宣布 Kotlin成为Android开发的 官方编程语言 后,更是令Kotlin一夜之间成为最受瞩目的编程语言之一。那么我们来看看Kotlin会给我们带来哪些好处吧。
假设你在做一个金融交易系统,需要定义一个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为我们自动生成的,方便吧:)
Kotlin中的 Extension 类似于Objective-C中的 Category ,可以帮助我们扩展已有的Class,而无需继承这个Class,无论我们能不能访问源码。这对于系统Class以及一些第三方library中的Class特别有帮助。
随着项目规模的扩大,你的代码里肯定少不了一系列Utils类(比如 FileUtils.java
, DateUtils.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爸爸已经帮我们考虑到了:kissing_heart:。请参看 Android KTX 项目。这个项目是 Android Jetpack 的一部分,而Jetpack也是我们接下来要分享的内容。
这是一个值得吹上三天三夜的革新。在Quora上有这样一个问题: 为什么空指针异常被称为10亿美金的错误? 相信每一个Java程序员都对这个问题深有体会。看看我们为了避免程序崩溃,不得不写多么丑陋的代码:
if (a != null && a.b != null && a.b.c != null) { println(a.b.c.toString()); }
这样写有两个问题,一是代码很丑陋,二是本少爷很容易忘记做空指针检查啊:triumph:!!!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)
这行代码不会被执行。这样设计有什么好处呢?回到我们上一个例子,如果我们将 a
, a.b
, a.b.c
都定义为Nullable,那么就可以这样写:
println(a?.b?.c?.toString())
如果它们中间任意一个为null,那么 a?.b?.c?.toString()
这个 chain 就会返回 null
。是不是很强大很方便?理论上讲,只要我们的类型定义合理,那么90%的空指针异常都是能避免。为什么我不敢说100%?因为有时候我们看设计文档确定某个值绝对不可能为null,于是给它定义为Non-Null,结果程序运行时偏偏就传过来一个null……:joy:
Android开发中多线程处理一直是一个难点,稍微不小心就容易出问题。你可能已经学习过 RxJava ,并在项目中成功使用RxJava来处理线程问题。这非常好。但是如果你的业务逻辑并没有复杂到必须用RxJava来解决,你应该看看Kotlin中的 Courtines 。Courtines通常翻译成_协程 ,在 Lua 等程序语言中已经有着广泛的应用。它的概念稍微有些复杂,我们可以暂时认为它是一种 无需锁_并且 没有线程切换开销 的轻量级线程。
我们看一个例子,假设我需要从网络取回来一些数据,然后保存到数据库,那么传统的异步回调写法是这样的:
networkRequest { result -> databaseSave(result) { rows -> // Result saved } }
而用Courtines的写法是这样的:
val result = networkRequest() databaseSave(result) // Result saved
你可能已经发现了,这段代码根本不关心线程如何切换,只关心我到底要实现什么功能。实际上Courtines背后远比这要复杂,想要熟练使用需要经历一些学习曲线,但好在曲线并不算陡峭。我们后面会有专门文章来讲解如合使用Courtines。如果你已经激动的等不及了(和第一次知道Courtines时的我一样),可以先跟着Google Codelabs中的 教程 来练练手。
虽然前面我们讲到,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而言,目前社区反映比较多的问题是可见性修饰符(Visibility Modifiers)。我们知道Java中有四种访问权限: public
, protected
, private
和 package-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() }
你还在等什么?