Kotlin入門(三)——類、對象、接口

本章內容包括:java

  • 類的基本要素
  • 類的繼承結構
  • 修飾符
  • 接口

0. 前言

上一篇的末尾,咱們提到了Kotlin的包和導入。api

本來我是準備把這篇的內容也放在上一篇的,可是後來一想,這張的內容會頗有點多,放進去的話可能會致使上一篇太大了,因此就單獨分紅一篇了。ide

在說類以前,咱們先來看下一個類的Java版和Kotlin版的對比,這個會一會兒就讓你對Kotlin感興趣。函數

咱們如今有一個需求,須要定義一個JavaBean類Person,這個類中包含這我的的姓名、電話號碼以及地址。工具

咱們先來看下Java的實現:ui

public class Person {
    private String firstName;
    private String lastName;
    private String telephone;
    private String address;

    public Person(String firstName, String lastName, String telephone, String address) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.telephone = telephone;
        this.address = address;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getTelephone() {
        return telephone;
    }

    public void setTelephone(String telephone) {
        this.telephone = telephone;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}
複製代碼

這是一個很基本的Java類,咱們定義了四個private屬性,而後給定了一個構造函數,而後對每一個屬性都給了get和set方法。this

相信你們學Java必定都寫過這個類。可是咱們想一想,就寫一個功能這麼簡單的類,Java卻須要咱們寫這麼多內容,有的同窗會說:Idea和Eclipse都不是提供了自動生成代碼的工具嗎。可是若是你看了Kotlin的實現,必定會以爲連自動生成工具都麻煩:spa

class Person(
    val firstName: String,
    val lastName: String,
    val telephone: String,
    val address: String
)
複製代碼

對,你沒有看錯,Kotlin的類就是這麼的簡單:.net

  • 因爲Kotlin的屬性默認的修飾符就是public。可是因爲咱們這個方法設置成了val,因此除了構造方法外,無法對這個屬性的值進行更改。可是從Kotlin編譯後會自動將val的屬性轉爲private final String firstName;var的屬性轉爲private String firstName;code

  • 因爲Kotlin會自動爲屬性生成getset方法,因此不必去顯式的寫getset方法,除非你須要自定義的getset方法。可是因爲這個類的屬性都是val,因此只會生成get方法。

  • Kotlin的默認構造方法是直接寫在類名後面的

接下來咱們就把這個代碼進行分解,逐步來說解Kotlin的類。

1. 類與繼承

1.1 類

與Java相似,Kotlin也是使用class關鍵字來表示類。

class Person() {}
複製代碼

類聲明由類名、類頭(指定其類型參數、主構造函數等)以及由花括號包圍的類體構成。類頭和類體都是可選的,一個類若是沒有類體,能夠省略花括號:

class Person()
複製代碼

1.2 構造函數

1.2.1 主構造函數

Kotlin的一個類能夠有一個主構造函數以及一個或者多個次構造函數。主構造函數是類頭的一部分,跟在類名以後:

class Person constructor(val name: String){}
複製代碼

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

class Person(val name: String){}
複製代碼

主構造方法主要有兩種目的:代表構造方法的參數,以及定義使用這些參數初始化的屬性。

可是主構造方法不容許直接有代碼塊,因此若是須要在主構造方法中添加初始化代碼,能夠放到init關鍵字的代碼塊中:

class Person(val _name: String) {
    val name: String
    init {
        name = _name
        println(name)
    }
}
複製代碼

可是同時,這個例子中,_name賦值給name,這個語句能夠放在name的定義中去,因此能夠改爲:

class Person(val _name: String) {
    val name: =  _name
    init {
        println(name)
    }
}
複製代碼

可是,若是主構造方法須要添加註解或者修飾符的話,這個constructor是不能省略的:

class Person private constructor(val name: String) {
}
複製代碼

1.2.2 次構造函數

類也能夠單純的聲明次構造方法而不聲明主構造方法:

class Person {
    val name: String

    constructor(_name: String) {
        name = _name
        println(name)
    }
}
複製代碼

若是類有一個主構造函數,每一個次構造函數須要委託給主構造函數, 能夠直接委託或者經過別的次構造函數間接委託。委託到同一個類的另外一個構造函數用 this 關鍵字便可:

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

值的注意下的是,初始化語句(init)實際上會成爲主構造方法的一部分。委託給主構造函數會做爲次構造函數的第一條語句,所以全部初始化塊與屬性初始化器中的代碼都會在次構造函數體以前執行。即便該類沒有主構造函數,這種委託仍會隱式發生,而且仍會執行初始化塊。 簡單點來講就是,無論你有沒有主構造方法,只要你有次構造方法,而且有init語句,他都會在執行次構造方法的函數體內的代碼以前,先去執行init語句:

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

class Person {
    val name: String

