1. 程式人生 > >Kotlin學習之旅(D3)-類與繼承

Kotlin學習之旅(D3)-類與繼承

Day 3

Kotlin學習之旅-第三天

今天的主題是:類與繼承

前言

今日目標

今天空閒的時候 baidu一下,發現簡書,掘金上有很多Kotlin學習總結,但是基本上都是把官方文件一字不落地複製貼上了過來。我不希望自己也是這樣子的,因此在Kotlin學習之旅裡面:

  • 我只會把最常用的歸納總結起來,其他的大家可以去官方文件進行檢視
  • 除了官方文件有的知識點,也會加上自己的思考,標註一些不太容易理解,或者容易踩坑的地方
  • 根據自己經驗,或者評論的意見,不斷補充和修改

把這一系列的學習經驗寫成簡潔易懂又實用的文章

話不多說, 今天我們的目標就是搞定下面幾個知識點:

  • 類與物件基本用法
  • 資料類
  • 巢狀類
  • 內部類
  • 繼承與介面

Tips:

類與物件基本用法

最基本的用法:

class Day3

沒有類體,只有類名,連大括號都省了

然後在Day3中加上構造方法

class Day3 constructor(name: String) {...}  // 關鍵字constructor

但是一般我們看別人寫的程式碼都是沒有constructor這個關鍵字的,為什麼呢?

官方文件給了答案:

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

也就是說,Day3可以寫成

class Day3(name: String) {...}

直接 類名(引數1,引數2){…} 這種格式就可以了

在kotlin裡面,只能有一個主建構函式,和多個次建構函式,例如:

class Day3(name: String){  // 1
    constructor(name: String, age: Int): this(name){  // 2
        println("this is second constructor and the age is " + age)
    }
}

fun main(args: Array<String>) {
    var day3 = Day3("hello")  // 3
}
  1. Day3(…) 括號裡的就是主建構函式,只是省略了constructor關鍵字
  2. { } 大括號裡的 constructor(…) 就是次建構函式,每個次建構函式都要委託給主建構函式
  3. 例項化一個Day3物件

執行程式碼的結果:啥都沒有~

因為我們沒有在主建構函式裡面做任何的操作,那麼如果我們要做初始化操作,要怎麼寫呢?

class Day3(name: String){

    init {   // 1
        println("this is main constructor")
    }
    constructor(name: String, age: Int): this(name){
        println("this is second constructor and the age is " + age)
    }
}

fun main(args: Array<String>) {
    var day3 = Day3("hello")    // 2
    var day33 = Day3("hello", 1)  // 3
}
  1. init就是主建構函式的初始化方法
  2. 使用主建構函式初始化Day3物件
  3. 使用次建構函式初始化Day3物件

執行程式碼的結果:

this is main constructor
this is main constructor
this is second constructor and the age is 1

這裡輸出了兩次main和一次second,原因就是上面我們講到的 每個次建構函式都要委託給主建構函式 ,通過this關鍵字,在呼叫次建構函式之前,都會先呼叫一次主建構函式,因此會有兩個main輸出~

通過這個例子,應該就能弄懂 類的定義,主/次建構函式,init的用法,如何例項化物件了

資料類

Kotlin中通過 data 關鍵字來表示資料類:

data class dataClass(val name: String, val age: Int)

在實際開發中,資料類的使用是很常見的,那麼Kotlin裡的資料類具有哪些特性呢?

編譯器自動從主建構函式中宣告的所有屬性匯出以下成員:

  • equals()/hashCode() 對;
  • toString() 格式是 "User(name=John, age=42)"
  • copy() 函式(見下文)。

為了確保生成的程式碼的一致性以及有意義的行為,資料類必須滿足以下要求:

  • 主建構函式需要至少有一個引數;
  • 主建構函式的所有引數需要標記為 valvar
  • 資料類不能是抽象、開放、密封或者內部的;

因此,有兩點需要注意:

  • 我們不能像普通類一樣 class test(name: String, age: Int) 引數必須標記 val 或 var
  • 不能寫 data class test(),而是必須至少有一個引數

這樣建立資料類,編譯器才不會報錯~

請注意,對於那些自動生成的函式,編譯器只使用在主建構函式內部定義的屬性。如需在生成的實現中排出一個屬性,請將其宣告在類體中

這句話是什麼意思呢?舉個例子:

data class Person(val name: String) {
    var age: Int = 0
}
fun main(args: Array<String>) {
    val person1 = Person("John")
    val person2 = Person("John")
    person1.age = 10
    person2.age = 20
    println("person1 == person2: ${person1 == person2}")
    println("person1 with age ${person1.age}: ${person1}")
    println("person2 with age ${person2.age}: ${person2}")
}

輸出結果:

person1 == person2: true
person1 with age 10: Person(name=John)
person2 with age 20: Person(name=John)

雖然age不一樣,但是 == 結果是 true

