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
}
- Day3(…) 括號裡的就是主建構函式,只是省略了constructor關鍵字
- { } 大括號裡的 constructor(…) 就是次建構函式,每個次建構函式都要委託給主建構函式
- 例項化一個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
}
- init就是主建構函式的初始化方法
- 使用主建構函式初始化Day3物件
- 使用次建構函式初始化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()
函式(見下文)。
為了確保生成的程式碼的一致性以及有意義的行為,資料類必須滿足以下要求:
- 主建構函式需要至少有一個引數;
- 主建構函式的所有引數需要標記為
val
或var
; - 資料類不能是抽象、開放、密封或者內部的;
因此,有兩點需要注意:
- 我們不能像普通類一樣 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
- 外部類
- 內部類
通過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
- open class Father就是父類,主建構函式裡面需要傳入String型別的引數name
- Son繼承自Father,需要用父類的主建構函式引數進行初始化
覆蓋方法
繼承自然免不了要覆蓋父類的方法,Kotlin裡面通過關鍵字override 來標識
open class Base {
open fun v() { ... } // 1
fun nv() { ... }
}
class Derived() : Base() {
override fun v() { ... } // 2
}
- 被覆蓋的方法需要用open標識
- 覆蓋的方法需要用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() {
// 可選的方法體
}
}
- interface 表示介面
- 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{
}
- 通過 :Father() 來繼承父類
- 通過 ,Interface 來實現介面
- 實現介面中定義的方法
總結
- 類與物件基本用法
- 資料類
- 巢狀類
- 內部類
- 繼承與介面
這幾個知識點我們今天就學習完了,明天我們會繼續學習 函式 和 Lambda表示式
Day 3 - Learning Kotlin Trip,Completed.