Kotlin入門指南


Kotlin的優(yōu)勢(shì)

代碼簡(jiǎn)潔高效、強(qiáng)大的when語法,不用寫分號(hào)結(jié)尾,findViewById光榮退休,空指針安全、強(qiáng)大的擴(kuò)展功能、函數(shù)式編程、支持lambda表達(dá)式、流式API等等

Kotlin基本語法

基本用法

類型和函數(shù)定義

在Kotlin中,常量用val聲明,變量用var聲明,關(guān)鍵字在前面,類型在后面以冒號(hào):隔開,也可以省略直接賦值(自動(dòng)進(jìn)行類型推斷):

    var str: String = "hello"  //字符串
    var a: Int = 10  //整形
    var array: Array<Int> = arrayOf(1, 2, 3)  //數(shù)組
    var str2: String? = null  //可空字符串變量

定義一個(gè)函數(shù)接受兩個(gè) int 型參數(shù),返回值為 int :

    fun sum(a: Int, b: Int): Int {
        return a+b
    }

該函數(shù)只有一個(gè)表達(dá)式函數(shù)體以及一個(gè)可以推斷類型的返回值,因此可以簡(jiǎn)寫為:

    fun sum(a: Int, b: Int) = a + b

無返回值的函數(shù):

    fun printSum(a: Int, b: Int): Unit {   //一般,Unit可以省略不寫
        println("sum of $a and $b is ${a + b}")
    }
    //也可簡(jiǎn)寫為:
    fun printSum(a: Int, b: Int) = println("sum of $a and $b is ${a + b}")

函數(shù)參數(shù)可以設(shè)置默認(rèn)值,當(dāng)參數(shù)被忽略時(shí)會(huì)使用默認(rèn)值。這樣相比其他語言可以減少重載:

    fun sum(a: Int, b: Int = 1) = a + b

另外Kotlin還支持?jǐn)U展函數(shù)和中綴表達(dá)式,下面是簡(jiǎn)單的例子:

//擴(kuò)展函數(shù)
fun String.isLetter(): Boolean {
    return matches(Regex("^[a-z|A-Z]$"))
}

//中綴表達(dá)式只能是成員函數(shù)或者擴(kuò)展函數(shù),而且函數(shù)只有一個(gè)參數(shù)
infix fun String.isEqual(that: String): Boolean{
    return this == that
}

fun main(args: Array) {
    var s = "a".isLetter()
    var a = "aa" isEqual "bb"
}

String對(duì)象中本沒有判斷是否是字母的方法,在Java中我們一般會(huì)定義一些Utils方法,而在Kotlin中可以定義類的擴(kuò)展函數(shù)。
第二個(gè)例子是給String類定義了一個(gè)擴(kuò)展函數(shù),并且該拓展函數(shù)以中綴表達(dá)式表示,給予了開發(fā)者定義類似于關(guān)鍵字的權(quán)利。

再比如,我們一般這樣創(chuàng)建一個(gè)map對(duì)象:

val kv = mapOf("a" to 1, "b" to 2)

這里的to就是一個(gè)中綴表達(dá)式,定義如下:

public infix fun<A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

Pair就是Map中存的對(duì)象,所以你也可以這樣創(chuàng)建:

val kv = mapOf(Pair("a", 1), Pair("b", 2))

流程控制語句

流程控制語句是編程語言的核心之一。跟java類似,Kotlin有以下的語句。

  • 分支語句 if、when
  • 循環(huán)語句 for、while
  • 跳轉(zhuǎn)語句 return、break、continue、throw

if表達(dá)式

在Kotlin中,if是一個(gè)表達(dá)式,即它會(huì)返回一個(gè)值。if作為代碼塊時(shí),最后一行作為返回值。

fun max(x: Int, y: Int): Int {
    return if (x > y) {
        println("max is $x")
        x
    }else{
        println("max is $y")
        y
    }
}

注意:Kotlin中沒有java的中的2>1?2:1這樣的三元表達(dá)式。

when表達(dá)式

Kotlin中沒有java中的switch-case表達(dá)式,when表達(dá)式就是用來代替switch-case的。when會(huì)對(duì)所有的分支進(jìn)行檢查直到有一個(gè)條件滿足。但相比switch而言,when語句要更加的強(qiáng)大,靈活:

fun cases(obj: Any) {
    when (obj) {
        1 -> print("第一項(xiàng)")
        "hello" -> print("這個(gè)是字符串hello")
        is Long -> print("這是一個(gè)Long類型數(shù)據(jù)")
        !is String -> print("這不是String類型的數(shù)據(jù)")
        else -> print("else類似于Java中的default")
    }
}

