【Kotlin】初識Kotlin之物件導向

woodwhale發表於2022-02-25

【Kotlin】初識Kotlin之物件導向

1、類

在Kotlin中,類用關鍵字class來定義

如果一個類具有類體,那麼需要使用{ }來寫類體內容,如果不需要類體,那麼只需要定義類名就可以了

// 定義一個alarmClock類
class AlarmClock {
    fun alarm() = println("叮鈴鈴...")
}

// 定義沒有類體的alarm類
class Alarm

1. 類修飾符

  • final:不能被繼承
  • open:可以被繼承
  • abstract:抽象類
  • enum:列舉類
  • data:資料類
  • sealed:密封類
  • annotation:註解類

Java 中 預設類都是public 的 ,kotlin 類預設都是final修飾的,不能用來繼承,需要新增open 修飾符

2. 成員修飾符

  • override: 重寫函式
  • open:可以被重寫
  • final:不能被重寫
  • abstract:抽象函式
  • lateinit:延遲初始化

3. 泛型修飾符

泛型修飾符多用於協變

  • in:相當於Java中的super關鍵字的作用
  • out:相當於Java中的extends關鍵字的作用

2、建構函式

在Kotlin中,建構函式與Java大有不同。

Kotlin的一個類中可以有一個主建構函式和多個次建構函式

1. 主建構函式

所謂主建構函式其實就是類頭括號中的一部分,例如

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

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

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

但是這樣我們僅僅是給Persion類傳入了name這個引數,並沒有讓他給成員變數賦值,這樣的效果和如下的Java程式碼一樣

class Person1 {
    public Person1(String name) {
        
    }
}

所以如何讓我們傳入的name成為成員變數呢?

在Kotlin的類中,有一個init關鍵字,可以作類的為初始化塊,可以理解為Java構造方法中的程式碼部分。

如果我們想給成員變數賦值,可以這麼做

class Person(name: String) {
    var name = "no name"
    init {
        this.name = name
    }
}

但是這樣其實是仿照Java的物件模式,而且這裡使用了var的變數

如果想直接一步到位,可以

class Person(name: String) {
    val name = name
}

Kotlin支援下面這樣的化簡,一步到位,與上述程式碼是一樣的功能

class Person(val name: String) {}

當然,和函式一樣,我們可以給這個成員變數賦預設值

class Person(val name: String = "woodwhale") {}

2. 次建構函式

次建構函式就直接在類體中,使用constructor宣告

class Person(val pets: MutableList<Pet> = mutableListOf())

class Pet {
    constructor(owner: Person) {
        owner.pets.add(this) // adds this pet to the list of its owner's pets
    }
}

上述程式碼的Pet類沒有使用主建構函式而使用了次建構函式進行初始化

class Person(val name: String) {
    val children: MutableList<Person> = mutableListOf()
    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
}

觀察上述程式碼,如果類有一個主建構函式,每個次建構函式需要委託給主建構函式, 可以直接委託或者通過別的次建構函式間接委託。委託到同一個類的另一個建構函式用 this 關鍵字即可

請注意,初始化塊中的程式碼實際上會成為主建構函式的一部分。委託給主建構函式會作為次建構函式的第一條語句,因此所有初始化塊與屬性初始化器中的程式碼都會在次建構函式體之前執行,例如:

class Constructors {
    init {
        println("Init block")
    }

    constructor(i: Int) {
        println("Constructor $i")
    }
}

fun main() {
    Constructors(1)
}

/*
    Init block
    Constructor 1

    程式已結束,退出程式碼0
*/

如果我們的一個類的主建構函式可見性設定為private,那麼我們將無法直接建立這個物件

image-20220224183612887

可以使用單例模式來實現,這裡舉一個懶漢式的例子:

fun main() {
    DontCreateMe.get()
}

