寫給 Java 開發者的 Kotlin 入坑指南

介紹

本文主要講解 Kotlin 基礎語法。java

主要內容

快速認識 Kotlin

基礎語法

擴展函數

委託

結尾

正文

快速認識 Kotlin

Kotlin 是著名 IDE 公司 JetBrains 創造出的一門基於 JVM 的語言。Kotlin 有着一下幾個特色:git

  • 簡潔,1行頂5行
  • 安全,主要指「空安全」
  • 兼容,與 Java 兼容
  • 工具友好,IntelliJ 對 Kotlin 簡直不要太友好

JetBrains 不只創造了 Kotlin,還創造了著名的 IntelliJ IDEA。Android 開發者使用的 Android Studio 就是基於 IntelliJ 改造出來的。github

基礎語法

1. 全部 Kotlin 類都是對象 (Everything in Kotlin is an object)

與 Java 不同是:Kotlin 沒有基本數據類型 (Primitive Types),全部 Kotlin 裏面的類都是對象,他們都繼承自: Any這個類;與 Java 相似的是,Kotlin 提供了以下的內置類型:安全

Type Bit width 備註
Double 64 Kotlin 沒有 double
Float 32 Kotlin 沒有 float
Long 64 Kotlin 沒有 long
Int 32 Kotlin 沒有 int/Integer
Short 16 Kotlin 沒有 short
Byte 8 Kotlin 沒有 byte

思考題1:

既然 Kotlin 與 Java 是兼容的,那麼 Kotlin Int 與 Java int、Java Integer 之間是什麼關係?bash

思考題2:

Kotlin Any 類型與 Java Object 類型之間有什麼關係?多線程

2. 可見性修飾符 (Visibility Modifiers)

修飾符 描述
public 與Java一致
private 與Java一致
protected 與Java一致
internal 同 Module 內可見

3. 變量定義 (Defining Variables)

定義一個 Int 類型的變量:

var a: Int = 1
複製代碼

定義一個 Int 類型的常量(~~不可變的變量、~~只讀的變量)

val b: Int = 1
複製代碼

類型可推導時,類型申明可省略:

val c = 1
複製代碼

語句末尾的;無關緊要:

val d: Int;
d = 1;
複製代碼

小結:框架

  • var 定義變量
  • val 定義常量(~~不可變的變量、~~只讀變量)
  • Kotlin 支持類型自動推導

思考題3:

Kotlin val 變量與 Java 的 final 有什麼關係?ide

4 空安全 (Null Safety)

定義一個可爲空的 String 變量:

var b: String? = "Kotlin"
b = null
print(b)
// 輸出 null
複製代碼

定義一個不可爲空的 String 變量:

var a: String = "Kotlin"
a = null
// 編譯器報錯,null 不能被賦給不爲空的變量
複製代碼

變量賦值:

var a: String? = "Kotlin"
var b: String = "Kotlin"
b = a // 編譯報錯,String? 類型不能夠賦值給 String 類型

a = b // 編譯經過
複製代碼

空安全調用

var a: String? = "Kotlin"
print(a.length) // 編譯器報錯,由於 a 是可爲空的類型
print(a?.length) // 使用?. 的方式調用,輸出 null
複製代碼

Elvis 操做符

// 下面兩個語句等價
val l: Int = if (b != null) b.length else -1
val l = b?.length ?: -1

// Elvis 操做符在嵌套屬性訪問時頗有用
val name = userInstance?.user?.baseInfo?.profile?.name?: "Kotlin"
複製代碼

小結:函數

  • T 表明不可爲空類型,編譯器會檢查,保證不會被 null 賦值
  • T? 表明可能爲空類型
  • 不能將 T? 賦值給 T
  • 使用 instance?.fun() 進行空安全調用
  • 使用 Elvis 操做符爲可空變量替代值,簡化邏輯

5. 類型檢查與轉換 (Type Checks and Casts)

類型判斷、智能類型轉換:

if (x is String) {
    print(x.length) // x 被編譯自動轉換爲 String
}
// x is String 相似 Java 裏的 instanceOf
複製代碼

不安全的類型轉換 as

val y = null
val x: String = y as String
//拋異常,null 不能被轉換成 String
複製代碼

安全的類型轉換 as?

val y = null
val z: String? = y as? String
print(z)
// 輸出 null
複製代碼

小結:工具

  • 使用 is 關鍵字進行類型判斷
  • 使用 as 進行類型轉換,可能會拋異常
  • 使用 as? 進行安全的類型轉換

6. if 判斷

基礎用法跟 Java 一毛同樣。它們主要區別在於:Java If is Statement,Kotlin If is Expression。所以它對比 Java 多了些「高級」用法,懶得講了。

7. for 循環

跟 Java 也差很少,隨便看代碼吧:

// 集合遍歷,跟 Java 差很少
for (item in collection) {
	print(item)
}

// 辣雞 Kotlin 語法
for (item in collection) print(item)

// 循環 1,2,3
for (i in 1..3) {
    println(i)
}

// 6,4,2,0
for (i in 6 downTo 0 step 2) {
    println(i)
}

複製代碼

8. when

when 就至關於高級版的 switch,它的高級之處在於支持模式匹配(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
複製代碼

9. 相等性 (Equality)

Kotlin 有兩種類型的相等性:

  • 結構相等 (Structural Equality)
  • 引用相等 (Referential Equality)

結構相等:

// 下面兩句兩個語句等價
a == b
a?.equals(b) ?: (b === null)
// 若是 a 不等於 null,則經過 equals 判斷 a、b 的結構是否相等
// 若是 a 等於 null,則判斷 b 是否是也等於 null
複製代碼

引用相等:

print(a === b)
// 判斷 a、b 是否是同一個對象
複製代碼

思考題4:

val a: Int = 10000
val boxedA: Int? = a
val anotherBoxedA: Int? = a
print(boxedA == anotherBoxedA)
print(boxedA === anotherBoxedA)
// 輸出什麼內容?
複製代碼

思考題5:

val a: Int = 1
val boxedA: Int? = a
val anotherBoxedA: Int? = a
print(boxedA == anotherBoxedA)
print(boxedA === anotherBoxedA)
// 輸出什麼內容?
複製代碼

10. 函數 (Functions)

fun triple(x: Int): Int {
    return 3 * x
}
// 函數名:triple
// 傳入參數:不爲空的 Int 類型變量
// 返回值:不爲空的 Int 類型變量
複製代碼

11. 類 (Classes)

類定義

使用主構造器(Primary Constructor)定義類一個 Person 類,須要一個 String 類型的變量:

class Person constructor(firstName: String) { ... }
複製代碼

若是主構造函數沒有註解或者可見性修飾符,constructor 關鍵字可省略:

class Person(firstName: String) { ... }
複製代碼

也可使用次構造函數(Secondary Constructor)定義類:

class Person {
    constructor(name: String) { ... }
}

// 建立 person 對象
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")
}
複製代碼

以上代碼輸出結果爲:

I am Kotlin.
Constructor
複製代碼

結論:init 代碼塊執行時機在類構造以後,但又在「次構造器」執行以前。

12. 繼承 (Inheritance)

  • 使用 open 關鍵字修飾的,能夠被繼承
  • 使用 open 關鍵字修飾的方法,能夠被重寫
  • 沒有 open 關鍵字修飾的類,不可被繼承
  • 沒有 open 關鍵字修飾的方法,不可被重寫
  • 以 Java 的思想來理解,Kotlin 的類和方法,默認狀況下是 final 的

定義一個可被繼承的 Base 類,其中的 add() 方法能夠被重寫,test() 方法不可被重寫:

open class Base {
    open fun add() { ... }
    fun test() { ... }
}
複製代碼

定義 Foo 繼承 Base 類,重寫 add() 方法

class Foo() : Base() {
    override fun add() { ... }
}
複製代碼
  • 使用 : 符號來表示繼承
  • 使用 override 重寫方法

13. This 表達式 (Expression)

class A {

    fun testA(){	}

    inner class B { // 在 class A 定義內部類 B

        fun testB(){	}

        fun foo() {
            this.testB() // ok
            this.testA() // 編譯錯誤
            this@A.testA() // ok
            this@B.testB() // ok
        }
    }
}
複製代碼

小結:

  • inner 關鍵字定義內部類
  • 在內部類當中訪問外部類,須要顯示使用 this@OutterClass.fun() 的語法

14. 數據類 (Data Class)

假設咱們有個這樣一個 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 代碼,大約會是這樣的:

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 爲咱們提供了另一種選擇,它叫作數據類:

data class Developer(var name: String)
複製代碼

上面這一行簡單的代碼,徹底能替代前面咱們的寫的那一大堆模板 Java 代碼,甚至額外多出了一些功能。若是將上面的數據類翻譯成等價的 Java 代碼,大概會長這個樣子:

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 的數據類不只爲咱們提供了 getter、setter、equals、hashCode、toString,還額外的幫咱們實現了 copy 方法!這也體現了 Kotlin 的簡潔特性。

序列化的坑

若是是舊工程遷移到 Kotlin,那麼可能須要注意這個坑:

// 定義一個數據類,其中成員變量 name 是不可爲空的 String 類型,默認值是 MOMO
data class Person(val age: Int, val name: String = "Kotlin")
val person = gson.fromJson("""{"age":42}""", Person::class.java)
print(person.name) // 輸出 null
複製代碼

