Kotlin 入門(一)

621發表於2019-02-28

基本資料型別

數字

Type Bit Width
Double 64
Float 32
Long 32
Int 32
Short 16
Byte 8

字面常量

  • 十進位制:123
    • Long型別用大寫L 標記:123L
  • 十六進位制:0x0F
  • 二進位制:0b0001011
  • 預設Double:123.5、123.5e10
  • Float 用f或者F標記:123.5f

數字字面值中的下劃線(自1.1起)

val oneMillion = 1_000_000
val creditCardNumber = 1234_5678_9012_3456L
val socialSecurityNumber = 999_99_9999L
val hexBytes = 0xFF_EC_DE_5E
val bytes = 0b11010010_01101001_10010100_10010010
複製程式碼

注意:

  1. 對於數字沒有隱式擴充轉換(如Java中的int可以隱式轉換為long
  2. kotlin 不支援八進位制
  3. Kotlin 中字元不是數字

字元

字元用 Char 型別表示。它們不能直接當作數字

fun check(c: Char) {
    if (c == 1) { // 錯誤:型別不相容
        // ……
    }
}
複製程式碼

每種數字型別都支援如下轉化

  • toByte(): Byte
  • toShort(): Short
  • toInt(): Int
  • toLong(): Long
  • toFloat(): Float
  • toDouble(): Double
  • toChar(): Char

布林

布林用 Boolean型別表示,它有兩個值:truefalse
若需要可空引用布林會被裝箱。
內建的布林運算有:

  • || – 短路邏輯或
  • && – 短路邏輯與
  • ! – 邏輯非

陣列

控制流

if表示式

在 Kotlin 中,if是一個表示式,即它會返回一個值。 因此就不需要三元運算子(條件 ? 然後 : 否則),因為普通的 if 就能勝任這個角色。

// 傳統用法
var max = a 
if (a < b) max = b

// With else 
var max: Int
if (a > b) {
    max = a
} else {
    max = b
}
 
// 作為表示式
val max = if (a > b) a else b
複製程式碼

if的分支可以是程式碼塊,最後的表示式作為該塊的值:

val max = if (a > b) {
    print("Choose a")
    a
} else {
    print("Choose b")
    b
}
複製程式碼

如果你使用 if 作為表示式而不是語句(例如:返回它的值或者把它賦給變數),該表示式需要有else 分支。

when

when取代了java switch 操作符。其最簡單的形式如下

when (x) {
    1 -> print("x == 1")
    2 -> print("x == 2")
    else -> { // 注意這個塊
        print("x is neither 1 nor 2")
    }
}

複製程式碼

if作為表示式一樣, when既可以被當做表示式使用也可以被當做語句使用,如果when 作為一個表示式使用,必須有else分支, 除非編譯器能夠檢測出所有的可能情況都已經覆蓋了。

while 迴圈

while (x > 0) {
    x--
}

do {
  val y = retrieveData()
} while (y != null) // y 在此處可見
複製程式碼

迴圈中的 Breakcontinue

  • return預設從最直接包圍它的函式或者匿名函式返回。
  • break終止最直接包圍它的迴圈。
  • continue繼續下一次最直接包圍它的迴圈。

函式

函式的宣告

Kotlin 中的函式使用 fun 關鍵字宣告:

fun double(x: Int): Int {
    return 2 * x
}
複製程式碼

定義函式

  1. 帶有兩個 Int 引數、返回 Int 的函式:
fun sum(a: Int, b: Int): Int {
    return a + b
}
複製程式碼
  1. 將表示式作為函式體、返回值型別自動推斷的函式:
fun sum(a: Int, b: Int) = a + b
複製程式碼
  1. 函式返回無意義的值,Unit是一種只有一個值——Unit 的型別。這個值不需要顯式返回:
fun printSum(a: Int, b: Int): Unit {
    println("sum of $a and $b is ${a + b}")
}
複製程式碼

注意:

  1. 當一個函式有大量的引數或預設引數時,可以通過命名引數來呼叫函式;
  2. 當一個函式呼叫混用位置引數與命名引數時,所有位置引數都要放在第一個命名引數之前
  3. 在呼叫Java 函式時不能使用命名引數語法,因為Java 位元組碼並不總是保留函式引數的名稱;

函式的作用域

Kotlin 中函式可以在檔案頂層宣告,不需要像Java一樣建立一個類來儲存一個函式。此外除了頂層函式,Kotlin 中函式也可以宣告在區域性作用域、作為成員函式以及擴充套件函式。

區域性函式

Kotlin 支援區域性函式,即一個函式在另一個函式內部:

fun count(){
    var count =0;
    fun innerCount(){
        print(count)
    }
}
複製程式碼

區域性函式可以訪問外部函式(即閉包)的區域性變數。

成員函式

成員函式是在類或物件內部定義的函式:

class Sample() {
    fun foo() { print("Foo") }
}
複製程式碼

成員函式以點表示法呼叫:

Sample().foo() // 建立類 Sample 例項並呼叫 foo
複製程式碼

泛型函式

函式可以有泛型引數,通過在函式名前使用尖括號指定:

fun <T> singletonList(item: T): List<T> { …… }
複製程式碼

行內函數

擴充套件函式

擴充套件一個類的新功能而無需繼承該類或使用像裝飾者這樣的任何型別的設計模式。
宣告一個擴充套件函式,我們需要用一個 接收者型別 也就是被擴充套件的型別來作為他的字首。

高階函式與Lambda表示式

高階函式是將函式用作引數或返回值的函式。

函式型別

  • 所有函式型別都有一個圓括號括起來的引數型別列表以及一個返回型別:(A, B) -> C 表示接受型別分別為 A 與 B 兩個引數並返回一個 C 型別值的函式型別。 引數型別列表可以為空,如 () -> A。Unit 返回型別不可省略。
  • 函式型別可以有一個額外的接收者型別,它在表示法中的點之前指定: 型別 A.(B) -> C 表示可以在 A 的接收者物件上以一個 B 型別引數來呼叫並返回一個 C 型別值的函式。 帶有接收者的函式字面值通常與這些型別一起使用。
  • 掛起函式屬於特殊種類的函式型別,它的表示法中有一個suspend修飾符 ,例如 suspend () -> Unit或者 suspend A.(B) -> C(協程相關)。

函式型別表示法可以選擇性地包含函式的引數名:(x: Int, y: Int) -> Point。 這些名稱可用於表明引數的含義。

函式型別例項化

  • 使用函式字面值的程式碼塊,採用以下形式之一:
    • lambda 表示式: { a, b -> a + b },
    • 匿名函式: fun(s: String): Int { return s.toIntOrNull() ?: 0 }

帶有接收者的函式字面值可用作帶有接收者的函式型別的值

  • 使用已有宣告的可呼叫引用:
    • 頂層、區域性、成員、擴充套件函式:::isOdd、 String::toInt
    • 頂層、成員、擴充套件屬性:List<Int>::size
    • 建構函式:::Regex

這包括指向特定例項成員的繫結的可呼叫引用:foo::toString

  • 使用實現函式型別介面的自定義類的例項:
class IntTransformer: (Int) -> Int {
    override operator fun invoke(x: Int): Int = TODO()
}

val intFunction: (Int) -> Int = IntTransformer()
複製程式碼

如果有足夠資訊,編譯器可以推斷變數的函式型別:

val a = { i: Int -> i + 1 }
複製程式碼

帶與不帶接收者的函式型別非字面值可以互換,其中接收者可以替代第一個引數,反之亦然。例如,(A, B) -> C 型別的值可以傳給或賦值給期待 A.(B) -> C 的地方,反之亦然:

val repeatFun: String.(Int) -> String = { times -> this.repeat(times) }
val twoParameters: (String, Int) -> String = repeatFun // OK

fun runTransformation(f: (String, Int) -> String): String {
    return f("hello", 3)
}
val result = runTransformation(repeatFun) // OK
複製程式碼

請注意,預設情況下推斷出的是沒有接收者的函式型別,即使變數是通過擴充套件函式引用來初始化的。 如需改變這點,請顯式指定變數型別。

Lambda 表示式

lambda 表示式與匿名函式是“函式字面值”,即未宣告的函式, 但立即做為表示式傳遞,考慮下面的例子:

max(strings, { a, b -> a.length < b.length })
複製程式碼

函式 max 是一個高階函式,它接受一個函式作為第二個引數。 其第二個引數是一個表示式,它本身是一個函式,即函式字面值,它等價於以下命名函式:

fun compare(a: String, b: String): Boolean = a.length < b.length
複製程式碼
Lambda 表示式語法

Lambda 表示式的完整語法形式如下:

val sum = { x: Int, y: Int -> x + y }
複製程式碼

lambda表示式總是括在花括號中, 完整語法形式的引數宣告放在花括號內,並有可選的型別標註, 函式體跟在一個->符號之後。如果推斷出的該lambda的返回型別不是 Unit,那麼該lambda 主體中的最後一個(或可能是單個)表示式會視為返回值。
如果我們把所有可選標註都留下,看起來如下:

val sum: (Int, Int) -> Int = { x, y -> x + y }
複製程式碼

將 lambda 表示式傳給最後一個引數:
在 Kotlin 中有一個約定:如果函式的最後一個引數接受函式,那麼作為相應引數傳入的 lambda 表示式可以放在圓括號之外:

val product = items.fold(1) { acc, e -> acc * e }
複製程式碼

如果該lambda表示式是呼叫時唯一的引數,那麼圓括號可以完全省略:

run { println("...") }
複製程式碼

尾遞迴函式

函式可以有泛型引數,通過在函式名前使用尖括號指定:

fun <T> singletonList(item: T): List<T> { …… }
複製程式碼

類與物件

類的成員

  • 建構函式與初始化塊
  • 函式
  • 屬性
  • 巢狀類與內部類
  • 物件宣告

建構函式

Kotlin 中使用關鍵字 class 宣告類

class Invoice { ... }
複製程式碼

在 JVM 上,如果主建構函式的所有的引數都有預設值,編譯器會生成 一個額外的無參建構函式

屬性與欄位

  1. 屬性可以用關鍵字var宣告為可變的,否則使用只讀關鍵字val
    1. 只讀屬性的用val開始代替var
    2. 只讀屬性不允許 setter。要使用一個屬性,只要用名稱引用它即可
  2. 宣告一個屬性的完整語法:
var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]
複製程式碼
  1. getter總是與屬性有著相同的可見性,setter 可見許可權不能高於屬性。
  2. private 類屬性預設不生成getter / setter,對它的訪問都是直接訪問,一旦擁有自定義的 getter / setter,訪問時就要通過 getter / setter了。(可以在生成的Java檔案中檢視)