class DontCreateMe private constructor () {
    companion object {
        private val me: DontCreateMe? = null
        get() {
            return field ?: DontCreateMe()
        }
        fun get(): DontCreateMe {
            return me!!
        }
    }
}

這裡的companion object 是伴生類,之後會將到

3、建立物件

物件就是例項化一個類,我們可以理解一下——類是一個藍圖,而物件是藍圖設計出來的產物

建立物件在Kotlin中不需要使用new關鍵字,和python類似,只需要在類名之後寫上一個()就可以

fun main() {
    val person = Person()
}

class Person

如果類帶有引數,這樣處理就好了

fun main() {
    val person = Person("woodwhale",18)
    println("姓名: ${person.name}, 年齡: ${person.age}")
}

class Person(val name: String, val age: Int)

/*
    姓名: woodwhale, 年齡: 18

    程式已結束,退出程式碼0
*/

4、類成員

一個類中可以有如下的成員:

  • 建構函式和初始化塊
  • 方法(函式)
  • 屬性
  • 巢狀類和內部類
  • 伴生物件
  • 物件宣告

上述的成員中,需要講解的就是巢狀類和內部類,還有一個物件宣告,其他的內容之前都講述過了

1. 巢狀類和內部類

首先在Java中我們只聽說過內部類這種名詞,巢狀類是什麼呢?

在Kotlin中,內部類用專門的inner來修飾,而類中沒有使用inner的類,就屬於巢狀類

例如:

// 巢狀類
class A {
    class B
}

// 內部類
class C {
    inner class D
}

值得注意的是,內部類可以訪問外部類的屬性

2. get與set

在Kotlin中,沒有Java傳統的getter和setter,我們直接通過例項物件.屬性的方式來訪問或者賦值

但是如果我想在get和set的過程中進行操作呢?例如,我想每次都獲取Person類中name的大寫

在Kotlin中,為了解決上述問題,在類中有getset兩個函式。如果我們沒有使用的話,那麼就是預設的。

使用方式:

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

這個getset是針對一個屬性來寫的,如果一個類中有很多的屬性,那麼這些屬性都可以使用自己的getset方法

舉個例子:

fun main() {
    val student = Student()
    student.name = "woodwhale"
    println(student.name)
}

class Student {
    var name: String = ""
    get() = field.lowercase()
    set(value) {
        field = value.uppercase()
        println(field)
    }
}

/*
    WOODWHALE
    woodwhale

    程式已結束,退出程式碼0
*/

在上述程式碼中,我們在get函式中,返回了 field.lowercase(),這裡的field指的就是name這個屬性,但是不能直接使用name.lowercase(),因為這樣的name也會有get和set,會導致無限遞迴從而記憶體溢位。而set函式中,我們給field賦值value.uppercase()也就是轉為大寫。

注意:

  • 一般情況下,我們不會修改get和set,使用預設的就可以了
  • val修飾的屬性不允許使用set方法,因為它是隻讀的
  • get和set的原則是,作用上面最近的一個屬性

3. 物件宣告

在Kotlin中我們可以直接使用object關鍵字來宣告一個物件,這個物件所有的屬性可以直接訪問。物件宣告非常適合用來寫Utils類

注意,使用object宣告得到的物件是一個單例,引用這個物件只需要使用使用其名就可以

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

    val allDataProviders: Collection<DataProvider>
        get() = // ……
}

fun main() {
    DataProviderManager.registerDataProvider(……)
}

因為物件宣告得到的是一個單例,如果引入變數,那麼這兩個變數指向的地址相同

var data1 = DataProviderManager
var data2 = DataProviderManager
data1.name = "test"
print("data1 name = ${data2.name}")  

4. 伴生物件

在Kotlin中,沒有static關鍵字,那麼如何在類中宣告一個靜態常量或者函式(方法)呢

這個時候,我們就使用Kotlin中的伴生物件,使用關鍵字companion 標記,這樣,類中的伴生物件就與這個類進行了關聯,我們可以直接通過這個外部類訪問伴生物件中的內部元素

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