    constructor(_name: String) {
        name = _name
        println(name)
    }

    init {
        println("init: 1")
    }
}

/* 輸出結果爲 init: 1 1 */
複製代碼

1.3 類的實例

回憶一下上一篇講基本類型:

val one = 1 // Int
val threeBillion = 3000000000 // Long
複製代碼
  • 首先咱們說過Kotlin和Java不同,全部的東西都是對象,包括基本類型。因此說咱們建立的Kotlin的Int型參數,實際上就是new了一個Int這個類的對象
  • 其次Kotlin建立對象是經過valvar關鍵字的
  • 最後就是一點你們應該都發現了,Kotlin是沒有new這個關鍵字的

因此從上面咱們能夠推出若是在Kotlin中建立一個對象:

  • 首先,咱們須要根據對象須要的場景,選擇究竟是val的對象仍是var的對象;
  • 而後寫對象名;
  • 緊跟着對象名的就是對象的類型,可是因爲Kotlin有類型推斷,因此此處這個顯式聲明類型能夠省略,由於他能夠經過等號右邊給定的內容自動推斷出該對象的類型(此處暫時先不考慮lateinit或者其餘的操做)
  • 以後就能夠寫等號=
  • 等號右邊就是咱們須要賦值給這個對象的初始化語句了,可是Kotlin沒有new關鍵字,因此是直接寫,不須要加new
val person1: Person = Person("1") // Kotlin沒有 new 關鍵字

val person2 = Person("1") // 因爲等號右邊已經給出了具體的內容,因此能夠省略掉顯式的指定類型
複製代碼

固然也有特殊狀況:

  • 使用lateinit關鍵字
lateinit var person3: Person
複製代碼

在這種狀況下必須顯式的指定變量類型,由於使用了lateinit關鍵字,能夠延遲初始化,可是從如今開始,直到初始化,期間若是使用了這個變量,運行後就會報錯lateinit property person3 has not been initialized

  • 在方法或者類中定義變量
fun main() {
    val person4: Person
    println(person4) // 此時IDE就會報紅,Variable 'person4' must be initialized
    person4 = Person("4")
    println(person4)
}
複製代碼

在方法中或者類中還能夠這樣,先不初始化,先定義變量,可是此時必須顯式指出其類型,而且在初始化以前都不可以使用變量,若是使用了,在編譯前也就是還在編輯時,IDE就會報紅,Variable 'person4' must be initialized。可是一旦初始化以後就能夠正常使用。

1.4 繼承

1.4.1 ObjectAnyextends:

咱們都知道,Java中存在着一個基類Object,全部的對象都會繼承自這個類,哪怕你本身建立的對象沒有指明具體繼承自哪一個類,可是Java會讓他繼承自Object類。而這個類裏面也有一些每一個類必有的方法如getClass()hashCode()equals()toString()等一系列方法。

一樣的,Kotlin也有這樣的基類,只不過叫作Any。可是不一樣的是Kotlin的Any只有三個方法:hashCode()equals()toString()

而在Java中,想要繼承某一個類的話,就須要在這個類的後面用extends關鍵字 + 超類名的方法去指明這個類繼承自那個類:

class Staff extends Person{
}
複製代碼

而在Kotlin中,就沒有extends這個關鍵字了,取而代之的是咱們的老朋友:

open class Person(val name: String)
class Staff(name: String) : Person(name) 
複製代碼

這個就表明了Staff類繼承自Person類。同時基類(Person)必須得被open修飾符修飾,由於Kotlin默認全部的類都是final的,因此不能被繼承,因此就須要open修飾符修飾它。

而且若是派生類有一個主構造函數,其基類能夠(而且必須) 用派生類主構造函數的參數就地初始化。

若是派生類沒有主構造函數,那麼每一個次構造函數必須使用super關鍵字初始化其基類型,或委託給另外一個構造函數作到這一點。 注意,在這種狀況下,不一樣的次構造函數能夠調用基類型的不一樣的構造函數:

// 代碼很是不規範,僅做爲例子參考
open class Person {
    val name: String
    var address: String = ""
    constructor(_name: String) {
        name = _name
    }
    constructor(_name: String, _address: String) {
        name = _name
        address = _address
    }
}

class Staff : Person {
    constructor(name: String) : super(name)
    constructor(name: String, address: String) : super(name, address)
}
複製代碼

1.4.2 override

覆蓋方法

和Java同樣,Kotlin也是經過override關鍵字來標明覆蓋,只不過不一樣的是Java是@override註解而Kotlin是override修飾符。

open class Person(val name: String) {
    open fun getName() {
        println("這是$this, name: $name")
    }
}

class Staff(name: String) : Person(name) {
    override fun getName() {
        println("這是$this, name: $name")
    }
}
複製代碼

咱們能夠看到Staff繼承自Person並重寫了getName()方法。 此時必須在Staff重寫的getName()方法前加上override修飾符,不然編譯器會報錯。

同時,與繼承類時同樣,Kotlin默認方法也是final的,若是想讓這個方法被重寫,就須要加上open關鍵字。可是重寫後的方法,也就是有override修飾的方法,默認是開放的,可是若是你想讓他再也不被重寫,就須要手動添加final修飾符:

class Staff(name: String) : Person(name) {
    final override fun getName() {
        println("這是$this, name: $name")
    }
}
複製代碼

覆蓋屬性

這個就是Kotlin有可是Java沒有的了。和覆蓋方法同樣,也就是在須要覆蓋的屬性前面加上override

open class Person {
    open val name = "123"
}

class Staff : Person() {
    override val name = "2"
}
複製代碼

同時你可使用var屬性去覆蓋一個val的屬性,可是反過來就不行了。由於var默認會有get()set()方法,而val只有get()方法,若是用val去覆蓋var,那麼varget()方法會沒法處理。

1.4.3 初始化順序

在構造派生類的新實例的過程當中,第一步完成其基類的初始化(在以前只有對基類構造函數參數的求值),所以發生在派生類的初始化邏輯運行以前。

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") }
}