幕後欄位

什麼是幕後欄位?

JVM類屬性中存在一個與之相對應的欄位。可以通過Android Studiotools->kotlin-> Show Kotlin Bytecode -> Decompile 檢視在JVM中生成的檔案。

下面這種情況就不存在幕後欄位

val isEmpty: Boolean
    get() = this.size == 0
複製程式碼

這種情況會被直接編譯成,而不存在isEmpty這個欄位:

public final boolean isEmpty() {
      return this.size == 0;
   }
複製程式碼

存在幕後欄位的情況:

  1. 使用預設getter / setter的屬性,一定有幕後欄位。對於 var屬性來說,只要 getter / setter 中有一個使用預設實現,就會生成幕後欄位,對於val屬性來說,只要自定義了get(),就不會存在幕後欄位,如上述的isEmpty屬性;對於private 屬性比較特殊,它不存在預設的getter/setter 有但是確有幕後欄位。

  2. 在自定義 getter / setter 中使用了 field 的屬性,一定有幕後欄位。這個field 就是我們訪問幕後欄位的「關鍵字」,它與Lambda表示式中的 it 類似,並不是一個真正的關鍵字,只在特定的語句內有特殊的意思,在其他語句內都不是關鍵字。

幕後屬性

幕後屬性的用處

很多時候,我們希望定義這樣的屬性:

  1. 對外表現為 val 屬性,只能讀不能寫;

  2. 在類內部表現為var屬性,也就是說只能在類內部改變它的值。