對於上面的狀況,因爲 Gson 最初是爲 Java 語言設計的序列化框架,並不支持 Kotlin 不可爲空默認值這些特性,從而致使本來不可爲空的屬性變成null,本來應該有默認值的變量沒有默認值。

對於這種情,市面上已經有了解決方案:

15. 擴展 (Extensions)

如何才能在不修改源碼的狀況下給一個類新增一個方法?好比我想給 Context 類增長一個 toast 類,怎麼作?

若是使用 Java,上面的需求是沒法被知足的。然而 Kotlin 爲咱們提供了擴展語法,讓咱們能夠輕鬆實現以上的需求。

擴展函數

爲 Context 類定義一個 toast 方法:

fun Context.toast(msg: String, length: Int = Toast.LENGTH_SHORT){
    Toast.makeText(this, msg, length).show()
}
複製代碼

擴展函數的使用:

val activity: Context? = getActivity()
activity?.toast("Hello world!")
activity?.toast("Hello world!", Toast.LENGTH_LONG)
複製代碼

屬性擴展

除了擴展函數,Kotlin 還支持擴展屬性,用法基本一致。

思考題6:

上面的例子中,咱們給不可爲空的 Context 類增長了擴展函數,所以咱們在使用這個方法的收須要判空。實際上,Kotlin 還支持咱們爲 可爲空的 類增長擴展函數:

// 爲 Context? 添加擴展函數
fun Context?.toast(msg: String, length: Int = Toast.LENGTH_SHORT){
    if (this == null) {    //do something }
    Toast.makeText(this, msg, length).show()
}
複製代碼

擴展函數使用:

val activity: Context? = getActivity()
activity.toast("Hello world!")
activity.toast("Hello world!", Toast.LENGTH_LONG)
複製代碼

請問這兩種定義擴展函數的方式,哪一種更好?分別適用於什麼情景?爲何?

16. 委託 (Delegation)

Kotlin 中,使用by關鍵字表示委託:

interface Animal {
    fun bark()
}

// 定義 Cat 類,實現 Animal 接口
class Cat : Animal {
    override fun bark() {
        println("喵喵")
    }
}

// 將 Zoo 委託給它的參數 animal
class Zoo(animal: Animal) : Animal by animal

fun main(args: Array<String>) {
    val cat = Cat()
    Zoo(cat).bark()
}
// 輸出結果:喵喵
複製代碼

屬性委託 (Property Delegation)

其實,從上面類委託的例子中,咱們就能知道,Kotlin 之因此提供委託這個語法,主要是爲了方便咱們使用者,讓咱們能夠很方便的實現代理這樣的模式。這一點在 Kotlin 的委託屬性這一特性上體現得更是淋漓盡致。

Kotlin 爲咱們提供的標準委託很是有用。

by lazy 實現」懶加載「

// 經過 by 關鍵字,將 lazyValue 屬性委託給 lazy {} 裏面的實現
val lazyValue: String by lazy {
    val result = compute()
    println("computed!")
    result
}

// 模擬計算返回的變量
fun compute():String{
    return "Hello"
}

fun main(args: Array<String>) {
    println(lazyValue)
    println("=======")
    println(lazyValue)
}
複製代碼

以上代碼輸出的結果:

computed!
Hello
=======
Hello
複製代碼

因而可知,by lazy 這種委託的方式,可讓咱們輕鬆實現懶加載。 其內部實現,大體是這樣的:

by lazy 執行流程

lazy 求值的線程模式: LazyThreadSafetyMode

Kotlin 爲lazy 委託提供三種線程模式,他們分別是:

  • LazyThreadSafetyMode.SYNCHRONIZED
  • LazyThreadSafetyMode.NONE
  • LazyThreadSafetyMode.PUBLICATION

上面這三種模式,前面兩種很好理解:

  1. LazyThreadSafetyMode.SYNCHRONIZED 經過加鎖實現多線程同步,這也是默認的模式。
  2. LazyThreadSafetyMode.NONE 則沒有任何線程安全代碼,線程不安全。

咱們詳細看看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 委託變量,它的初始化方法是可能會被多個線程執行屢次的,但最後這個變量的取值是僅以第一次算出的值爲準的。即,哪一個線程最早算出這個值,就以這個值爲準。

by Delegates.observable 實現"觀察者模式"的變量

觀察者模式,又被稱爲訂閱模式。最多見的場景就是:好比讀者們訂閱了MOMO公衆號,每次MOMO更新的時候,讀者們就會收到推送。而觀察者模式應用到變量層面,就延伸成了:若是這個的值改變了,就通知我

class User {
    // 爲 name 這個變量添加觀察者,每次 name 改變的時候,都會執行括號內的代碼
    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適用於什麼樣的場景?

結尾

看完了?別走!留下思考題答案吧!

繼續閱讀請點擊-->【Kotlin Jetpack 實戰】傳送門

相關文章
相關標籤/搜索