fun main() {
    println("Constructing Derived(\"hello\", \"world\")")
    val d = Derived("hello", "world")
}
複製代碼

運行結果是:

Constructing Derived("hello", "world")
Argument for Base: Hello
Initializing Base
Initializing size in Base: 5
Initializing Derived
Initializing size in Derived: 10
複製代碼

1.4.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")
    }

    val fillColor: String get() = super.borderColor
}
複製代碼

可是若是在一個內部類中訪問外部類的超類的內容,能夠經過外部類名限定的super關鍵字super@Outer來實現:

class FilledRectangle: Rectangle() {
    fun draw() { /* …… */ }
    val borderColor: String get() = "black"
    
    inner class Filler {
        fun fill() { /* …… */ }
        fun drawAndFill() {
            super@FilledRectangle.draw() // 調用 Rectangle 的 draw() 實現
            fill()
            println("Drawn a filled rectangle with color ${super@FilledRectangle.borderColor}") // 使用 Rectangle 所實現的 borderColor 的 get()
        }
    }
}
複製代碼

1.4.6 覆蓋規則

在 Kotlin 中,實現繼承由下述規則規定:若是一個類從它的直接超類繼承相同成員的多個實現, 它必須覆蓋這個成員並提供其本身的實現(也許用繼承來的其中之一)。 爲了表示採用從哪一個超類型繼承的實現,咱們使用由尖括號中超類型名限定的super,如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()
    }
}
複製代碼

1.5 抽象類

Kotlin中的抽象類用abstract關鍵字。抽象成員能夠在本類中不用實現。

同時不用說的就是,在抽象類中不須要使用open標註。

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

abstract class Rectangle : Polygon() {
    abstract override fun draw()
}
複製代碼

2. 屬性和字段

屬性的定義咱們已經在前面說到過了,主要是valvar兩個關鍵字,而如今首先要說的,就是GetterSetter

2.1 GetterSetter

聲明一個屬性完整的語法是:

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]
複製代碼

其中,property_initializergettersetter都是可選的,而且若是類型能夠從property_initializer推斷出來,則也是可選的。

而對於不論是val仍是var,若是你訪問這個屬性的時候就直接返回這個屬性的值的話,getter是能夠省略的,而若是你想返回的時候後作些操做的話,就能夠自定義getter(比方說咱們如今有一個Person類,裏面有nameage以及isAdult三個屬性,其中isAdult咱們須要去設置他的get方法,當age大於等於18的時候就返回true,不然返回false):

class Person(_name: String, _age: Int) {
    val name: String = _name
    val age: Int = _age
    val isAdult: Boolean
        get() = age >= 18
}
複製代碼

而同時咱們須要在設置這我的的age的時候,作一個判斷,若是輸入的值小於0的話,就拋異常,不然才更改age的值。這個時候咱們就須要自定義set方法了:

class Person(_name: String, _age: Int) {
    val name: String = _name
    var age: Int = _age
        set(value) {
            if (value <= 0) {
                throw Exception("年齡必須大於0")
            } else {
                field = value
            }
        }
    val isAdult: Boolean
        get() = age >= 18
}

fun main() {
    try {
        val person = Person("314", 18)
        person.age = 0
    } catch (e: Exception) {
        println(e.message)
    }
}
複製代碼

運行結果就是年齡必須大於0。可是這塊有個小要點,就是屬性的set方法在對象初始化的時候是不起做用的,也就是說,若是我給上面這個Person類建立對象的時候,給age傳入0或者負數的話:

fun main() {
    try {
        val person = Person("314", 0)
        println(person.age)
    } catch (e: Exception) {
        println(e.message)
    }
}
複製代碼