說明只有在主建構函式內部定義的屬性才具有toString()、 equals()、 hashCode() 、copy()這幾個方法,由於屬性age定義在類體中,因此是沒有的。

最後,標準庫提供了Pair和Triple這兩個標準資料類

我們來看一下Pair的原始碼:

public data class Pair<out A, out B>(
        public val first: A,
        public val second: B
                                    ) : Serializable {

    /**
     * Returns string representation of the [Pair] including its [first] and [second] values.
     */
    public override fun toString(): String = "($first, $second)"
}

會發現其中他就是key-value格式的資料類,重寫了toString()方法,其他的預設屬性都是一樣的~

在程式碼中執行一下:

fun main(args: Array<String>) {

    var pair = Pair("1","2")
    println(pair)
    println(pair.toString())
    println(pair.toList())
}

輸出結果:

(1, 2)
(1, 2)
[1, 2]

Triple也是類似的,這裡就不看了。資料類知識點大概就是這麼多~

巢狀類與內部類

巢狀在類裡面的類,就是巢狀類,這句話說起來比較拗口,直接看程式碼:

class Outer {   // 1
    private val bar: Int = 1
    class Nested {      // 2
        fun foo() = 2
    }
}

val demo = Outer.Nested().foo() // == 2
  1. 外部類
  2. 內部類

通過Outer.Nested().foo()呼叫內部類的方法

但是這個時候 Nested 類是不能訪問Outer類的成員變數的,直接訪問的話會報錯~

如果需要,要加上inner關鍵字,讓Nested成為內部類

class Outer {
    private val bar: Int = 1
    inner class Inner {
        fun foo() = bar
    }
}

val demo = Outer().Inner().foo() // == 1

繼承與介面

這部分在實際應用中也是非常重要的,經常都會用到,畢竟抽象封裝多型這三大特性,除了類與物件以外基本上就是通過繼承和介面實現了

繼承

我們都知道,在Java裡面是隻能實現單繼承的,也就是一個子類只能有一個父類,但是通過介面的方式,其實也就相當於實現了多繼承,在Kotlin裡面也是一樣的,我們先說繼承

open class Father(name: String)     // 1
class Son(name: String) : Father(name)  // 2
  1. open class Father就是父類,主建構函式裡面需要傳入String型別的引數name
  2. Son繼承自Father,需要用父類的主建構函式引數進行初始化

覆蓋方法

繼承自然免不了要覆蓋父類的方法,Kotlin裡面通過關鍵字override 來標識

open class Base {
    open fun v() { ... }   // 1
    fun nv() { ... }
}
class Derived() : Base() {
    override fun v() { ... }  // 2
}
  1. 被覆蓋的方法需要用open標識
  2. 覆蓋的方法需要用override標識

那麼如果不想讓子類繼續覆蓋要怎麼做呢,只要加上final關鍵字就可以了

open class AnotherDerived() : Base() {
    final override fun v() { ... }
}

呼叫父類實現

在子類中可以通過super關鍵字來呼叫父類的屬性和方法

open class Foo {
    open fun f() { println("Foo.f()") }
    open val x: Int get() = 1
}

class Bar : Foo() {
    override fun f() { 
        super.f()
        println("Bar.f()") 
    }
    
    override val x: Int get() = super.x + 1
}

介面

在Kotlin中,我們使用interface關鍵字來定義介面

interface MyInterface {    // 1
    fun bar()    // 2
    fun foo() {
      // 可選的方法體
    }
}
  1. interface 表示介面
  2. fun xxx() 定義方法名,不需要實現

介面中的繼承

interface Named {
    val name: String
}

interface Person : Named {
    val firstName: String   // 1
    val lastName: String
    
    override val name: String get() = "$firstName $lastName"  // 2
}

data class Employee(
    // 不必實現“name”
    override val firstName: String,
    override val lastName: String,
    val position: Position
) : Person

可以看到,Person 繼承 Named ,並且重寫了 name 屬性的get()方法,因此在Employee類實現Person介面的時候,只要實現 firstName, lastName 兩個屬性就可以了,而不用實現 name 屬性

一般來說,我們都會使用到繼承+介面兩種方式,格式是這樣的:

class Day3(name: String) : DayFather(), MyInterface{  // 1,2

    init {
        println("this is main constructor")
    }
    constructor(name: String, age: Int): this(name){
        println("this is second constructor and the age is " + age)
    }
    
     override fun test1() {   // 3
        TODO("not implemented") 
    }

    override fun test2() {
        TODO("not implemented")
    }
}

interface test{
    fun test1()
    fun test2()
}

open class DayFather{
    
}
  1. 通過 :Father() 來繼承父類
  2. 通過 ,Interface 來實現介面
  3. 實現介面中定義的方法

總結

  • 類與物件基本用法
  • 資料類
  • 巢狀類
  • 內部類
  • 繼承與介面

這幾個知識點我們今天就學習完了,明天我們會繼續學習 函式Lambda表示式

Day 3 - Learning Kotlin Trip,Completed.