val size get() = _size
private var _size:Int = 0
複製程式碼

對應的java程式碼

private int _size;
public final int getSize() {
    return this._size;
}
複製程式碼

這個_size屬性就是幕後屬性

編譯器常量

已知值的屬性可以使用 const 修飾符標記為 編譯期常量。這些屬性需要滿足以下條件

  • 位於頂層或者是 object 宣告companion object 的一個成員
  • String或原生型別值初始化
  • 沒有自定義getter

繼承

Kotlin 中所有類都有一個共同的超類 Any,這對於沒有超型別宣告的類是預設超類:

內聯類

巢狀類

密封類

資料類

物件表示式與物件宣告

物件表示式

我們需要建立一個對某個類做了輕微改動的類的物件,而不用為之顯式宣告新的子類, Java 用匿名內部類 處理這種情況。

  view.setOnClickListener(object :View.OnClickListener{
            override fun onClick(v: View?) {
             
            }
        })
複製程式碼

多個超型別可以由跟在冒號後面的逗號分隔的列表指定,如果我們只需要“一個物件而已”,並不需要特殊超型別,那麼我們可以簡單地寫:

fun foo() {
    val adHoc = object {
        var x: Int = 0
        var y: Int = 0
    }
    print(adHoc.x + adHoc.y)
}
複製程式碼

請注意,匿名物件可以用作只在本地和私有作用域中宣告的型別。如果你使用匿名物件作為公有函式的返回型別或者用作公有屬性的型別,那麼該函式或屬性的實際型別會是匿名物件宣告的超型別,如果你沒有宣告任何超型別,就會是Any。在匿名物件中新增的成員將無法訪問

class C {
    // 私有函式,所以其返回型別是匿名物件型別
    private fun foo() = object {
        val x: String = "x"
    }