如果我們有很多分支需要用相同的方式處理,則可以把多個(gè)分支條件放在一起,用逗號(hào)分隔,
也可以用任意表達(dá)式(而不只是常量)作為分支條件,也可以檢測(cè)一個(gè)值在 in 或者不在 !in 一個(gè)區(qū)間或者集合中:

when (x) {
        1 -> print("x == 1")
        2 -> print("x == 2")
        3, 4 -> print("x == 3 or x == 4")
        in 5..9 -> print("x in [5..9]")
        is Long -> print("x is Long")
        !in 10..20 -> print("x is outside the range")
        parseInt(s) -> print("s encodes x")
        else -> { 
            print("x is funny")
        }
    }

其他for,while,break,continue等,跟Java中用法基本一樣。

NPE空指針的處理

Kotlin的空指針處理相比于java有著極大的提高,可以說是不用擔(dān)心出現(xiàn)NullPointerException的錯(cuò)誤,kotlin對(duì)于對(duì)象為null的情況有嚴(yán)格的界定,編碼的階段就需要用代碼表明引用是否可以為null,為null的情況需要強(qiáng)制性的判斷處理。

可選型定義

非空類型

先說可選型的定義,當(dāng)我們?cè)贙otlin中定義一個(gè)變量時(shí),默認(rèn)就是非空類型的,當(dāng)你將一個(gè)非空類型置空的時(shí)候,編譯器會(huì)告訴你這不可行。例如:

var a: String=null //編譯器直接報(bào)錯(cuò) Null can not be value of a non null type string

這里注意:Kotlin的成員變量(全局變量)必須要初始化,甚至是基本數(shù)據(jù)類型都要手動(dòng)給一個(gè)初始值,局部變量可以不用初始化,上面的例子是成員變量的聲明。

編譯器直接表示a是一個(gè)non null type, 你不可以直接賦一個(gè)null值。對(duì)于我們java原住民來說聲明變量時(shí)如果不去賦值,編譯器會(huì)默認(rèn)賦null(除去基本數(shù)據(jù)類型),但在Kotlin這是不允許的。

可選型(可空類型)

在定義可選型的時(shí)候,我們只要在非空類型的后面添加一個(gè) ? 就可以了。

var b: String? = null  //ok

在使用可選型變量的時(shí)候,這個(gè)變量就有可能為空,所以在使用前我們應(yīng)該對(duì)其進(jìn)行空判斷(在 Java 中我們經(jīng)常這樣做),這樣往往帶來帶來大量非業(yè)務(wù)相關(guān)的工作,這些空判斷代碼本身沒有什么實(shí)際意義,并且讓代碼的可讀性和簡(jiǎn)潔性帶來了巨大的挑戰(zhàn)。
Kotlin 為了解決這個(gè)問題,它并不允許我們直接使用一個(gè)可選型的變量去調(diào)用方法或者屬性。例如:

var b: String? = "abc"
val l = b.length // compilation error

你可以和 Java 中一樣,在使用變量之前先進(jìn)行空判斷,然后再去調(diào)用。如果使用這種方法,那么空判斷是必須的。

val length = if (b != null) b.length else -1

注意: 如果你定義的變量是全局變量,即使你做了空判斷,依然不能使用變量去調(diào)用方法或者屬性。
Kotlin 為可選型提供了一個(gè)安全調(diào)用操作符 ?.,使用該操作符可以方便調(diào)用可選型的方法或者屬性。

var length = b?.length  //length類型是可選型Int?

Kotlin還提供了一個(gè)強(qiáng)轉(zhuǎn)的操作符!!,這個(gè)操作符能夠強(qiáng)行調(diào)用變量的方法或者屬性,而不管這個(gè)變量是否為空,如果這個(gè)時(shí)候該變量為空時(shí),那么就會(huì)發(fā)生 NPE。所以如果不想繼續(xù)陷入NPE 的困境無法自拔,請(qǐng)盡量不要選用該操作符。

var length = b!!.length  //可能會(huì)報(bào)NPE錯(cuò)誤(Caused by: kotlin.KotlinNullPointerException)
Elvis 操作符?:

回到?.的調(diào)用上來,這個(gè)調(diào)用方式存在一個(gè)讓人不安的處理,就是在變量為null的情況下,會(huì)直接返回null,這樣空指針的隱患還在。

var b: String? = "abc"
var length : Int = b?.length //報(bào)錯(cuò)類型不匹配 Int? 和 Int

修正的話,需要通過if判斷來進(jìn)行判空處理

