Kotlin 基礎入門

函數類型

在 Kotlin 中 函數是一等公民。

// 定義
fun greetPeople(name: String, makeGreet: (String) -> Unit)

// 調用
greetPeople("Jimy", :: greetingWithChinese)

在上面的定義中,markGreet 是個什麼?是個函數類型對象的引用,只有對象才能被作爲函數的參數。也就是說,我們需要把一個函數類型的一個具體 “對象” 當做函數參數傳遞。
這個 makeGreet 被稱之爲函數引用,Function Reference,這也是 Kotlin 官方的說法。

這也說明,在 Kotlin 中,函數也是一個對象。

函數引用
在 Kotlin 裏,一個函數名的左邊加上雙冒號,它就不表示這個函數本身了,而表示一個對象,或者說一個指向對象的引用,但,這個對象可不是函數本身,而是一個和這個函數具有相同功能的對象。

// 這是函數的定義
fun greetingWithChinese(name: String) {
    println("早上好, $name !")
}

:: greetingWithChinese // 表示函數對象

既然它表示一個函數的對象,那麼,我們也可以把它賦值給一個變量。

val a = :: greetingWithChinese
val b = a

所以,我們也可以把這個變量作爲函數參數來調用

greetPeople("Jimy", a)
greetPeople("Jimy", b)

甚至,我們還可以直接通過這個引用調用 invoke() 來執行這個函數對象。

(:: greetingWithChinese).invoke("老王")
a.invoke("Jimy") 
b("Lucy") // 還可以這樣調用,這個是 Kotlin 的語法糖,實際上調用的 invoke 方法

我們可以對一個函數對象調用 invoke() 方法, 但是,不能對一個函數調用 invoke() 方法

greetingWithChinese("Jimy") // 這個編譯報錯

高階函數
事實上,函數不僅可以當做一個函數的參數,還能作爲函數的返回值,

定義:一個函數如果參數類型是函數或者返回值類型是函數,那麼這就是一個高階函數。

換個角度看函數類型。

fun greetingWithChinese(name: String) {
    println("早上好, $name !")
}

如果將這個函數的參數類型和返回值類型,抽象出來,它的函數類型就是 (String) -> Unit,
那麼,我們就可以直接聲明一個變量,指定這個變量的函數類型是它,

val a = (String) -> Unit -> Int = :: greetingWithChinese

注意
前面說了,可以把這個變量,當做參數傳遞給另一個函數。那麼思考一下,有沒有一種可能:我把這個函數本身直接挪過來作爲參數使用呢?什麼意思?

// 這個是之前說的調用方式 
greetPeople("Jimy", a)

我能不能向下面這麼寫?

greetPeople(
    "Lucy", 
    fun greetingWithChinese(name:String) {
        println("早上好, $name !")
    }
)

如圖:

咦,編輯器報錯了,Anonymous functions with names are prohibited
禁止使用帶有名稱的匿名函數。提示我把函數名稱移除。

greetPeople(
    "Lucy", 
    fun(name: String) {
        println("早上好, $name !")
    }
)

這樣就可以了。這也很好理解,函數名稱是給其他地方調用的,當前這種情況下這個函數作爲參數,並不會在其它地方調用,這個函數名稱也就沒有什麼作用,不允許有函數名稱,這種寫法叫做匿名函數。

而匿名函數實質上是一個表達式。
【】【】圖【】【】!!!
既然是表達式,那也就可以賦值給一個變量:

// 把匿名函數賦值給變量 c
val c = fun(name: String) {
        println("早上好, $name !")
    }

// 將 c 作爲參數傳遞,調用 greetPeople
greetPeople("Lucy", c)

那又有人會問了,能否把一個常規有名字的函數賦值給一個變量。比如

// 編譯報錯
val d = fun greetingWithChinese(name: String) {
        println("早上好, $name !")
    }

這是不允許的。
講道理,我都要把它賦值給一個變量了,那麼它本身的名字完全沒有必要。

有名字的函數不能賦值給一個變量,但是前面提到了,有名字的函數的引用,用雙冒號的形式,則可以。

只有對象纔可以賦值給一個變量,仔細想一想,匿名函數,其實不是函數,是一個對象。它是一個表達式,本質上是一個與它形式相同(參數,返回值一致)的函數類型的具體對象。
它和一個函數前面加上雙冒號是一樣的。

Lambda 表達式
對於一個函數類型的變量,我們除了可以把一個函數對象或者一個匿名函數賦值給它,還可以賦值一個 lambda 表達式。
什麼是 lambda 表達式?

// 匿名函數
val c = fun(name: String) {
        println("早上好, $name !")
    }

// 等價於下面的 lambda 表達式
{ name: String -> Unit
    println("早上好, $name !")
}

一個完整的 Lambda 表達式聲明如下:

val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }

由於 Kotlin 支持類型推導,所以它可以簡化爲

val sum: (Int, Int) -> Int = { x, y -> x + y }

或者:

val sum = { x: Int, y: Int -> x + y }

這個 lambda 表達式,也是一個函數類型的具體對象。所以也可以當做另一個函數的參數傳入。
回到前面的例子,調用 greetPeople() 就可以寫爲

greetPeople(
    "Lucy", 
    { name: String -> Unit
        println("早上好, $name !")
    })

而 Kotlin 中的函數,如果最後一個參數是 lambda 表達式,則可以將 lambda 表達式寫到參數外面:

greetPeople("Lucy") { name: String -> Unit
        println("早上好, $name !")
    }

就是這麼個形式,還有,lambda 表達式支持類型推導,前面定義了函數

fun greetPeople(name: String, makeGreet: (String) -> Unit)

從這個函數定義處,已經知道,調用時 lambda 函數表達式應該是什麼樣的函數類型,它的參數,返回值類型都確定了,所以可以在 lambda 表達式中省略,如下:

greetPeople("Lucy") { name ->
        println("早上好, $name !")
    }

再來看 Android 中常用的一個例子, 給 View 設置點擊事件:

public interface OnClickListener {
  void onClick(View v);
}
public void setOnClickListener(OnClickListener listener) {
  this.listener = listener;
}

java 中調用:

view.setOnClickListener(new OnClickListener() {
  @Override
  void onClick(View v) {
    doSomething();
  }
});

到 Kotlin 中就可以這麼寫:

fun setOnclickListener(onClick: (View) -> Unit) {
    this.onClick = onClick
}

view.setOnclickListener({view: View -> Unit
    doSomething()
})

將 lambda 表達式移到括號外面:

view.setOnclickListener() {view: View -> Unit
    doSomething()
}

參數和返回值類型可推導,省略參數和返回值類型:

view.setOnclickListener() {view ->
    doSomething()
}

如果 lambda 表達式是唯一參數,則括號都可以直接省略,同時 lambda 表達式如果是單參數,則這個參數也可以直接省略不寫。因爲對於 Kotlin 的 Lambda,單參數有一個默認的名稱:it, 使用的時候用 it 替代。

view.setOnclickListener() {
    doSomething()
}

所以就變成我們經常看到的樣子了。

對於多參數的 lambda 表達式,我們不能省略掉參數,但是如果參數沒有被用到,可以用 _ 來代替。

所以 Kotlin lambda 表達式本質是什麼呢?
其實和匿名函數一樣,本質上也是一個函數類型的具體對象。塔可以作爲函數的參數傳入,也可以賦值給一個變量。

另外 lambda 表達式也可以進行自調用

{x: Int, y: Int -> x + y}(1, 2)

總結一下

lambda 表達式語法:
1. lambda 表達式必須通過 {} 來包裹
2. 如果 lambda 聲明瞭參數部分的類型,且返回值支持類型推導,則 lambda 表達式變量就可以省略函數類型聲明
3. 如果 lambda 變量聲明瞭函數類型,那麼 lambda 表達式的參數部分的類型就可以省略
4. 如果 lambda 表達式返回的不是 Unit 類型,則默認最後一行表達式的值類型就是返回值類型。

lambda 表達式和函數的區別

  1. fun 在沒有等號、只有花括號的情況下,就是代碼塊函數體,如果返回值非 Unit,必須帶 return
fun foo(x: Int) {
    print(x)
}

fun foo(x: Int, y: Int): Int {
    return x + y
}
  1. fun 帶有等號,沒有花括號,是單表達式函數體,可以省略 return
fun foo(x: Int, y: Int) = x + y
  1. 不管是用 val 還是 fun 聲明,如果是等號加花括號的語法,就是聲明一個 lambda 表達式。
val foo = { x: Int, y: Int ->
    x + y
}
// 調用方式: foo.invoke(1, 2) 或者 foo(1, 2)
fun foo(x: Int) = { y: Int ->
    x + y
}
// 調用方式: foo(1).invoke(2) 或者 foo(1)(2)

類、接口與對象

Kotlin 中用 'class' 聲明類,如果沒有實體類,可以省略 {}

Kotlin 多個類可以聲明在同一個文件中

構造函數

在 Kotlin 中的一個類可以有一個主構造函數以及一個或多個次構造函數。主構造函數是類頭的一部分:它跟在類名(與可選的類型參數)後。

class Student constructor(id: Int, name: String, age: Int) {

}

如果主構造函數沒有任何註解或者可見性修飾符,可以省略這個 constructor 關鍵字。

class Student(id: Int, name: String, age: Int) {
    constructor(name: String, age: Int): this(0, name, age)  // 次構造函數
}

init 代碼塊