    // 公有函式,所以其返回型別是 Any
    fun publicFoo() = object {
        val x: String = "x"
    }

    fun bar() {
        val x1 = foo().x        // 沒問題
        val x2 = publicFoo().x  // 錯誤:未能解析的引用“x”
    }
}
複製程式碼

就像 Java 匿名內部類一樣,物件表示式中的程式碼可以訪問來自包含它的作用域的變數。 (與 Java 不同的是,這不僅限於 final 變數。)

fun countClicks(window: JComponent) {
    var clickCount = 0
    var enterCount = 0

    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            clickCount++
        }

        override fun mouseEntered(e: MouseEvent) {
            enterCount++
        }
    })
    // ……
}
複製程式碼

物件宣告

單例模式在一些場景中很有用, 而 Kotlin(繼 Scala 之後)使單例宣告變得很容易:

object DataProviderManager {
    fun registerDataProvider(provider: DataProvider) {
        // ……
    }

    val allDataProviders: Collection<DataProvider>
        get() = // ……
}
複製程式碼

物件宣告不能在區域性作用域(即直接巢狀在函式內部),但是它們可以巢狀到其他物件宣告或非內部類中
這稱為物件宣告。並且它總是在 object關鍵字後跟一個名稱。
就像變數宣告一樣,物件宣告不是一個表示式,不能用在賦值語句的右邊。

物件宣告的初始化過程是執行緒安全的。

如需引用該物件,我們直接使用其名稱即可:

DataProviderManager.registerDataProvider(……)
複製程式碼

這些物件可以有超型別:

object DefaultListener : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { …… }

    override fun mouseEntered(e: MouseEvent) { …… }
}
複製程式碼

伴生物件

類內部的物件宣告可以用 companion 關鍵字標記:

class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}
複製程式碼

該伴生物件的成員可通過只使用類名作為限定符來呼叫:

val instance = MyClass.create()
複製程式碼

可以省略伴生物件的名稱,在這種情況下將使用名稱 Companion:

class MyClass {
    companion object { }
}

val x = MyClass.Companion
複製程式碼

其自身所用的類的名稱(不是另一個名稱的限定符)可用作對該類的伴生物件 (無論是否命名)的引用:

class MyClass1 {
    companion object Named { }
}

val x = MyClass1

class MyClass2 {
    companion object { }
}

val y = MyClass2
複製程式碼

請注意,即使伴生物件的成員看起來像其他語言的靜態成員,在執行時他們仍然是真實物件的例項成員,而且,例如還可以實現介面:

interface Factory<T> {
    fun create(): T
}

class MyClass {
    companion object : Factory<MyClass> {
        override fun create(): MyClass = MyClass()
    }
}

val f: Factory<MyClass> = MyClass
複製程式碼

物件表示式與物件宣告的語義差別

  • 物件表示式是在使用他們的地方立即執行(及初始化)的;
  • 物件宣告是在第一次被訪問到時延遲初始化的;
  • 伴生物件的初始化是在相應的類被載入(解析)時,與 Java 靜態初始化器的語義相匹配。

介面

Kotlin 的介面與 Java 8類似,既包含抽象方法的宣告,也包含實現。與抽象類不同的是,介面無法儲存狀態。它可以有屬性但必須宣告為抽象或提供訪問器實現。

interface MyInterface {
    fun bar()
    fun foo() {
      // 可選的方法體
    }
}
複製程式碼

屬性

在介面中宣告的屬性要麼是抽象的,要麼提供訪問器的實現。在介面中宣告的屬性不能有幕後欄位(backing field),因此介面中宣告的訪問器不能引用它們。通過我們上述對幕後欄位的解釋,對於var屬性,必須自定義gettersetter 對於val屬性自定義getter

interface MyInterface {
    val prop: Int

    val propertyWithImplementation: String
        get() = "foo"

    var prop2:Int

    var propWithGetAndSet:String
        get() = "prop2"
        set(value) {
            print(value)
        }

    fun foo() {
        print(prop)
    }
}


複製程式碼

介面的繼承

與java類似 一個類或者物件可以實現一個或多個介面,與Java 不同的是 只需定義所缺少的實現。

interface MyInterface2:MyInterface{
    fun pritlnProps(){
        println("propertyWithImplementation = "+propertyWithImplementation)
        println("propWithGetAndSet = "+propWithGetAndSet)
        println("prop = "+prop)
        println("prop2 = "+prop2)
    }
}

class A :MyInterface,MyInterface2{
    override val prop: Int = 1

    override var prop2: Int = 2

}
複製程式碼

解決覆蓋衝突

與類繼承的覆蓋規則是一致的,即需要通過super關鍵字來指定從哪個超型別繼承的實現。

參考連結

相關文章