val length = if (b != null) b?.length else -1

這種寫法可以簡(jiǎn)化成Elvis 操作符?:,例如

val length = b?.length ?: -1

b?.length ?: -1if (b != null) b.length else -1 完全等價(jià)的。
其實(shí)你還可以在?: 后面添加任何表達(dá)式,比如你可以在后面用returnthrow(在 Kotlin 中它們都是表達(dá)式)。

Kotlin函數(shù)式編程

下面是維基百科上對(duì)于函數(shù)式編程的定義:

函數(shù)式編程(英語:functional programming)或稱函數(shù)程序設(shè)計(jì),又稱泛函編程,是一種編程范型,它將電腦運(yùn)算視為數(shù)學(xué)上的函數(shù)計(jì)算,并且避免使用程序狀態(tài)以及易變對(duì)象。函數(shù)編程語言最重要的基礎(chǔ)是λ演算(lambda calculus)。而且λ演算的函數(shù)可以接受函數(shù)當(dāng)作輸入(引數(shù))和輸出(傳出值)。

下面是關(guān)于高階函數(shù)的定義:

在數(shù)學(xué)和計(jì)算機(jī)科學(xué)中,高階函數(shù)是至少滿足下列一個(gè)條件的函數(shù):接受一個(gè)或多個(gè)函數(shù)作為輸入,輸出一個(gè)函數(shù)

f(x) = x^2
g(x) = x + 1
g(f(x))就是一個(gè)高階函數(shù)

不難推斷出函數(shù)式編程最重要的基礎(chǔ)是高階函數(shù)。也就是支持函數(shù)可以接受函數(shù)當(dāng)作輸入(引數(shù))和輸出(傳出值)。

函數(shù)式編程的精髓在于函數(shù)本身。在函數(shù)式編程中函數(shù)是第一等公民,與其他數(shù)據(jù)類型一樣,處于平等地位,可以賦值給其他變量,也可以作為參數(shù),傳入另一個(gè)函數(shù),或者作為別的函數(shù)的返回值。

從Lambda表達(dá)式說起

Lambda 表達(dá)式俗稱匿名函數(shù),熟悉Java的大家應(yīng)該也明白這是個(gè)什么概念。Kotlin 的 Lambda表達(dá)式更“純粹”一點(diǎn), 因?yàn)樗钦嬲袻ambda抽象為了一種類型,而 Java 8 的 Lambda 只是單方法匿名接口實(shí)現(xiàn)的語法糖罷了。

        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(v.getContext(), "Click", Toast.LENGTH_SHORT).show();
            }
        });

這可以轉(zhuǎn)換為Kotlin代碼(使用Anko庫函數(shù)toast):