類中允許存在多個 init 代碼塊,對於不同的業務初始化可以放在不同的 init 代碼塊中,按代碼順序一次執行。
類似於 java 類中的靜態代碼塊。

init 代碼塊中可以直接訪問構造函數的參數,其它地方不可以。

成員變量

聲明

  1. 和 java 類似,可以在類代碼塊中直接聲明。
  2. 構造方法參數加上 var 或者 val 修飾,則可以視爲類中聲明瞭一個同名的成員變量。也可以加上可見性修飾符。

初始化

成員變量聲明時需進行初始化,或者顯式聲明延遲初始化,使用關鍵字 lateinit 顯示聲明延遲初始化。

class Student(id: Int, var name: String, var age: Int) {
    constructor(name: String, age: Int): this(0, name, age)

    init {
        println("student name is $name")
    }

    fun printInfo() {
        age += 1
    }

    lateinit var gender: String  // 延遲初始化
}

繼承

使用

open class Person(id: Int, name: String)

class Student(id: Int, var name: String, var age: Int): Person(id,name) {
    // ...
}

被繼承的類需要用 open 修改,同樣父類的中法,默認是不可被重寫的,若允許被重寫,需要使用 open 修飾。

接口

interface MyInterface {
    fun bar()
    val propertyWithImplementation: String
        get() = "foo"

    fun foo() {
        print(prop)
    }
}

接口允許有屬性,接口中的方法允許有默認實現,接口不需要用 open 修飾,因爲接口就是用來讓子類實現,然後被被子類重寫的。包括屬性也是可以被重寫的。接口可以繼承自其它接口。

內部類

java 中在類中再聲明類,稱之爲內部類,如果使用 static 修飾,則爲靜態內部類。

class A {
    private String a

    class B {

    }

    static class C {

    }
}

請問 B 和 C 中能訪問 A 的成員 a 嗎?

在 Kotlin 中沒有 static 關鍵字,默認聲明就是靜態內部類,如要聲明內部類,需要添加 inner 關鍵字

class A {
    // 聲明內部類需要使用 inner 關鍵字
    inner class B {

    }

    // 靜態內部類
    class c {

    }
}

伴生對象

Kotlin 中沒有了 static 關鍵字,但是有伴生對象的概念,類似於 java 中的 static

class Student {
    companion object {
        const val TAG = "Student" // 常量
        var count = 0 // 靜態變量

        // 靜態方法
        fun test() {

        }
    }
}

object 是在 Kotlin 中的用法。

  1. 在類中與 companion 一起,表示伴生對象。
  2. 聲明類。Kotlin 中除了用 class 聲明類意外,還可以用 object 來聲明一個類,用 object 聲明的類,天生單例。
object SystemUtils {
    ...
}
  1. 實現某個接口,或者抽象類:
interface ICount {
    fun count(num: Int): Int
}

val myCount  = object: ICount {
        override fun count(num: Int): Int {
            TODO("Not yet implemented")
        }
    }

可見性修飾符

  1. public 公開,可見性最大,哪裏都可以引用
  2. private 私有,可見性最小,根據聲明位置可以分爲類中可見和文件中可見。
  3. protected 保護, 相當於 private + 子類可見。
  4. internal 內部,同一 module 可見

java 中 protected 表示包內可見 + 子類可見
Kotlin protected 的可見範圍收窄了,原因是 相比於包, Kotlin 更注重 module。

另外 private 修飾 java 中的內部類對外部類可見,Kotlin 中的內部類對外部類不可見。

Kotlin 中的類和方法,不寫時,默認是 public + final 的。

數據類