// 訪問到物件的內部元素
val instance = MyClass.create()

當然,我們可以省略掉這個伴生物件的名字,在上述程式碼中是Factory,我們可以直接呼叫如下:

class MyClass {
    companion object {
        fun create(): MyClass = MyClass()
    }
}

val instance = MyClass.Companion.create()

注意:

  • 一個類中只能宣告一個伴生物件,也就是companion只能使用一次

  • 儘管伴生物件看起來像其他語言中的靜態成員,但是他們在執行的時候是真實物件的例項成員,我們還可以讓伴生物件實現介面

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


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

物件表示式和物件宣告之間有一個重要的語義差別:

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

5. 延遲初始化

在Kotlin中,如果我們在類中普通的宣告一個成員屬性,那麼需要賦初始值,因為Kotlin為了避免null的出現。

如果我們不想進行更多的開銷去初始化一個用的很少的成員屬性,我們可以使用延遲初始化的方式。

使用lateinit關鍵字修飾屬性,我們就不必宣告其初始值,編譯器或預設你的這個屬性會在使用前被使用者賦值

class MyTest {
    lateinit var subject: String
}

注意:

  • lateinit修飾符只能用於在類體中的屬性(不是在主建構函式中宣告的var屬性,並且僅當該屬性沒有定義get和set的手)
  • lateinit修飾符修飾的屬性不能是原生型別(Int等),可以是String等
  • 如果沒有給lateinit修飾符修飾的屬性賦值就直接讀取,會報錯
  • 我們可以使用value.isInitialized屬性來判斷value是否被初始化過
  • 被初始化過的屬性無法再次被初始化,相當於一個單例屬性

5、繼承

1. 父子類

在Kotlin中,所有的類都有一個公共的父類(超類),這個類是Any,類似於Java中的Object,對於沒有宣告父類的類,其父類預設是Any

Kotlin和Java在繼承這塊和一樣,只能單繼承,但是可以多實現(實現多個介面)

在預設情況下,Kotlin的類是final的,他們不能被繼承,如果想讓這個類可以被繼承,需要使用open關鍵字標記這個類

open class Base // 該類開放繼承

如果需要宣告一個子類,可以使用如下方式,使用:表示繼承或實現

open class Base(p: Int)

class Derived(p: Int) : Base(p)

如果子類有一個主建構函式,那麼必須使用父類的主建構函式的引數當場初始化

如果子類沒有主建構函式,那麼每個次建構函式的都使用super關鍵字初始化,或者委託給另一個次建構函式,不同的次建構函式可以呼叫父類的不同的建構函式

class MyView : View {
    constructor(ctx: Context) : super(ctx)
    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}

2. 重寫方法

在Java中,我們使用@override註解的方式來宣告一個重寫的方法,而在Kotlin中,我們直接使用override修飾符來進行修飾函式就可以了

open class Shape {
    open fun draw() { /*……*/ }
    fun fill() { /*……*/ }
}

class Circle() : Shape() {
    override fun draw() { /*……*/ }
}

需要注意的是,重寫的方法也需要加上open來修飾,如果不使用open修飾(例如Shape.fill()),那麼子類中將不能擁有一個名為fill()的函式。在此基礎上,如果一個類沒有用open修飾,那麼這個類中的方法即使用open來修飾也沒有作用

子類的子類也可以重寫方法,如果一個子類不想讓其子類再次重寫該方法,可以加上final修飾符

open class Rectangle() : Shape() {
    final override fun draw() { /*……*/ }
}

3. 重寫屬性

與Java不同的是,Kotlin可以重寫屬性,而Java的子類中的屬性可以與父類同名,這樣父類中的屬性就成了隱藏屬性。

Kotlin中的重寫屬性,同樣也是使用override來修飾屬性,前提是父類中的這個屬性被open修飾

open class Shape {
    open val vertexCount: Int = 0
}

