背景
近幾年,Android 相關(guān)的新技術(shù)層出不窮。往往這個(gè)技術(shù)還沒(méi)學(xué)完,下一個(gè)新技術(shù)又出來(lái)了。很多人都是一臉黑人問(wèn)號(hào)?不少開(kāi)發(fā)者甚至開(kāi)始哀嚎:“求求你們別再創(chuàng)造新技術(shù)了,我們學(xué)不動(dòng)了!”
在這些新技術(shù)里,Kotlin
,協(xié)程
,Jetpack
是最具代表性的,它們的知識(shí)體系復(fù)雜,學(xué)起來(lái)難度大,學(xué)完后實(shí)戰(zhàn)的坑也多。本系列文章原本是我為小組新人培訓(xùn)準(zhǔn)備的,現(xiàn)在重新整理分享出來(lái)。
簡(jiǎn)介
本文主要講解 Kotlin 基礎(chǔ)語(yǔ)法
。
本文是《Kotlin Jetpack 實(shí)戰(zhàn)》的開(kāi)篇。
每個(gè) Java 開(kāi)發(fā)者都應(yīng)該學(xué) Kotlin
推薦學(xué)習(xí) Kotlin 的理由有很多,比如:Kotlin 更簡(jiǎn)潔,Kotlin 有協(xié)程,Kotlin 有擴(kuò)展函數(shù),學(xué)了 Kotlin 后學(xué)別的語(yǔ)言會(huì)很快,比如:Python,Swift,Dart, Ruby...
不過(guò),如果你是 Android 開(kāi)發(fā)者,我勸你別再做無(wú)謂的掙扎了,趕緊入坑吧!
1. 快速認(rèn)識(shí) Kotlin
Kotlin 是著名 IDE 公司 JetBrains 創(chuàng)造出的一門(mén)基于 JVM 的語(yǔ)言。Kotlin 有著以下幾個(gè)特點(diǎn):
- 簡(jiǎn)潔,1行頂5行
- 安全,主要指“空安全”
- 兼容,與 Java 兼容
- 工具友好,IntelliJ 對(duì) Kotlin 簡(jiǎn)直不要太友好
JetBrains 不僅創(chuàng)造了 Kotlin,還創(chuàng)造了著名的 IntelliJ IDEA。Android 開(kāi)發(fā)者使用的 Android Studio 就是基于 IntelliJ 改造出來(lái)的。
2. 所有 Kotlin 類(lèi)都是對(duì)象 (Everything in Kotlin is an object)
與 Java 不一樣是:Kotlin 沒(méi)有基本數(shù)據(jù)類(lèi)型
(Primitive Types),所有 Kotlin 里面的類(lèi)都是對(duì)象,它們都繼承自: Any
這個(gè)類(lèi);與 Java 類(lèi)似的是,Kotlin 提供了如下的內(nèi)置類(lèi)型:
Type | Bit width | 備注 |
---|---|---|
Double | 64 | Kotlin 沒(méi)有 double |
Float | 32 | Kotlin 沒(méi)有 float |
Long | 64 | Kotlin 沒(méi)有 long |
Int | 32 | Kotlin 沒(méi)有 int/Integer |
Short | 16 | Kotlin 沒(méi)有 short |
Byte | 8 | Kotlin 沒(méi)有 byte |
思考題1:
既然 Kotlin 與 Java 是兼容的,那么 Kotlin Int 與 Java int、Java Integer 之間是什么關(guān)系?
思考題2:
Kotlin Any 類(lèi)型與 Java Object 類(lèi)型之間有什么關(guān)系?
3. 可見(jiàn)性修飾符 (Visibility Modifiers)
修飾符 | 描述 |
---|---|
public | 與Java一致 |
private | 與Java一致 |
protected | 與Java一致 |
internal | 同 Module 內(nèi)可見(jiàn) |
4. 變量定義 (Defining Variables)
定義一個(gè) Int 類(lèi)型的變量:
var a: Int = 1
定義一個(gè) Int 類(lèi)型的常量(不可變的變量?只讀的變量?)
val b: Int = 1
類(lèi)型可推導(dǎo)時(shí),類(lèi)型申明可省略:
val c = 1
語(yǔ)句末尾的;
可有可無(wú):
val d: Int;
d = 1;
小結(jié):
-
var
定義變量 -
val
定義常量(不可變的變量?只讀變量?) - Kotlin 支持類(lèi)型自動(dòng)推導(dǎo)
思考題3:
Kotlin val 變量與 Java 的 final 有什么關(guān)系?
5. 空安全 (Null Safety)
定義一個(gè)可為空的 String 變量:
var b: String? = "Kotlin"
b = null
print(b)
// 輸出 null
定義一個(gè)不可為空的 String 變量:
var a: String = "Kotlin"
a = null
// 編譯器報(bào)錯(cuò),null 不能被賦給不為空的變量
變量賦值:
var a: String? = "Kotlin"
var b: String = "Kotlin"
b = a // 編譯報(bào)錯(cuò),String? 類(lèi)型不可以賦值給 String 類(lèi)型
a = b // 編譯通過(guò)
空安全調(diào)用
var a: String? = "Kotlin"
print(a.length) // 編譯器報(bào)錯(cuò),因?yàn)?a 是可為空的類(lèi)型
print(a?.length) // 使用?. 的方式調(diào)用,輸出 null
Elvis 操作符
// 下面兩個(gè)語(yǔ)句等價(jià)
val l: Int = if (b != null) b.length else -1
val l = b?.length ?: -1
// Elvis 操作符在嵌套屬性訪問(wèn)時(shí)很有用
val name = userInstance?.user?.baseInfo?.profile?.name?: "Kotlin"
小結(jié):
- T 代表不可為空類(lèi)型,編譯器會(huì)檢查,保證不會(huì)被 null 賦值
- T? 代表可能為空類(lèi)型
- 不能將 T? 賦值給 T
- 使用 instance?.fun() 進(jìn)行空安全調(diào)用
- 使用 Elvis 操作符為可空變量替代值,簡(jiǎn)化邏輯
6. 類(lèi)型檢查與轉(zhuǎn)換 (Type Checks and Casts)
類(lèi)型判斷、智能類(lèi)型轉(zhuǎn)換:
if (x is String) {
print(x.length) // x 被編譯自動(dòng)轉(zhuǎn)換為 String
}
// x is String 類(lèi)似 Java 里的 instanceOf
不安全的類(lèi)型轉(zhuǎn)換 as
val y = null
val x: String = y as String
//拋異常,null 不能被轉(zhuǎn)換成 String
安全的類(lèi)型轉(zhuǎn)換 as?
val y = null
val z: String? = y as? String
print(z)
// 輸出 null
小結(jié):
- 使用
is
關(guān)鍵字進(jìn)行類(lèi)型判斷 - 使用
as
進(jìn)行類(lèi)型轉(zhuǎn)換,可能會(huì)拋異常 - 使用
as?
進(jìn)行安全的類(lèi)型轉(zhuǎn)換
7. if 判斷
基礎(chǔ)用法跟 Java 一毛一樣。它們主要區(qū)別在于:Java If
is Statement,Kotlin If
is Expression。因此它對(duì)比 Java 多了些“高級(jí)”用法,懶得講了,咱看后面的實(shí)戰(zhàn)吧。
8. for 循環(huán)
跟 Java 也差不多,隨便看代碼吧:
// 集合遍歷,跟 Java 差不多
for (item in collection) {
print(item)
}
// 辣雞 Kotlin 語(yǔ)法
for (item in collection) print(item)
// 循環(huán) 1,2,3
for (i in 1..3) {
println(i)
}
// 6,4,2,0
for (i in 6 downTo 0 step 2) {
println(i)
}
9. when
when 就相當(dāng)于高級(jí)版的 switch,它的高級(jí)之處在于支持模式匹配(Pattern Matching)
:
val x = 9
when (x) {
in 1..10 -> print("x is in the range")
in validNumbers -> print("x is valid")
!in 10..20 -> print("x is outside the range")
is String -> print("x is String")
x.isOdd() -> print("x is odd")
else -> print("none of the above")
}
// 輸出:x is in the range
10. 相等性 (Equality)
Kotlin 有兩種類(lèi)型的相等性:
- 結(jié)構(gòu)相等 (Structural Equality)
- 引用相等 (Referential Equality)
結(jié)構(gòu)相等:
// 下面兩句兩個(gè)語(yǔ)句等價(jià)
a == b
a?.equals(b) ?: (b === null)
// 如果 a 不等于 null,則通過(guò) equals 判斷 a、b 的結(jié)構(gòu)是否相等
// 如果 a 等于 null,則判斷 b 是不是也等于 null
引用相等:
print(a === b)
// 判斷 a、b 是不是同一個(gè)對(duì)象
思考題4:
val a: Int = 10000
val boxedA: Int? = a
val anotherBoxedA: Int? = a
print(boxedA == anotherBoxedA)
print(boxedA === anotherBoxedA)
// 輸出什么內(nèi)容?
思考題5:
val a: Int = 1
val boxedA: Int? = a
val anotherBoxedA: Int? = a
print(boxedA == anotherBoxedA)
print(boxedA === anotherBoxedA)
// 輸出什么內(nèi)容?
11. 函數(shù) (Functions)
fun triple(x: Int): Int {
return 3 * x
}
// 函數(shù)名:triple
// 傳入?yún)?shù):不為空的 Int 類(lèi)型變量
// 返回值:不為空的 Int 類(lèi)型變量
12. 類(lèi) (Classes)
類(lèi)定義
使用主構(gòu)造器(Primary Constructor)定義類(lèi)一個(gè) Person 類(lèi),需要一個(gè) String 類(lèi)型的變量:
class Person constructor(firstName: String) { ... }
如果主構(gòu)造函數(shù)沒(méi)有注解或者可見(jiàn)性修飾符,constructor 關(guān)鍵字可省略:
class Person(firstName: String) { ... }
也可以使用次構(gòu)造函數(shù)(Secondary Constructor)定義類(lèi):
class Person {
constructor(name: String) { ... }
}
// 創(chuàng)建 person 對(duì)象
val instance = Person("Kotlin")
init 代碼塊
Kotlin 為我們提供了 init 代碼塊,用于放置初始化代碼:
class Person {
var name = "Kotlin"
init {
name = "I am Kotlin."
println(name)
}
constructor(s: String) {
println(“Constructor”)
}
}
fun main(args: Array<String>) {
Person("Kotlin")
}
以上代碼輸出結(jié)果為:
I am Kotlin.
Constructor
結(jié)論:init 代碼塊執(zhí)行時(shí)機(jī)在類(lèi)構(gòu)造之后,但又在“次構(gòu)造器”執(zhí)行之前。
13. 繼承 (Inheritance)
- 使用 open 關(guān)鍵字修飾的
類(lèi)
,可以被繼承 - 使用 open 關(guān)鍵字修飾的
方法
,可以被重寫(xiě) -
沒(méi)有
open 關(guān)鍵字修飾的類(lèi),不可
被繼承 -
沒(méi)有
open 關(guān)鍵字修飾的方法,不可
被重寫(xiě) - 以 Java 的思想來(lái)理解,
Kotlin 的類(lèi)和方法,默認(rèn)情況下是 final 的
定義一個(gè)可被繼承的
Base 類(lèi),其中的 add() 方法可以被重寫(xiě)
,test() 方法不可
被重寫(xiě):
open class Base {
open fun add() { ... }
fun test() { ... }
}
定義 Foo 繼承 Base 類(lèi),重寫(xiě) add() 方法
class Foo() : Base() {
override fun add() { ... }
}
- 使用
:
符號(hào)來(lái)表示繼承 - 使用
override
重寫(xiě)方法
14. This 表達(dá)式 (Expression)
class A {
fun testA(){ }
inner class B { // 在 class A 定義內(nèi)部類(lèi) B
fun testB(){ }
fun foo() {
this.testB() // ok
this.testA() // 編譯錯(cuò)誤
this@A.testA() // ok
this@B.testB() // ok
}
}
}
小結(jié):
-
inner
關(guān)鍵字定義內(nèi)部類(lèi) - 在內(nèi)部類(lèi)當(dāng)中訪問(wèn)外部類(lèi),需要顯示使用
this@OutterClass.fun()
的語(yǔ)法
15. 數(shù)據(jù)類(lèi) (Data Class)
假設(shè)我們有個(gè)這樣一個(gè) Java Bean:
public class Developer {
private String name;
public Developer(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Developer developer = (Developer) o;
return name != null ? name.equals(developer.name) : developer.name == null;
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
return result;
}
@Override
public String toString() {
return "Developer{" + "name='" + name + '}';
}
}
如果我們將其翻譯成 Kotlin 代碼,大約會(huì)是這樣的:
class Developer(var name: String?) {
override fun equals(o: Any?): Boolean {
if (this === o) return true
if (o == null || javaClass != o.javaClass) return false
val developer = o as Developer?
return if (name != null) name == developer!!.name else developer!!.name == null
}
override fun hashCode(): Int {
return if (name != null) name!!.hashCode() else 0
}
override fun toString(): String {
return "Developer{" + "name='" + name + '}'.toString()
}
}
然而,Kotlin 為我們提供了另外一種選擇,它叫做數(shù)據(jù)類(lèi)
:
data class Developer(var name: String)
上面這一行簡(jiǎn)單的代碼,完全能替代
前面我們的寫(xiě)的那一大堆模板 Java 代碼,甚至額外多出了一些功能。如果將上面的數(shù)據(jù)類(lèi)
翻譯成等價(jià)的 Java 代碼,大概會(huì)長(zhǎng)這個(gè)樣子:
public final class Developer {
@NotNull
private String name;
public Developer(@NotNull String name) {
super();
this.name = name;
}
@NotNull
public final String getName() { return this.name; }
public final void setName(@NotNull String var1) { this.name = var1; }
@NotNull
public final Developer copy(@NotNull String name) { return new Developer(name); }
public String toString() { return "Developer(name=" + this.name + ")"; }
public int hashCode() { return this.name != null ? this.name.hashCode() : 0; }
public boolean equals(Object var1) {
if (this != var1) {
if (var1 instanceof Developer) {
Developer var2 = (Developer)var1;
if (Intrinsics.areEqual(this.name, var2.name)) {
return true;
}
}
return false;
} else {
return true;
}
}
}
可以看到,Kotlin 的數(shù)據(jù)類(lèi)不僅為我們提供了 getter、setter、equals、hashCode、toString,還額外的幫我們實(shí)現(xiàn)了 copy 方法!這也體現(xiàn)了 Kotlin 的簡(jiǎn)潔
特性。
序列化的坑
如果是舊工程遷移到 Kotlin,那么可能需要注意這個(gè)坑:
// 定義一個(gè)數(shù)據(jù)類(lèi),其中成員變量 name 是不可為空的 String 類(lèi)型,默認(rèn)值是 Android
data class Person(val age: Int, val name: String = "Kotlin")
val person = gson.fromJson("""{"age":42}""", Person::class.java)
print(person.name) // 輸出 null
對(duì)于上面的情況,由于 Gson 最初是為 Java 語(yǔ)言設(shè)計(jì)的序列化框架,并不支持 Kotlin 不可為空
、默認(rèn)值
這些特性,從而導(dǎo)致原本不可為空的屬性變成null
,原本應(yīng)該有默認(rèn)值的變量沒(méi)有默認(rèn)值。
對(duì)于這種情,市面上已經(jīng)有了解決方案:
16. 擴(kuò)展 (Extensions)
如何才能在不修改源碼的情況下給一個(gè)類(lèi)新增一個(gè)方法?比如我想給 Context 類(lèi)增加一個(gè) toast 類(lèi),怎么做?
如果使用 Java,上面的需求是無(wú)法被滿足的。然而 Kotlin 為我們提供了擴(kuò)展
語(yǔ)法,讓我們可以輕松實(shí)現(xiàn)以上的需求。
擴(kuò)展函數(shù)
為 Context 類(lèi)定義一個(gè) toast 方法:
fun Context.toast(msg: String, length: Int = Toast.LENGTH_SHORT){
Toast.makeText(this, msg, length).show()
}
擴(kuò)展函數(shù)的使用:
val activity: Context? = getActivity()
activity?.toast("Hello world!")
activity?.toast("Hello world!", Toast.LENGTH_LONG)
屬性擴(kuò)展
除了擴(kuò)展函數(shù),Kotlin 還支持擴(kuò)展屬性
,用法基本一致。
思考題6:
上面的例子中,我們給不可為空的
Context 類(lèi)增加了擴(kuò)展函數(shù),因此我們?cè)谑褂眠@個(gè)方法的時(shí)候需要判斷。實(shí)際上,Kotlin 還支持我們?yōu)?可為空的
類(lèi)增加擴(kuò)展函數(shù):
// 為 Context? 添加擴(kuò)展函數(shù)
fun Context?.toast(msg: String, length: Int = Toast.LENGTH_SHORT){
if (this == null) { //do something }
Toast.makeText(this, msg, length).show()
}
擴(kuò)展函數(shù)使用:
val activity: Context? = getActivity()
activity.toast("Hello world!")
activity.toast("Hello world!", Toast.LENGTH_LONG)
請(qǐng)問(wèn)這兩種定義擴(kuò)展函數(shù)的方式,哪種更好?分別適用于什么情景?為什么?
17. 委托 (Delegation)
Kotlin 中,使用by
關(guān)鍵字表示委托:
interface Animal {
fun bark()
}
// 定義 Cat 類(lèi),實(shí)現(xiàn) Animal 接口
class Cat : Animal {
override fun bark() {
println("喵喵")
}
}
// 將 Zoo 委托給它的參數(shù) animal
class Zoo(animal: Animal) : Animal by animal
fun main(args: Array<String>) {
val cat = Cat()
Zoo(cat).bark()
}
// 輸出結(jié)果:喵喵
屬性委托 (Property Delegation)
其實(shí),從上面類(lèi)委托的例子中,我們就能知道,Kotlin 之所以提供委托這個(gè)語(yǔ)法,主要是為了方便我們使用者,讓我們可以很方便的實(shí)現(xiàn)代理
這樣的模式。這一點(diǎn)在 Kotlin 的委托屬性
這一特性上體現(xiàn)得更是淋漓盡致。
Kotlin 為我們提供的標(biāo)準(zhǔn)委托非常有用。
by lazy 實(shí)現(xiàn)”懶加載“
// 通過(guò) by 關(guān)鍵字,將 lazyValue 屬性委托給 lazy {} 里面的實(shí)現(xiàn)
val lazyValue: String by lazy {
val result = compute()
println("computed!")
result
}
// 模擬計(jì)算返回的變量
fun compute():String{
return "Hello"
}
fun main(args: Array<String>) {
println(lazyValue)
println("=======")
println(lazyValue)
}
以上代碼輸出的結(jié)果:
computed!
Hello
=======
Hello
由此可見(jiàn),by lazy 這種委托的方式,可以讓我們輕松實(shí)現(xiàn)懶加載
。
其內(nèi)部實(shí)現(xiàn),大致是這樣的:
lazy 求值的線程模式: LazyThreadSafetyMode
Kotlin 為lazy 委托
提供三種線程模式,他們分別是:
- LazyThreadSafetyMode.SYNCHRONIZED
- LazyThreadSafetyMode.NONE
- LazyThreadSafetyMode.PUBLICATION
上面這三種模式,前面兩種很好理解:
- LazyThreadSafetyMode.SYNCHRONIZED 通過(guò)加鎖實(shí)現(xiàn)多線程同步,這也是默認(rèn)的模式。
- LazyThreadSafetyMode.NONE 則沒(méi)有任何線程安全代碼,線程不安全。
我們?cè)敿?xì)看看LazyThreadSafetyMode.PUBLICATION
,官方文檔的解釋是這樣的:
Initializer function can be called several times on concurrent access to uninitialized [Lazy] instance value, but only the first returned value will be used as the value of [Lazy] instance.
意思就是,用LazyThreadSafetyMode.PUBLICATION
模式的 lazy 委托變量,它的初始化方法是可能會(huì)被多個(gè)線程執(zhí)行多次的,但最后這個(gè)變量的取值是僅以第一次算出的值為準(zhǔn)的。即,哪個(gè)線程最先算出這個(gè)值,就以這個(gè)值為準(zhǔn)。
by Delegates.observable 實(shí)現(xiàn)"觀察者模式"的變量
觀察者模式
,又被稱為訂閱模式
。最常見(jiàn)的場(chǎng)景就是:比如讀者們訂閱了Android
公眾號(hào),每次Android
更新的時(shí)候,讀者們就會(huì)收到推送。而觀察者模式應(yīng)用到變量層面,就延伸成了:如果這個(gè)的值改變了,就通知我
。
class User {
// 為 name 這個(gè)變量添加觀察者,每次 name 改變的時(shí)候,都會(huì)執(zhí)行括號(hào)內(nèi)的代碼
var name: String by Delegates.observable("<no name>") {
prop, old, new ->
println("name 改變了:$old -> $new")
}
}
fun main(args: Array<String>) {
val user = User()
user.name = "first: Tom"
user.name = "second: Jack"
}
以上代碼的輸出為:
name 改變了:<no name> -> first: Tom
name 改變了:first: Tom -> second: Jack
思考題7:
lazy 委托的LazyThreadSafetyMode.PUBLICATION
適用于什么樣的場(chǎng)景?
結(jié)尾
都看到這了,點(diǎn)個(gè)贊唄!