data class Student(val name: String, var age: Int = 18)
  • 自動生成 getter()/setter() 方法 !!!???
  • 編譯器自動生成 equals()/hashCode() 方法
  • 編譯器自動生成 toString() 方法
  • 編譯器自動生成 `componentN()`` 函數(解構)
  • 編譯器自動生成 copy 函數
data class Student(val name: String, var age: Int = 18) {
    var isBoy = true // 該屬性不會在生成的方法中
}

密封類

sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()

可以用來代替枚舉,使用 When 表達式時如果能覆蓋所有,則無需 else 分支。

fun eval(expr: Expr): Double = when(expr) {
    is Const -> expr.number
    is Sum -> eval(expr.e1) + eval(expr.e2)
    NotANumber -> Double.NaN
    // 不再需要 `else` 子句,因爲我們已經覆蓋了所有的情況
}

其它方便的特性:

函數參數默認值

fun sayHi(name: String = "world") = println("Hello " + name)

重載函數再也不用寫很多個了。

本地函數(嵌套函數)

本地函數是個啥玩意?
我們知道在函數中可以聲明局部變量(這不是廢話嗎?)在 Kotlin 中我們甚至還可以在方法中類似聲明局部變量一樣聲明一個方法,稱之爲本地函數。

fun login(user: String, password: String, illegalStr: String) {
    // 驗證 user 是否爲空
    if (user.isEmpty()) {
        throw IllegalArgumentException(illegalStr)
    }
    // 驗證 password 是否爲空
    if (password.isEmpty()) {
        throw IllegalArgumentException(illegalStr)
    }
    // 執行登錄
}

校驗參數這不部分代碼有些冗餘,我們可以抽成一個方法,但是我們又沒有必要暴露到其它地方,因爲只有這個 login 方法會用到,則可以在 login 方法內部聲明一個嵌套函數

fun login(user: String, password: String, illegalStr: String) {
    fun validate(value: String, illegalStr: String) {
      if (value.isEmpty()) {
          throw IllegalArgumentException(illegalStr)
      }
    }
    validate(user, illegalStr)
    validate(password, illegalStr)
    // 執行登錄
}

try-catch 表達式

Kotlin 中 try-catch 語句也可以是一個表達式,允許代碼塊的最後一行作爲返回值

val a: Int? = try { parseInt(input) } catch (e: NumberFormatException) { null }

其它知識:
數組、集合(可變集合、不可變集合)、泛型(逆變和協變)、枚舉、委託、註解、反射
內聯函數/中綴函數

== 和 ===

Kotlin 中用 == 判斷 equals 相等,對象內容相等
=== 判斷相等引用的內存地址相等

拓展函數和拓展熟悉

顧名思義,可以給一個類“增加”額外的方法和屬性。
舉個例子

data class Student(val name: String)

這個類本身沒有“上課”這個方法,我現在給它增加一個拓展函數。

fun Student.attendClass() {
    println("${this.name} 正在上課")
}

這樣,Student 這個類的實例對象就可以調用這個拓展方法了。

val student = Student("Jimy")
student.attendClass()

拓展方法直接定義在文件中。
拓展屬性類似。
想一想:三方庫裏面的類,不能直接修改,是不是可以增加拓展方法和拓展屬性了?有沒有很激動。

Top-level

kotlin 文件已 .kt 爲後綴,在 Kotlin 文件中,我們可以直接聲明的變量,方法,也就是不在 class 內部聲明,稱之爲 top-level declaration 頂層聲明。

// 屬於 package,不在 class/object 內
const val KEY = "123456"

fun test() {
    println("---test---")
}

它不屬於任何 class , 而是直接屬於 package, 它和靜態變量一樣是全局的,使用起來更方便,調用它的時候連類名都不用寫:

import com.sharpcj.demo.test
import com.sharpcj.demo.KEY

test()
val x = KEY

結合前面說的,寫工具類一般就有三種方式:

// companion object
class Util {
    companion object {
        fun test1() {

        }
        fun test2() {

        }
    }
}

// object
object Util {
    fun test1() {

        }
    fun test2() {

    } 
}

// top level 方法
fun test1() {
}
fun test2() {
} 

建議:

  • 如果想寫工具類的功能,直接創建文件,寫 top-level「頂層」函數。雖然沒有 class 的概念,但是相關的方法,寫在同一個文件中,方便閱讀管理。
  • 如果需要繼承別的類或者實現接口,就用 objectcompanion object

by lazy (委託)

前面提到,如果一個屬性需要延遲初始化,可以使用 lateinit 進行修飾,另外還有另外一種方式,就是使用 by lazy 方法初始化。
在給一個變量賦值的時候使用 by lazy 代碼塊,可以做到單例,在第一次使用時初始化。by lazy 實際上使用了 Kotlin 的委託特性,底層原理和 DCL 的單例模式類似。

val student: Student by lazy {
    Student(name = "Lucy", age = 18)
}

這裏順便說一下用 Kotlin 實現單例,看看有多有多方便了。
前面提到,用 object 聲明一個類,即是單例的。

object Singleton

再看看 DCL 的方式,java 實現:

public class Singleton {
    private Singleton(){}

    private volatile static Singleton instance;

    private static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

再看看 Kotlin 實現

class Singleton private constructor() {
    companion object {
        val instance: Singleton by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
            Singleton()
        }
    }
}

這樣是不是很方便,當然上述方式也有一個缺點,就是獲取單例的時候沒有辦法傳參,那麼改爲和 java 一樣的方式看看:

class Singleton private constructor() {
    companion object {
        @Volatile
        private var instance: Singleton? = null

        fun getInstance(): Singleton {
            return instance?: synchronized(this) {
                instance?:Singleton().also {
                    // 初始化工作
                    instance = it
                }
            }
        }
    }
}

這次可以傳參進來了,代碼依然簡潔很多。

其它:
數組、集合(可變集合和不可變集合,集合操作符)、泛型(協變和逆變)、註解、反射。

委託、內聯函數、中綴函數、攜程等等。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章