class Rectangle : Shape() {
    override val vertexCount = 4
}

我們可以使用var來覆寫val的屬性,但是反過來就不行,原理就是val只有get方法,如果給val重寫成了var,那麼這個set如何管理呢?

我們可以直接在主構造器中使用override關鍵字作為屬性宣告的一部分

// 形狀介面
interface Shape {
    val vertexCount: Int
}

class Rectangle(override val vertexCount: Int = 4) : Shape // 總是有 4 個頂點

4. 子類初始化順序

在子類例項化物件的過程中,第一步是完成父類的初始化,父類初始化之後,就到了子類進行初始化

fun main() {
   Derived("w","d")
}

open class Base(val name: String) {

    init { println("Initializing Base") }

    open val size: Int =
        name.length.also { println("Initializing size in Base: $it") }
}

class Derived(
    name: String,
    val lastName: String,
) : Base(name.capitalize().also { println("Argument for Base: $it") }) {

    init { println("Initializing Derived") }

    override val size: Int =
        (super.size + lastName.length).also { println("Initializing size in Derived: $it") }
}

/*
    Argument for Base: W
    Initializing Base
    Initializing size in Base: 1
    Initializing Derived
    Initializing size in Derived: 2

    程式已結束,退出程式碼0

*/

這意味著,父類建構函式執行時,子類中宣告或覆蓋的屬性都還沒有初始化。如果在父類初始化邏輯中(直接或通過另一個覆蓋的 open 成員的實現間接)使用了任何一個這種屬性,那麼都可能導致不正確的行為或執行時故障。設計一個父類時,應該避免在建構函式、屬性初始化器以及 init 塊中使用 open 成員。

5. 子類呼叫父類

在子類中,使用super關鍵字可以呼叫父類(超類)中的函式和屬性

open class Rectangle {
    open fun draw() { println("Drawing a rectangle") }
    val borderColor: String get() = "black"
}

class FilledRectangle : Rectangle() {
    override fun draw() {
        super.draw()
        println("Filling the rectangle")
    }
    // 呼叫父類中的borderColor
    val fillColor: String get() = super.borderColor
}

如果在一個內部類中需要訪問外部類的父類(超類),那麼美可以通過由外部類名限定的super關鍵字來實現,super@OuterClass

class FilledRectangle: Rectangle() {
    override fun draw() { 
        val filler = Filler()
        filler.drawAndFill()
    }

    inner class Filler {
        fun fill() { println("Filling") }
        fun drawAndFill() {
            // 呼叫 Rectangle 的 draw() 實現
            super@FilledRectangle.draw() 
            fill()
            // 使用 Rectangle 所實現的 borderColor 的 get()
            println("Drawn a filled rectangle with color ${super@FilledRectangle.borderColor}") 
        }
    }
}

6. super覆蓋衝突解決

在Kotlin中,如果一個類從它的直接超類繼承相同成員的多個實現,那麼這個類必須覆蓋掉這個成員,並且自己提供實現方式。

為了表示從哪個超類繼承了實現,我們使用帶尖括號的super<Base>來限定使用那個超類

open class Rectangle {
    open fun draw() { /* …… */ }
}

interface Polygon {
    fun draw() { /* …… */ } // 介面成員預設就是“open”的
}

class Square() : Rectangle(), Polygon {
    // 編譯器要求覆蓋 draw():
    override fun draw() {
        super<Rectangle>.draw() // 呼叫 Rectangle.draw()
        super<Polygon>.draw() // 呼叫 Polygon.draw()
    }
}

在上述例子中,Square類既繼承Rectangle類,又實現了Polygon介面。但是父類和介面中都有draw()這個方法函式,為了消除歧義,Square類必須自己實現draw()方法函式,可以是使用super實現,也可以自己重寫

6、抽象類

類或者其中的成員可以宣告為abstract,表示抽象類或者抽象成員