view.setOnClickListener(object : View.onClickListener {
    override fun onClick(v: View) {
        toast("Click")
    }
}

Lambda表達(dá)式由箭頭左側(cè)函數(shù)的參數(shù)(小括號(hào)里面的內(nèi)容)定義的,將值返回給箭頭右側(cè)。在這里,將得到的View返回給Unit,這樣可以對(duì)上述代碼稍做簡(jiǎn)化:

view.setOnClickListener({ view -> toast("Click")})

在定義函數(shù)時(shí),必須在箭頭左邊使用中括號(hào)并指定參數(shù)值,而函數(shù)執(zhí)行代碼在右邊。
如果左邊沒有使用參數(shù),甚至可以省去左邊部分:

view.setOnClickListener({ toast("Click") })

如果被執(zhí)行的函數(shù)是當(dāng)前函數(shù)的最后一個(gè)參數(shù)的話,也可以將這個(gè)作為參數(shù)的函數(shù)放到圓括號(hào)外面:

view.setOnClickListener() { toast("Click") }

最后,如果函數(shù)是唯一的參數(shù),還可以去掉原來的小括號(hào):

view.setOnClickListener { toast("Click") }

比起起初的Java代碼,目前的代碼量小于原來的五分之一,且更容易理解。令人印象深刻。Anko給出一個(gè)(本質(zhì)上說是函數(shù)名的)簡(jiǎn)化版本,由之前展示過的擴(kuò)展函數(shù)組成,該函數(shù)也由上述形式實(shí)現(xiàn):

view.onClick { toast("Click") }

以上是Java中的接口映射到Kotlin中Lambda表達(dá)式實(shí)例。

Kotlin中Lambda表達(dá)式

下面詳細(xì)介紹Kotlin中Lambda表達(dá)式。

var add = {x: Int, y: Int -> x + y}

我們可以這樣使用:

fun main(args: Array) {
val add = {x: Int, y: Int -> x + y}
add.invoke(1, 2)
//或者簡(jiǎn)寫為
add(1, 2)
}

它可以像函數(shù)一樣使用()調(diào)用,在kotlin中操作符是可以重載的,()操作符對(duì)應(yīng)的就是類的重載函數(shù)invoke()。
還可以想下面這樣定義一個(gè)變量:

val numFun: (a: Int, b: Int) -> Int

它不是一個(gè)普通的變量,它必須指向一個(gè)函數(shù),并且函數(shù)簽名必須一致:

fun main(args: Array) {
    val sumLambda = {a: Int, b: Int -> a + b}
    var numFun: (a: Int, b: Int) -> Int
    numFun = {a: Int, b: Int -> a + b}
    numFun = sumLambda
    numFun = ::sum
    numFun(1,2)
}

fun sum(a: Int, b: Int): Int {
    return a + b
}

可以看到這個(gè)變量可以等于一個(gè)lambda表達(dá)式,也可以等于另一個(gè)lambda表達(dá)式變量,還可以等于一個(gè)普通函數(shù),但是在函數(shù)名前需要加上(::)來獲取函數(shù)引用,有點(diǎn)類似于C++中的函數(shù)指針。

我們還可以將一個(gè)函數(shù)傳遞給另一個(gè)函數(shù),比如:

fun <T> doMap(list: List<T>, function: (it: T) -> Unit) {
        for (item in list) {
            function(item)
        }
    }

第一個(gè)參數(shù)是一個(gè)List,第二個(gè)參數(shù)是一個(gè)函數(shù),目的就是將List中的每一個(gè)元素都執(zhí)行一次第二個(gè)函數(shù)。使用方法如下:

val strList = listOf("a" ,"b", "c", "d")
doMap(strList, {item -> println("item: $item, ") })

第二個(gè)參數(shù)直接傳進(jìn)去了一個(gè)lambda表達(dá)式,當(dāng)然也可以傳一個(gè)函數(shù)引用:

doMap(strList, ::printLetter)
fun printLetter(item: String) {
    println("item: $item, ")
}

效果和上面的代碼一樣。

另外Kotlin還支持局部函數(shù)和函數(shù)作為返回值,看下面的代碼:

fun main(args: Array) {
        val addResult = lateAdd(2, 4)
        addResult()
    }
    //局部函數(shù),函數(shù)引用
    fun lateAdd(a: Int, b: Int): ()->Int {
        fun add(): Int {
            return a + b
        }
        return ::add
    }

在lateAdd內(nèi)部定義了一個(gè)局部函數(shù),最后返回了該局部函數(shù)的引用,對(duì)結(jié)果使用()操作符拿到最終的結(jié)果,達(dá)到延遲計(jì)算的目的。

基于以上函數(shù)式編程的特性,Kotlin可以像RxJava一樣很方便的進(jìn)行響應(yīng)式編程,比如:
計(jì)算二維數(shù)組每一個(gè)子列表的乘積,再求和,可以這樣寫:

            val list = listOf(
                    listOf(1, 2, 3),
                    listOf(4, 5, 6),
                    listOf(7, 8, 9)
            )
            list.map {it: List<Int> -> it.fold(1){ a, b -> a * b } }  
                .fold(0){ a, b -> a + b }
                .log()

Kotlin函數(shù)式編程中常用函數(shù) forEach,filter,map,reduce(fold)

forEach

遍歷函數(shù),循環(huán)遍歷所有元素,元素是it,可對(duì)每個(gè)元素進(jìn)行相關(guān)操作;
假設(shè)我們現(xiàn)在需要打印列表中每個(gè)的名字:

val nameList = arrayOf("Jim","Tom", "Marry","Lin")
nameList.forEach { println(it) }

filter

過濾函數(shù)將用戶給定的布爾邏輯作用于集合,返回由原集合中符合條件的元素組合的一個(gè)子集。假設(shè)一個(gè)邏輯,將數(shù)組中是2的倍數(shù)的數(shù)篩選出來,使用Kotlin和Java的實(shí)現(xiàn)做一個(gè)簡(jiǎn)單的對(duì)比。

int[] all = {1, 2, 3, 4, 5, 6, 7, 8, 9};
List<Integer> filters = new ArrayList<>();
for (int a : all) {
    if (a % 2 == 0) {
        filters.add(a);
    }
}

Kotlin代碼的實(shí)現(xiàn):

val all = arrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
val filters = all.filter { it % 2 == 0 }

Kotlin 還提供一系列類似的過濾函數(shù):

  • filterIndexed, 同 filter,不過在邏輯判斷的方法塊中可以拿到當(dāng)前item的index
  • filterNot,與filter相反,只返回不符合條件的元素組合
  • 針對(duì) Map 類型數(shù)據(jù)集合,提供了 filterKeys 和 filterValues 方法,方便只做key或者value的判斷

map

映射函數(shù)也是一個(gè)高階函數(shù),將一個(gè)集合經(jīng)過一個(gè)傳入的變換函數(shù)映射成另外一種集合。
假設(shè)我們現(xiàn)在需要將一系列的名字的字符串長(zhǎng)度保存到另一個(gè)數(shù)組:
使用Java實(shí)現(xiàn)過程如下:

String[] names = {"James", "Tommy", "Jim", "Andy"};
int[] namesLength = new int[names.length];
for (int i = 0; i < names.length ; i ++) {
    namesLength[i] = names[i].length();
}

使用Kotlin實(shí)現(xiàn)如下:

val names = arrayOf("James", "Tommy", "Jim", "Andy");
val namesLength = names.map { it.length }

同 filter 相似,Kotlin 也提供的 mapIndexed 的類似方法方便使用,針對(duì) Map 類型的集合也有 mapKeys 和 mapValues 的封裝。

reduce

歸納函數(shù)將一個(gè)數(shù)據(jù)集合的所有元素通過傳入的操作函數(shù)實(shí)現(xiàn)數(shù)據(jù)集合的積累疊加。同fold一樣,不過fold可以帶初始值。
假設(shè)我們現(xiàn)在需要計(jì)算一系列的名字的總字符串長(zhǎng)度,使用Java實(shí)現(xiàn)如下:

String[] names = {"James", "Tommy", "Jim", "Andy"};
int totalLength = 0;
for (int i = 0; i < names.length ; i ++) {
    totalLength =+ names[i].length();
}

使用Kotlin實(shí)現(xiàn)如下:

val names = arrayOf("James", "Tommy", "Jim", "Andy");
val namesLength = names.map { it.length }.reduce { r, s -> r + s }

關(guān)于Anko

Jetbrains給Android帶來的不僅是Kotlin,還有Anko。從Anko的官方說明來看這是一個(gè)雄心勃勃的要代替XML寫Layout的新的開發(fā)方式。Anko最重要的一點(diǎn)是引入了DSL(Domain Specific Language 領(lǐng)域相關(guān)語言)的方式開發(fā)Android界面布局。當(dāng)然,本質(zhì)是代碼實(shí)現(xiàn)布局。不過使用Anko完全不用經(jīng)歷Java純代碼寫Android的痛苦。

然而,這個(gè)不是我們能在這個(gè)庫中得到的唯一一個(gè)功能。Anko包含了很多的非常有幫助的函數(shù)和屬性來避免讓你寫很多的模版代碼。多看看Anko源碼的實(shí)現(xiàn)方式對(duì)學(xué)習(xí)Kotlin語言是非常有幫助的。

Anko提供了非常簡(jiǎn)單的DSL來處理異步任務(wù),它能夠滿足大部分的需求。它提供了一個(gè)基本的doAsync函數(shù)用于在子線程執(zhí)行代碼,可以選擇通過調(diào)用uiThread的方式回到主線程。在子線程中執(zhí)行請(qǐng)求如下這么簡(jiǎn)單:

        doAsync {
            Thread.sleep(3000)   //耗時(shí)操作
            uiThread {
                toast("background task finish")
            }
        }

UIThread有一個(gè)很不錯(cuò)的一點(diǎn)就是可以依賴于調(diào)用者。如果它是被一個(gè)Activity調(diào)用的,那么如果activity.isFinishing()返回true,則uiThread不會(huì)執(zhí)行,這樣就不會(huì)在Activity銷毀的時(shí)候遇到崩潰的情況了。

Kotlin中的類和對(duì)象

Kotlin 的類特性

Kotlin中的類與接口和Java中的有些區(qū)別:

  • Kotlin中接口可以包含屬性申明
  • Kotlin的類申明,默認(rèn)是final和public的
  • Kotlin的嵌套類并不是默認(rèn)在內(nèi)部的。它們不包含外部類的隱式引用
  • Kotlin的構(gòu)造函數(shù),分為主構(gòu)造函數(shù)和次構(gòu)造函數(shù)
  • Kotlin中可以使用data關(guān)鍵字來申明一個(gè)數(shù)據(jù)類
  • Kotlin中可以使用object關(guān)鍵字來表示單例對(duì)象、伴生對(duì)象等

Kotlin類的成員可以包含:

  • 構(gòu)造函數(shù)和初始化塊
  • 屬性
  • 函數(shù)
  • 嵌套類和內(nèi)部類
  • 對(duì)象聲明

構(gòu)造函數(shù)

在Kotlin中可以有一個(gè)主構(gòu)造函數(shù),一個(gè)或者多個(gè)次構(gòu)造函數(shù)。

主構(gòu)造函數(shù)

主構(gòu)造函數(shù)直接跟在類名后面,如下:

open class Person constructor(var name: String, var age: Int) : Any() {
    ...
}

主構(gòu)造函數(shù)中的屬性可以是可變的(var)也可以是不變的(val)。如果主構(gòu)造函數(shù)沒有任何注解或者可見性修飾符,可以省略constructor關(guān)鍵字(屬性默認(rèn)是val),而且Koltin中的類默認(rèn)就是繼承Any的,也可以省略。所以可以簡(jiǎn)化成如下:

open class Person(name: String, age: Int) {
    ...
}

主構(gòu)造函數(shù)不能包括任何代碼。初始化代碼可以放到以init關(guān)鍵字作為前綴的初始化塊中:

open class Person constructor(var name: String, var age: Int){
    init {
        println("Student(name = $name, age = $age) created")
    }
}

主構(gòu)造函數(shù)的參數(shù)可以在初始化塊中使用,也可以在類體內(nèi)申明的屬性初始化器中使用。

次構(gòu)造函數(shù)

我們也可以在類體中使用constructor申明次構(gòu)造函數(shù),次構(gòu)造函數(shù)的參數(shù)不能使用val或者var申明。

class Student public constructor(name: String, age: Int) : Person(name, age) {
    var grade: Int = 1

    init {
        println("Student(name = $name, age = $age) created")
    }

    constructor(name: String, age: Int, grade: Int) : this(name, age){
        this.grade = grade
    }
}

如果一個(gè)類有主構(gòu)造函數(shù),那么每個(gè)次構(gòu)造函數(shù)都需要委托給主構(gòu)造函數(shù),委托到同一個(gè)類的另一個(gè)構(gòu)造函數(shù)可以使用this關(guān)鍵字,如上面這個(gè)例子this(name, age)

如果一個(gè)非抽象類沒有申明任何構(gòu)造函數(shù)(包括主或者次),它會(huì)生成一個(gè)不帶參數(shù)的主構(gòu)造函數(shù)。構(gòu)造函數(shù)的可見性是public。

抽象類

下面就是一個(gè)抽象類,類需要用abstract修飾,其中也有抽象方法,跟Java有區(qū)別的是Kotlin的抽象類可以包含抽象屬性:

abstract class Person(var name: String, var age: Int){

    abstract var addr: String
    abstract val weight: Float

    abstract fun doEat()
    abstract fun doWalk()

    fun doSwim() {
        println("I am Swimming ... ")
    }

    open fun doSleep() {
        println("I am Sleeping ... ")
    }
}

在上面這個(gè)抽象類中,有doEat和doWalk抽象函數(shù),同時(shí)還有具體的實(shí)現(xiàn)函數(shù)doSwim,在Kotlin中如果類和方法沒有修飾符的化,默認(rèn)就是final的。這個(gè)跟Java是不一樣的。所以doSwim其實(shí)是final的,也就是說Person的子類不能覆蓋這個(gè)方法。如果一個(gè)類或者類的方法想要設(shè)計(jì)成被覆蓋(override)的,那么就需要加上open修飾符。下面是一個(gè)Person的子類:

class Teacher(name: String, age: Int) : Person(name, age) {

    override var addr:String = "Guangzhou"
    override val weight: Float = 100.0F

    override fun doEat() {
        println("Teacher is Eating ... ")
    }

    override fun doWalk() {
        println("Teacher is Walking ... ")
    }

    override fun doSleep() {
        super.doSleep()
        println("Teacher is Sleeping ... ")
    }
    
//    編譯錯(cuò)誤,doSwim函數(shù)默認(rèn)是final的,需要加上open修飾符才能重寫
//    override fun doSwim() {
//        println("Teacher is Swimming ... ")
//    }

}

如果子類覆蓋了父類的方法或者屬性,需要使用override關(guān)鍵字修飾。如果子類沒有實(shí)現(xiàn)父類的抽象方法,或者沒有給抽象屬性賦值,則必須把子類也定義成抽象類。

抽象函數(shù)的特征有以下幾點(diǎn):

  • 抽象函數(shù)、抽象屬性必須使用abstract關(guān)鍵字修飾
  • 抽象函數(shù)或者抽象類不用手動(dòng)添加open關(guān)鍵字,默認(rèn)就是open類型
  • 抽象函數(shù)沒有具體的實(shí)現(xiàn),抽象屬性不用賦值
  • 含有抽象函數(shù)或者抽象屬性的類,必須要使用abstract關(guān)鍵字修飾。抽象類可以有具體實(shí)現(xiàn)的函數(shù),這樣的函數(shù)默認(rèn)是final的(不能被覆蓋)。如果想要被覆蓋,需要手工加上open關(guān)鍵字

接口

和Java類似,Kotlin使用interface作為接口的關(guān)鍵詞,Kotlin 的接口與 Java 8 的接口類似。與抽象類相比,他們都可以包含抽象的方法以及方法的實(shí)現(xiàn):

interface ProjectService {
    val name: String
    val owner: String
    fun save(project: Project)
    fun print() {
        println("I am project")
    }
}

接口是沒有構(gòu)造函數(shù)的,和繼承一樣,我們也是使用冒號(hào):來實(shí)現(xiàn)一個(gè)接口,如果要實(shí)現(xiàn)多個(gè)接口,使用逗號(hào),分開。

繼承

在Kotlin中,所有的類都默認(rèn)繼承Any這個(gè)類,Any并不是跟Java的java.lang.Object一樣,因?yàn)樗挥衑quals(),hashCode()和toString()這三個(gè)方法。
除了抽象類和接口默認(rèn)是可被繼承外,其他類默認(rèn)是不可以被繼承的(相當(dāng)于默認(rèn)都帶有final修飾符)。而類中的方法也是默認(rèn)不可以被繼承的。

  • 如果你要繼承一個(gè)類,你需要使用open關(guān)鍵字修飾這個(gè)類
  • 如果你要重寫一個(gè)類的某個(gè)方法,這個(gè)方法也需要使用open關(guān)鍵字修飾