運行結果沒有任何異常,輸出0

不知道你們注意到了沒有,咱們給setter傳入的是value,而用field承接了傳入的value

其實這個value是咱們自定義的,也就是說set()這個括號裏面的名字你能夠隨便寫,只要符合Kotlin命名規範。 可是這個field是不可變的,這個field至關因而this.屬性,也就至關因而set的這個值自己,也就是說,若是你想在setter中改變這個屬性的值的話,就必須得把最終的值傳給fieldfield就至關因而這個屬性,而setterthis.屬性是沒有意義的,你寫了的話,IDEA反而會提示你讓你改爲field

1

2.2 編譯器常量

若是隻讀屬性的值在編譯器是已知的,就可使用const去修飾將其標記爲編譯器常量,這種屬性須要知足下列要求:

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

2.3 延遲初始化屬性與變量

通常,屬性聲明爲非空類型就必須得在構造函數中去初始化。可是這樣也會不是很方便,例如像Android中的view的對象(TextView、Button等view的對象,須要被findViewById)。在這種狀況下,咱們無法去提供一個構造器去讓其初始化,這個時候就可使用lateinit修飾符:

class MainActivity : AppCompatActivity() {

    private lateinit var mTextView: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mTextView = findViewById(R.id.textView_base_model)
    }
}
複製代碼

在被lateinit修飾的變量被初始化前,若是訪問這個變量的話,就會拋一個異常。

3. 接口

在Kotlin中使用interface來定義接口:

interface Food {
    fun cook() {
        // 可選的方法體
    }
    fun eat()
}
複製代碼

3.1 實現接口

和Java同樣,一個類能夠實現多個接口:

class Person : Food, Action {
    override fun eat() {
    }

    override fun walk() {
    }

    override fun play() {
    }
}
複製代碼

3.2 接口中的屬性

和Java同樣,Kotlin的接口中也能夠存在屬性。

只不過若是想要在Kotlin中定義屬性,必須保證這個屬性要麼是抽象的,要麼指定了訪問器。

interface Food {
    val isCookFinished: Boolean // 抽象的屬性
    val isEatFinished: Boolean // 指定了訪問器的屬性
        get() = isCookFinished

    fun cook() {
        // 可選的方法體
    }

    fun eat()
}
複製代碼

3.3 接口的繼承

和類同樣,接口也能夠繼承自另一個接口,能夠在父接口的基礎上去添加新的方法或者屬性。

4. 可見性修飾符

在Kotlin中,主要有4種修飾符:

  • private
  • protected
  • internal
  • public

若是沒有指定修飾符,默認是public

4.1 修飾符在包內

咱們以前提到過,Kotlin能夠直接直接在頂層聲明類、函數和屬性。

// 文件名:example.kt
package foo

private fun foo() { …… } // 在 example.kt 內可見

public var bar: Int = 5 // 該屬性隨處可見
    private set         // setter 只在 example.kt 內可見
    
internal val baz = 6    // 相同模塊內可見
複製代碼
  • 若是不指定任何修飾符,默認爲public,意味着隨處可見;
  • 若是聲明爲private,它只能在聲明他的文件內可見;
  • 若是聲明爲internal,它只能在相同的模塊(模塊咱們會在本文的4.4講到)中可見;
  • 頂層中不可以使用protected(理由也很好想到——都沒有類,怎麼存在子類的概念)

4.2 修飾符在類和接口內

對於在類或者接口內的方法或者屬性,咱們四種修飾符均可用:

open class Outer {
    private val a = 1
    protected open val b = 2
    internal val c = 3
    val d = 4  // 默認 public
    
    protected class Nested {
        public val e: Int = 5
    }
}

class Subclass : Outer() {
    // a 不可見
    // b、c、d 可見
    // Nested 和 e 可見

    override val b = 5   // 「b」爲 protected
}

class Unrelated(o: Outer) {
    // o.a、o.b 不可見
    // o.c 和 o.d 可見(相同模塊)
    // Outer.Nested 不可見,Nested::e 也不可見
}
複製代碼
  • private:只在這個類的內部可見;
  • protected:只在這個類的內部以及他的子類可見;
  • internal:只在這個模塊內可見;
  • public:隨處可見。

4.3 局部

局部變量、函數和類不能夠有可見性修飾符。

4.4 Kotlin中的模塊

可見性修飾符internal意味着該成員只在相同模塊內可見。更具體地說, 一個模塊是編譯在一塊兒的一套 Kotlin 文件:

  • 一個 IntelliJ IDEA 模塊;
  • 一個 Maven 項目;
  • 一個 Gradle 源集(例外是test源集能夠訪問maininternal聲明);
  • 一次<kotlinc>Ant 任務執行所編譯的一套文件。
相關文章
相關標籤/搜索