抽象成員在當前這個類中不需要實現

抽象類不需要標註open

我們一般是讓一個類實現抽象類

class Polygon : Rectangle(){ 
     override fun draw() {}
}

abstract class Rectangle {
    abstract fun draw()
}

當然,我們也可以用一個抽象成員覆蓋一個非抽象的開放成員

open class Polygon {
    open fun draw() {}
}

abstract class Rectangle : Polygon() {
    abstract override fun draw()
}

7、介面

1. 介面定義

在物件導向中,介面很重要!在Kotlin中,也是使用關鍵字 interface來定義介面

interface MyInterface {
    fun bar()
    fun foo() {
      // 可選的方法體
    }
}

2. 介面實現

介面的實現和繼承相似,也是使用:,只不過繼承是: 類名(),介面是: 介面名

class Child : MyInterface {
    override fun bar() {
        println(114514)
    }
}

3. 介面屬性

介面中可以定義屬性,要麼是抽象的,要麼提供get函式來實現。介面宣告的屬性沒有幕後欄位(backing field),也就是無法使用field來指明這個屬性

fun main() {
    val child = Child()
    println(child.propertyWithImplementation)
    child.sout1()

}

interface MyInterface {
    val test: Int // 抽象的

    val propertyWithImplementation: String
        get() = "default"

    fun sout1() {
        print(test)
    }
}

class Child : MyInterface {
    override val test: Int = 29
    override fun sout1() {
        super.sout1()
    }
}

/*
    default
    29
    程式已結束,退出程式碼0
*/

4. 介面繼承

一個介面可以從其他介面進行繼承,從而提供父類的成員

interface Named {
    val name: String
}

// 繼承Named介面
interface Person : Named {
    val firstName: String
    val lastName: String
    // 重寫屬性
    override val name: String get() = "$firstName $lastName"
}

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

5. super覆蓋衝突解決

實現多個介面的時候,我們可能會遇到同一個方法結成多個實現的問題

我們的解決方式和繼承一樣,使用super<interface>.funciton()來實現

interface A {
    fun foo() { print("A") }
    fun bar()
}

interface B {
    fun foo() { print("B") }
    fun bar() { print("bar") }
}

class C : A {
    override fun bar() { print("bar") }
}

class D : A, B {
    override fun foo() {
        super<A>.foo()
        super<B>.foo()
    }

    override fun bar() {
        super<B>.bar()
    }
}

在上述程式碼中,介面A和B都定義了foo()bar()方法,兩者都實現了foo(),但是隻有B實現了bar(),因為C是實現了A,所以一定要重寫bar()方法。而D實現了A和B,所以需要兩個方法都重寫,可以使用super進行選擇

6. 函式式介面

在Kotlin中,如果一個介面有且僅有一個抽象成員,可以有多個非抽象成員,那麼我們可以使用函式式介面來縮短程式碼,使程式碼更加簡潔

例如有這樣一個介面

fun interface IntPredicate {
   fun accept(i: Int): Boolean
}

如果我們使用最原本的方法來實現這個介面中的accept()方法,需要這樣寫

// 建立一個類的例項
val isEven = object : IntPredicate {
   override fun accept(i: Int): Boolean {
       return i % 2 == 0
   }
}

但是,因為這個介面中,有且僅有一個抽象成員,那麼可以使用函式式介面來縮短程式碼量

// 通過 lambda 表示式建立一個例項,這樣得到的isEven物件具有重寫好的accept方法
val isEven = IntPredicate { it % 2 == 0 }

使用方式也和普通物件一樣

fun interface IntPredicate {
   fun accept(i: Int): Boolean
}

val isEven = IntPredicate { it % 2 == 0 }

fun main() {
   println("Is 7 even? - ${isEven.accept(7)}")
}

8、更多的Kotlin內容

由於篇幅限制,本章僅僅講述Kotlin的物件導向,之後將補充Kotlin的重點知識!

相關文章