open class Base(type: String){
    open fun canBeOverride() {
        println("I can be override.")
    }

    fun cannotBeOverride() {
        println("I can't be override.")
    }
}

class SubClass(type: String) :Base(type){
    override fun canBeOverride() {
        super.canBeOverride()
        println("Override!!!")
    }

//    override fun cannotBeOverride() {  編譯錯(cuò)誤。
//        super.cannotBeOverride()
//        println("Override!!!")
//    }
}

如果Base有構(gòu)造函數(shù),那么子類的主構(gòu)造函數(shù)必須繼承。

單例模式

Kotlin中沒有靜態(tài)屬性和方法,但是也提供了實(shí)現(xiàn)類似單例的功能,使用object關(guān)鍵字聲明一個(gè)object對(duì)象。

object StringUtils{
    val separator: String = """\"""
    fun isDigit(value: String): Boolean{
        for (c in value) {
            if (!c.isDigit()) {
                return false
            }
        }
        return true
    }
}

fun main(args: Array<String>) {
    println("C:${StringUtils.separator}Users${StringUtils.separator}Denny") //打印c:\Users\Denny
    println(StringUtils.isDigit("12321231231"))  //打印true
}

我們反編譯后可以知道StringUtils轉(zhuǎn)成了Java代碼:

public final class StringUtils {
   @NotNull
   private static final String separator = "\\";
   public static final StringUtils INSTANCE;

   @NotNull
   public final String getSeparator() {
      return separator;
   }

   public final boolean isDigit(@NotNull String value) {
      Intrinsics.checkParameterIsNotNull(value, "value");
      String var4 = value;
      int var5 = value.length();

      for(int var3 = 0; var3 < var5; ++var3) {
         char c = var4.charAt(var3);
         if (!Character.isDigit(c)) {
            return false;
         }
      }

      return true;
   }

   static {
      StringUtils var0 = new StringUtils();
      INSTANCE = var0;
      separator = "\\";
   }
}

object對(duì)象只能通過對(duì)象名字來訪問,不能使用構(gòu)造函數(shù)。
我們?cè)贘ava中通常會(huì)寫一些Utils類,這樣的類我們?cè)贙otlin中就可以直接使用object對(duì)象。

伴生對(duì)象(companion object)

Kotlin中還提供了伴生對(duì)象 ,跟java中的static關(guān)鍵字有些相似,用companion object關(guān)鍵字聲明:

class DataProcessor {
    fun process() {
        println("Process Data")
    }
    
    companion object StringUtils {    //StringUtils可以省略
        val TAG = "DataProcessor"
        fun isEmpty(s: String): Boolean {
            return s.isEmpty()
        }
    }

}

一個(gè)類只能有一個(gè)伴生對(duì)象,伴生對(duì)象的初始化是在相應(yīng)的類被加載解析時(shí),即使伴生對(duì)象的成員看起來像其他語言的靜態(tài)成員,在運(yùn)行時(shí)他們?nèi)匀皇钦鎸?shí)的對(duì)象的實(shí)例成員。

另外,如果想使用Java中的靜態(tài)成員和靜態(tài)方法的話,我們可以用:

  • @JvmField注解:生成與該屬性相同的靜態(tài)字段
  • @JvmStatic注解:在單例對(duì)象和伴生對(duì)象中生成對(duì)應(yīng)的靜態(tài)方法

嵌套類(Nested Class)

class NestedClassesDemo {
    class Outer {
        private val zero: Int = 0
        val one: Int = 1

        class Nested {
            fun getTwo() = 2
            class Nested1 {
                val three = 3
                fun getFour() = 4
            }
        }
    }
}

我們可以這樣來訪問內(nèi)部類:

val one = NestedClassesDemo.Outer().one
val two = NestedClassesDemo.Outer.Nested().getTwo()
val three = NestedClassesDemo.Outer.Nested.Nested1().thre

但是普通的嵌套類,并不會(huì)持有外部類的引用,如果要持有外部類的引用,那么我們需要把嵌套類標(biāo)記為內(nèi)部類,使用inner關(guān)鍵字即可:

class NestedClassesDemo {
class Outer {
        private val zero: Int = 0
        val one: Int = 1

        inner class Inner {
            fun accessOuter() = {
                println(zero) // works
                println(one) // works
            }
        }
}

匿名內(nèi)部類(Annonymous Inner Class)

匿名內(nèi)部類,就是沒有名字的內(nèi)部類。既然是內(nèi)部類,那么它自然也是可以訪問外部類的變量的。
我們使用對(duì)象表達(dá)式創(chuàng)建一個(gè)匿名內(nèi)部類實(shí)例:

        button.setOnClickListener(object : View.OnClickListener{
            override fun onClick(v: View) {
                println("Clicked")
            }
        })

如果對(duì)象實(shí)例是一個(gè)函數(shù)接口(Java中只有一個(gè)抽象方法的接口),可以使用lambda表達(dá)式:

        button.setOnClickListener({view -> println("Clicked")})

對(duì)象表達(dá)式(Object expressions)

如果父類型有構(gòu)造函數(shù),則必須將構(gòu)造函數(shù)的參數(shù)賦值;多個(gè)父類通過“,”分割:

open class A(x: Int) {  
    public open val y: Int = x  
}  
  
interface B {...}  
  
val ab: A = object : A(1), B {  
    override val y = 15  
}  

有時(shí),只需要一個(gè)對(duì)象表達(dá)式,不想繼承任何的父類型,實(shí)現(xiàn)如下:

val adHoc = object {  
  var x: Int = 0  
  var y: Int = 0  
}  
print(adHoc.x + adHoc.y)  

對(duì)象表達(dá)式和對(duì)象聲明的區(qū)別

  • 對(duì)象表達(dá)式(Object expressions),在它們使用的地方,是立即(immediately)執(zhí)行(或初始化)
  • 對(duì)象聲明(Object declarations),會(huì)延遲(lazily)初始化;但第一次訪問該對(duì)象時(shí)才執(zhí)行
  • 伴生對(duì)象(Companion Objects),當(dāng)外部類被加載時(shí)初始化,跟Java靜態(tài)代碼框初始化相似

總結(jié)

以上簡(jiǎn)單介紹了一下Kotlin的基本語法,函數(shù)式編程和Kotlin類和對(duì)象的入門知識(shí)。由于這只是一篇入門的文章,對(duì)于Kolin中更多的知識(shí)點(diǎn),比如泛型,協(xié)程,與Java的互調(diào)等更深層的內(nèi)容并沒有介紹。各位讀者如果想要深入學(xué)習(xí)Kotlin編程,可以參考下面的Kotlin學(xué)習(xí)進(jìn)行學(xué)習(xí)。


  • 路漫漫其修遠(yuǎn)兮,吾將上下而求索。
  • 怕什么真理無窮,進(jìn)一寸有一寸的歡喜。

Kotlin學(xué)習(xí)資料

  1. Kotlin基礎(chǔ)教程
  2. 《Kotlin for android developers》中文版翻譯
  3. Kotlin的類和對(duì)象
  4. Kotlin面向?qū)ο蠊P記
  5. Object Expressions and Declarations
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容