Kotlin極簡教程:第7章 面向物件程式設計
在前面的章節中,我們學習了Kotlin的語言基礎知識、型別系統、集合類以及泛型相關的知識。在本章節以及下一章中,我們將一起來學習Kotlin對面向物件程式設計以及函數語言程式設計的支援。
7.1 面向物件程式設計思想
7.1.1 一切皆是對映
《易傳·繫辭上傳》:“易有太極,是生兩儀,兩儀生四象,四象生八卦。” 如今的網際網路世界,其基石卻是01(陰陽),不得不佩服我華夏先祖的博大精深的智慧。
一切皆是對映
計算機領域中的所有問題,都可以通過向上一層進行抽象封裝來解決.這裡的封裝的本質概念,其實就是“對映”。
就好比通過的電子電路中的電平進行01邏輯對映,於是有了布林代數,數字邏輯電路系統;
對01邏輯的進一步封裝抽象成CPU指令集對映,誕生了組合語言;
通過組合語言的向上抽象一層編譯直譯器,於是有了pascal,fortran,C語言;
再對核心函式api進行封裝形成開發包(Development Kit), 於是有了Java,C++ 。
從面向過程到面向物件,再到設計模式,架構設計,面向服務,Sass/Pass/Iass等等的思想,各種軟體理論思想五花八門,但萬變不離其宗。
- 你要解決一個怎樣的問題?
- 你的問題領域是怎樣的?
- 你的模型(資料結構)是什麼?
- 你的演算法是什麼?
- 你對這個世界的本質認知是怎樣的?
- 你的業務領域的邏輯問題,流程是什麼?
Grady Booch:我對OO程式設計的目標從來就不是複用。相反,對我來說,物件提供了一種處理複雜性的方式。這個問題可以追溯到亞里士多德:您把這個世界視為過程還是物件?在OO興起運動之前,程式設計以過程為中心–例如結構化設計方法。然而,系統已經到達了超越其處理能力的複雜性極點。有了物件,我們能夠通過提升抽象級別來構建更大的、更復雜的系統–我認為,這才是面向物件程式設計運動的真正勝利。
最初, 人們使用物理的或邏輯的二進位制機器指令來編寫程式, 嘗試著表達思想中的邏輯, 控制硬體計算和顯示, 發現是可行的;
接著, 創造了助記符 —— 組合語言, 比機器指令更容易記憶;
再接著, 創造了編譯器、直譯器和計算機高階語言, 能夠以人類友好自然的方式去編寫程式, 在犧牲少量效能的情況下, 獲得比組合語言更強且更容易使用的語句控制能力:條件、分支、迴圈, 以及更多的語言特性: 指標、結構體、聯合體、列舉等, 還創造了函式, 能夠將一系列指令封裝成一個獨立的邏輯塊反覆使用;
逐漸地,產生了面向過程的程式設計方法;
後來, 人們發現將資料和邏輯封裝成物件, 更接近於現實世界, 且更容易維護大型軟體, 又出現了面向物件的程式語言和程式設計方法學, 增加了新的語言特性: 繼承、 多型、 模板、 異常錯誤。
為了不必重複開發常見工具和任務, 人們創造和封裝了容器及演算法、SDK, 垃圾回收器, 甚至是併發庫;
為了讓計算機語言更有力更有效率地表達各種現實邏輯, 消解軟體開發中遇到的衝突, 還在語言中支援了超程式設計、 高階函式, 閉包 等有用特性。
為了更高效率地開發可靠的軟體和應用程式, 人們逐漸構建了程式碼編輯器、 IDE、 程式碼版本管理工具、公共庫、應用框架、 可複用元件、系統規範、網路協議、 語言標準等, 針對遇到的問題提出了許多不同的思路和解決方案, 並總結提煉成特定的技術和設計模式, 還探討和形成了不少軟體開發過程, 用來保證最終釋出的軟體質量。 儘管編寫的這些軟體和工具還存在不少 BUG ,但是它們都“奇蹟般地存活”, 並共同構建了今天蔚為壯觀的網際網路時代的電商,網際網路金融,雲端計算,大資料,物聯網,機器智慧等等的“虛擬世界”。
7.1.2 二進位制01與易經陰陽
二進位制數是用0和1兩個數碼來表示的數。它的基數為2,進位規則是“逢二進一”,借位規則是“借一當二”,由18世紀德國數理哲學大師萊布尼茲發現。當前的計算機系統使用的基本上是二進位制系統。
19世紀愛爾蘭邏輯學家B對邏輯命題的思考過程轉化為對符號0,1的某種代數演算,二進位制是逢2進位的進位制。0、1是基本算符。因為它只使用0、1兩個數字符號,非常簡單方便,易於用電子方式實現。
二進位制的發現直接導致了電子計算器和計算機的發明,並讓計算機得到了迅速的普及,進入各行各業,成為人類生活和生產的重要工具。
二進位制的實質是通過兩個數字“0”和“1”來描述事件。在人類的生產、生活等許多領域,我們可以通過計算機來虛擬地描述現實中存在的事件,並能通過給定的條件和引數模擬事件變化的規律。二進位制的計算機幾乎是萬能的,能將我們生活的現實世界完美複製,並且還能根據我們人類給定的條件模擬在現實世界難以實現的各種實驗。
但是,不論計算機能給我們如何多變、如何完美、如何複雜的畫面,其本源只是簡單的“0”和“1”。“0”和“1”在計算機中通過不同的組合與再組合,模擬出一個紛繁複雜、包羅永珍的虛擬世界。我們簡單圖示如下:
二進位制的“0”和“1”通過計算機裡能夠創造出一個虛擬的、紛繁的世界。自然界中的陰陽形成了現實世界的萬事萬物。
所以自然世界的“陰”“陽”作為基礎切實地造就了複雜的現實世界,計算機的“0”和“1”形象地模擬現實世界的一切現象,易學中的“卦”和“陰陽爻”抽象地揭示了自然界存在的事件和其變化規律。
所以說,程式設計的本質跟大自然創造萬物的本質是一樣的。
7.1.3 從面向過程到面向物件
從IBM公司的約翰·巴庫斯在1957年開發出世界上第一個高階程式設計語言Fortran至今,高階程式設計語言的發展已經經歷了整整半個世紀。在這期間,程式設計語言主要經歷了從面向過程(如C和Pascal語言)到面向物件(如C++和Java語言),再到面向元件程式設計(如.NET平臺下的C#語言),以及面向服務架構技術(如SOA、Service以及最近很火的微服務架構)等。
面向過程程式設計
結構化程式設計思想的核心:功能分解(自頂向下,逐層細化)。
1971年4月份的 Communications of ACM上,尼古拉斯·沃斯(Niklaus Wirth,1934年2月15日—, 結構化程式設計思想的創始人。因發明了Euler、Alogo-W、Modula和Pascal等一系列優秀的程式語言並提出了結構化程式設計思想而在1984年獲得了圖靈獎。)發表了論文“通過逐步求精方式開發程式’(Program Development by Stepwise Refinement),首次提出“結構化程式設計”(structure programming)的概念。
不要求一步就編製成可執行的程式,而是分若干步進行,逐步求精。
第一步編出的程式抽象度最高,第二步編出的程式抽象度有所降低…… 最後一步編出的程式即為可執行的程式。
用這種方法程式設計,似乎複雜,實際上優點很多,可使程式易讀、易寫、易除錯、易維護、易保證其正確性及驗證其正確性。
結構化程式設計方法又稱為“自頂向下”或“逐步求精”法,在程式設計領域引發了一場革命,成為程式開發的一個標準方法,尤其是在後來發展起來的軟體工程中獲得廣泛應用。有人評價說Wirth的結構化程式設計概念“完全改變了人們對程式設計的思維方式”,這是一點也不誇張的。
尼古拉斯· 沃思教授在程式設計界提出了一個著名的公式:
程式 = 資料結構 + 演算法
面向物件程式設計
面向物件程式設計思想的核心:應對變化,提高複用。
阿倫·凱(Alan Kay):面向物件程式設計思想的創始人。2003年因在面向物件程式設計上所做的巨大貢獻而獲得圖靈獎。
The best way to predict the future is to invent it,預測未來最好的方法是創造它!(Alan Kay)
阿倫·凱是Smalltalk面向物件程式語言的發明人之一,也是面向物件程式設計思想的創始人之一,同時,他還是膝上型電腦最早的構想者和現代Windows GUI的建築師。最早提出PC概念和網際網路的也是阿倫·凱,所以人們都尊稱他為“預言大師”。他是當今IT界屈指可數的技術天才級人物。
面向物件程式設計思想主要是複用性和靈活性(彈性)。複用性是面向物件程式設計的一個主要機制。靈活性主要是應對變化的特性,因為客戶的需求是不斷改變的,怎樣適應客戶需求的變化,這是軟體設計靈活性或者說是彈性的問題。
Java是一種面向物件程式語言,它基於Smalltalk語言,作為OOP語言,它具有以下五個基本特性:
- 萬物皆物件,每一個物件都會儲存資料,並且可以對自身執行操作。因此,每一個物件包含兩部分:成員變數和成員方法。在成員方法中可以改變成員變數的值。
- 程式是物件的集合,他們通過傳送訊息來告知彼此所要做的事情,也就是呼叫相應的成員函式。
- 每一個物件都有自己的由其他物件所構成的儲存,也就是說在建立新物件的時候可以在成員變數中使用已存在的物件。
- 每個物件都擁有其型別,每個物件都是某個類的一個例項,每一個類區別於其它類的特性就是可以向它傳送什麼型別的訊息,也就是它定義了哪些成員函式。
- 某一個特定型別的所有物件都可以接受同樣的訊息。另一種對物件的描述為:物件具有狀態(資料,成員變數)、行為(操作,成員方法)和標識(成員名,記憶體地址)。
面嚮物件語言其實是對現實生活中的實物的抽象。
每個物件能夠接受的請求(訊息)由物件的介面所定義,而在程式中必須由滿足這些請求的程式碼,這段程式碼稱之為這個介面的實現。當向某個物件傳送訊息(請求)時,這個物件便知道該訊息的目的(該方法的實現已定義),然後執行相應的程式碼。
我們經常說一些程式碼片段是優雅的或美觀的,實際上意味著它們更容易被人類有限的思維所處理。
對於程式的複合而言,好的程式碼是它的表面積要比體積增長的慢。
程式碼塊的“表面積”是是我們複合程式碼塊時所需要的資訊(介面API協議定義)。程式碼塊的“體積”就是介面內部的實現邏輯(API背後的實現程式碼)。
在面向物件程式設計中,一個理想的物件應該是隻暴露它的抽象介面(純表面, 無體積),其方法則扮演箭頭的角色。如果為了理解一個物件如何與其他物件進行復合,當你發現不得不深入挖掘物件的實現之時,此時你所用的程式設計正規化的原本優勢就蕩然無存了。
面向元件和麵向服務
- 面向元件
我們知道面向物件支援重用,但是重用的單元很小,一般是類;而面向元件則不同,它可以重用多個類甚至一個程式。也就是說面向元件支援更大範圍內的重用,開發效率更高。如果把面向物件比作重用零件,那麼面向元件則是重用部件。
- 面向服務
將系統進行功能化,每個功能提供一種服務。現在非常流行微服務MicroService技術以及SOA(面向服務架構)技術。
面向過程(Procedure)→面向物件(Object)→ 面向元件(Component) →面向服務(Service)
正如解決數學問題通常我們會談“思想”,諸如反證法、化繁為簡等,解決計算機問題也有很多非常出色的思想。思想之所以稱為思想,是因為“思想”有拓展性與引導性,可以解決一系列問題。
解決問題的複雜程度直接取決於抽象的種類及質量。過將結構、性質不同的底層實現進行封裝,向上提供統一的API介面,讓使用者覺得就是在使用一個統一的資源,或者讓使用者覺得自己在使用一個本來底層不直接提供、“虛擬”出來的資源。
計算機中的所有問題 , 都可以通過向上抽象封裝一層來解決。同樣的,任何複雜的問題, 最終總能夠迴歸最本質,最簡單。
面向物件程式設計是一種自頂向下的程式設計方法。萬事萬物都是物件,物件有其行為(方法),狀態(成員變數,屬性)。OOP是一種程式設計思想,而不是針對某個語言而言的。當然,語言影響思維方式,思維依賴語言的表達,這也是辯證的來看。
所謂“面嚮物件語言”,其實經典的“過程式語言”(比如Pascal,C),也能體現面向物件的思想。所謂“類”和“物件”,就是C語言裡面的抽象資料型別結構體(struct)。
而面向物件的多型是唯一相比struct多付出的代價,也是最重要的特性。這就是SmallTalk、Java這樣的面嚮物件語言所提供的特性。
回到一個古老的話題:程式是什麼?
在面向物件的程式設計世界裡,下面的這個公式
程式 = 演算法 + 資料結構
可以簡單重構成:
程式 = 基於物件操作的演算法 + 以物件為最小單位的資料結構
封裝總是為了減少操作粒度,資料結構上的封裝導致了資料的減少,自然減少了問題求解的複雜度;對程式碼的封裝使得程式碼得以複用,減少了程式碼的體積,同樣使問題簡化。這個時候,演算法操作的就是一個抽象概念的集合。
在面向物件的程式設計中,我們便少不了集合類容器。容器就用來存放一類有共同抽象概念的東西。這裡說有共同概念的東西(而沒有說物件),其實,就是我們上一個章節中講到的泛型。這樣對於一個通用的演算法,我們就可以最大化的實現複用,作用於的集合。
面向物件的本質就是讓物件有多型性,把不同物件以同一特性來歸組,統一處理。至於所謂繼承、虛表、等等概念,只是其實現的細節。
在遵循這些面向物件設計原則基礎上,前輩們總結出一些解決不同問題場景的設計模式,以GOF的23中設計模式最為知名。
我們用一幅圖簡單概括一下面向物件程式設計的知識框架:
講了這麼多思考性的思想層面的東西,我們下面來開始Kotlin的面向物件程式設計的學習。Kotlin對面向物件程式設計是完全支援的。
7.2 類與建構函式
Kotlin和Java很相似,也是一種面向物件的語言。下面我們來一起學習Kotlin的面向物件的特性。如果您熟悉Java或者C++、C#中的類,您可以很快上手。同時,您也將看到Kotlin與Java中的面向物件程式設計的一些不同的特性。
Kotlin中的類和介面跟Java中對應的概念有些不同,比如介面可以包含屬性宣告;Kotlin的類宣告,預設是final和public的。
另外,巢狀類並不是預設在內部的。它們不包含外部類的隱式引用。
在建構函式方面,Kotlin簡短的主建構函式在大多數情況下都可以滿足使用,當然如果有稍微複雜的初始化邏輯,我們也可以宣告次級建構函式來完成。
我們還可以使用 data 修飾符來宣告一個數據類,使用 object 關鍵字來表示單例物件、伴生物件等。
Kotlin類的成員可以包含:
- 建構函式和初始化塊
- 屬性
- 函式
- 巢狀類和內部類
- 物件宣告
7.2.1 宣告類
和大部分語言類似,Kotlin使用class作為類的關鍵字,當我們宣告一個類時,直接通過class加類名的方式來實現:
class World
這樣我們就聲明瞭一個World類。
7.2.2 建構函式
在 Kotlin 中,一個類可以有
- 一個主建構函式(primary constructor)
- 一個或多個次建構函式(secondary constructor)
主建構函式
主建構函式是類頭的一部分,直接放在類名後面:
open class Student constructor(var name: String, var age: Int) : Any() {
...
}
如果主建構函式沒有任何註解或者可見性修飾符,可以省略這個 constructor 關鍵字。如果建構函式有註解或可見性修飾符,這個 constructor 關鍵字是必需的,並且這些修飾符在它前面:
annotation class MyAutowired
class ElementaryStudent public @MyAutowired constructor(name: String, age: Int) : Student(name, age) {
...
}
與普通屬性一樣,主建構函式中宣告的屬性可以是可變的(var)或只讀的(val)。
主建構函式不能包含任何的程式碼。初始化的程式碼可以放到以 init 關鍵字作為字首的初始化塊(initializer blocks)中:
open class Student constructor(var name: String, var age: Int) : Any() {
init {
println("Student{name=$name, age=$age} created!")
}
...
}
主構造的引數可以在初始化塊中使用,也可以在類體內宣告的屬性初始化器中使用。
次建構函式
在類體中,我們也可以宣告字首有 constructor 的次建構函式,次建構函式不能有宣告 val 或 var :
class MiddleSchoolStudent {
constructor(name: String, age: Int) {
}
}
如果類有一個主建構函式,那麼每個次建構函式需要委託給主建構函式, 委託到同一個類的另一個建構函式用 this 關鍵字即可:
class ElementarySchoolStudent public @MyAutowired constructor(name: String, age: Int) : Student(name, age) {
override var weight: Float = 80.0f
constructor(name: String, age: Int, weight: Float) : this(name, age) {
this.weight = weight
}
...
}
如果一個非抽象類沒有宣告任何(主或次)建構函式,它會有一個生成的不帶引數的主建構函式。建構函式的可見性是 public。
私有主建構函式
我們如果希望這個建構函式是私有的,我們可以如下宣告:
class DontCreateMe private constructor() {
}
這樣我們在程式碼中,就無法直接使用主建構函式來例項化這個類,下面的寫法是不允許的:
val dontCreateMe = DontCreateMe() // cannot access it
但是,我們可以通過次建構函式引用這個私有主建構函式來例項化物件:
7.2.2 類的屬性
我們再給這個World類加入兩個屬性。我們可能直接簡單地寫成:
class World1 {
val yin: Int
val yang: Int
}
在Kotlin中,直接這樣寫語法上是會報錯的:
意思很明顯,是說這個類的屬性必須要初始化,或者如果不初始化那就得是抽象的abstract屬性。
我們把這兩個屬性都給初始化如下:
class World1 {
val yin: Int = 0
val yang: Int = 1
}
我們再來使用測試程式碼來看下訪問這兩個屬性的方式:
>>> class World1 {
... val yin: Int = 0
... val yang: Int = 1
... }
>>> val w1 = World1()
>>> w1.yin
0
>>> w1.yang
1
上面的World1類的程式碼,在Java中等價的寫法是:
public final class World1 {
private final int yin;
private final int yang = 1;
public final int getYin() {
return this.yin;
}
public final int getYang() {
return this.yang;
}
}
我們可以看出,Kotlin中的類的欄位自動帶有getter方法和setter方法。而且寫起來比Java要簡潔的多。
7.2.3 函式(方法)
我們再來給這個World1類中加上一個函式:
class World2 {
val yin: Int = 0
val yang: Int = 1
fun plus(): Int {
return yin + yang
}
}
val w2 = World2()
println(w2.plus()) // 輸出 1
7.3 抽象類
7.3.1 抽象類的定義
含有抽象函式的類(這樣的類需要使用abstract修飾符來宣告),稱為抽象類。
下面是一個抽象類的例子:
abstract class Person(var name: String, var age: Int) : Any() {
abstract var addr: String
abstract val weight: Float
abstract fun doEat()
abstract fun doWalk()
fun doSwim() {
println("I am Swimming ... ")
}
open fun doSleep() {
println("I am Sleeping ... ")
}
}
7.3.2 抽象函式
在上面的這個抽象類中,不僅可以有抽象函式abstract fun doEat()
abstract fun doWalk()
,同時可以有具體實現的函式fun doSwim()
, 這個函式預設是final的。也就是說,我們不能重寫這個doSwim函式:
如果一個函式想要設計成能被重寫,例如fun doSleep()
,我們給它加上open關鍵字即可。然後,我們就可以在子類中重寫這個open fun doSleep()
:
class Teacher(name: String, age: Int) : Person(name, age) {
override var addr: String = "HangZhou"
override val weight: Float = 100.0f
override fun doEat() {
println("Teacher is Eating ... ")
}
override fun doWalk() {
println("Teacher is Walking ... ")
}
override fun doSleep() {
super.doSleep()
println("Teacher is Sleeping ... ")
}
// override fun doSwim() { // cannot be overriden
// println("Teacher is Swimming ... ")
// }
}
抽象函式是一種特殊的函式:它只有宣告,而沒有具體的實現。抽象函式的宣告格式為:
abstract fun doEat()
關於抽象函式的特徵,我們簡單總結如下:
- 抽象函式必須用abstract關鍵字進行修飾
- 抽象函式不用手動新增open,預設被open修飾
- 抽象函式沒有具體的實現
- 含有抽象函式的類成為抽象類,必須由abtract關鍵字修飾。抽象類中可以有具體實現的函式,這樣的函式預設是final(不能被覆蓋重寫),如果想要重寫這個函式,給這個函式加上open關鍵字。
7.3.3 抽象屬性
抽象屬性就是在var或val前被abstract修飾,抽象屬性的宣告格式為:
abstract var addr : String
abstract val weight : Float
關於抽象屬性,需要注意的是:
- 抽象屬相在抽象類中不能被初始化
- 如果在子類中沒有主建構函式,要對抽象屬性手動初始化。如果子類中有主建構函式,抽象屬性可以在主建構函式中宣告。
綜上所述,抽象類和普通類的區別有:
1.抽象函式必須為public或者protected(因為如果為private,則不能被子類繼承,子類便無法實現該方法),預設情況下預設為public。
也就是說,這三個函式
abstract fun doEat()
abstract fun doWalk()
fun doSwim() {
println("I am Swimming ... ")
}
預設的都是public的。
另外抽象類中的具體實現的函式,預設是final的。上面的三個函式,等價的Java的程式碼如下:
public abstract void doEat();
public abstract void doWalk();
public final void doSwim() {
String var1 = "I am Swimming ... ";
System.out.println(var1);
}
2.抽象類不能用來建立物件例項。也就是說,下面的寫法編譯器是不允許的:
3.如果一個類繼承於一個抽象類,則子類必須實現父類的抽象方法。實現父類抽象函式,我們使用override關鍵字來表明是重寫函式:
class Programmer(override var addr: String, override val weight: Float, name: String, age: Int) : Person(name, age) {
override fun doEat() {
println("Programmer is Eating ... ")
}
override fun doWalk() {
println("Programmer is Walking ... ")
}
}
如果子類沒有實現父類的抽象函式,則必須將子類也定義為為abstract類。例如:
abstract class Writer(override var addr: String, override val weight: Float, name: String, age: Int) : Person(name, age) {
override fun doEat() {
println("Programmer is Eating ... ")
}
abstract override fun doWalk();
}
doWalk函式沒有實現父類的抽象函式,那麼我們在子類中把它依然定義為抽象函式。相應地這個子類,也成為了抽象子類,需要使用abstract關鍵字來宣告。
如果抽象類中含有抽象屬性,在實現子類中必須將抽象屬性初始化,除非子類也為抽象類。例如我們宣告一個Teacher類繼承Person類:
class Teacher(name: String, age: Int) : Person(name, age) {
override var addr: String // error, 需要初始化,或者宣告為abstract
override val weight: Float // error, 需要初始化,或者宣告為abstract
...
}
這樣寫,編譯器會直接報錯:
解決方法是,在實現的子類中,我們將抽象屬性初始化即可:
class Teacher(name: String, age: Int) : Person(name, age) {
override var addr: String = "HangZhou"
override val weight: Float = 100.0f
override fun doEat() {
println("Teacher is Eating ... ")
}
override fun doWalk() {
println("Teacher is Walking ... ")
}
}
7.4 介面
7.4.1 介面定義
和Java類似,Kotlin使用interface作為介面的關鍵詞:
interface ProjectService
Kotlin 的介面與 Java 8 的介面類似。與抽象類相比,他們都可以包含抽象的方法以及方法的實現:
interface ProjectService {
val name: String
val owner: String
fun save(project: Project)
fun print() {
println("I am project")
}
}
7.4.2 實現介面
介面是沒有建構函式的。我們使用冒號:
語法來實現一個介面,如果有多個用,
逗號隔開:
class ProjectServiceImpl : ProjectService
class ProjectMilestoneServiceImpl : ProjectService, MilestoneService
我們也可以實現多個介面:
class Project
class Milestone
interface ProjectService {
val name: String
val owner: String
fun save(project: Project)
fun print() {
println("I am project")
}
}
interface MilestoneService {
val name: String
fun save(milestone: Milestone)
fun print() {
println("I am Milestone")
}
}
class ProjectMilestoneServiceImpl : ProjectService, MilestoneService {
override val name: String
get() = "ProjectMilestone"
override val owner: String
get() = "Jack"
override fun save(project: Project) {
println("Save Project")
}
override fun print() {
// super.print()
super<ProjectService>.print()
super<MilestoneService>.print()
}
override fun save(milestone: Milestone) {
println("Save Milestone")
}
}
當子類繼承了某個類之後,便可以使用父類中的成員變數,但是並不是完全繼承父類的所有成員變數。具體的原則如下:
1.能夠繼承父類的public和protected成員變數;不能夠繼承父類的private成員變數;
2.對於父類的包訪問許可權成員變數,如果子類和父類在同一個包下,則子類能夠繼承;否則,子類不能夠繼承;
3.對於子類可以繼承的父類成員變數,如果在子類中出現了同名稱的成員變數,則會發生隱藏現象,即子類的成員變數會遮蔽掉父類的同名成員變數。如果要在子類中訪問父類中同名成員變數,需要使用super關鍵字來進行引用。
7.4.3 覆蓋衝突
在kotlin中, 實現繼承通常遵循如下規則:如果一個類從它的直接父類繼承了同一個函式的多個實現,那麼它必須重寫這個函式並且提供自己的實現(或許只是直接用了繼承來的實現) 為表示使用父類中提供的方法我們用 super 表示。
在重寫print()
時,因為我們實現的ProjectService、MilestoneService都有一個print()
函式,當我們直接使用super.print()
時,編譯器是無法知道我們想要呼叫的是那個裡面的print函式的,這個我們叫做覆蓋衝突:
這個時候,我們可以使用下面的語法來呼叫:
super<ProjectService>.print()
super<MilestoneService>.print()
7.4.4 介面中的屬性
在介面中宣告的屬性,可以是抽象的,或者是提供訪問器的實現。
在企業應用中,大多數的型別都是無狀態的,如:Controller、ApplicationService、DomainService、Repository等。
因為介面沒有狀態, 所以它的屬性是無狀態的。
interface MilestoneService {
val name: String // 抽象的
val owner: String get() = "Jack" // 訪問器
fun save(milestone: Milestone)
fun print() {
println("I am Milestone")
}
}
class MilestoneServiceImpl : MilestoneService {
override val name: String
get() = "MilestoneServiceImpl name"
override fun save(milestone: Milestone) {
println("save Milestone")
}
}
7.5 抽象類和介面的差異
概念上的區別
介面主要是對動作的抽象,定義了行為特性的規約。
抽象類是對根源的抽象。當你關注一個事物的本質的時候,用抽象類;當你關注一個操作的時候,用介面。
語法層面上的區別
介面不能儲存狀態,可以有屬性但必須是抽象的。
一個類只能繼承一個抽象類,而一個類卻可以實現多個介面。
類如果要實現一個介面,它必須要實現介面宣告的所有方法。但是,類可以不實現抽象類宣告的所有方法,當然,在這種情況下,類也必須得宣告成是抽象的。
介面中所有的方法隱含的都是抽象的。而抽象類則可以同時包含抽象和非抽象的方法。
設計層面上的區別
抽象類是對一種事物的抽象,即對類抽象,而介面是對行為的抽象。抽象類是對整個類整體進行抽象,包括屬性、行為,但是介面卻是對類區域性(行為)進行抽象。
繼承是 is a
的關係,而 介面實現則是 has a
的關係。如果一個類繼承了某個抽象類,則子類必定是抽象類的種類,而介面實現就不需要有這層型別關係。
設計層面不同,抽象類作為很多子類的父類,它是一種模板式設計。而介面是一種行為規範,它是一種輻射式設計。也就是說:
對於抽象類,如果需要新增新的方法,可以直接在抽象類中新增具體的實現,子類可以不進行變更;
而對於介面則不行,如果介面進行了變更,則所有實現這個介面的類都必須進行相應的改動。
實際應用上的差異
在實際使用中,使用抽象類(也就是繼承),是一種強耦合的設計,用來描述A is a B
的關係,即如果說A繼承於B,那麼在程式碼中將A當做B去使用應該完全沒有問題。比如在Android中,各種控制元件都可以被當做View去處理。
如果在你設計中有兩個型別的關係並不是is a
,而是is like a
,那就必須慎重考慮繼承。因為一旦我們使用了繼承,就要小心處理好子類跟父類的耦合依賴關係。組合優於繼承。
7.6 繼承
繼承是面向物件程式設計的一個重要的方式,因為通過繼承,子類就可以擴充套件父類的功能。
在Kotlin中,所有的類會預設繼承Any這個父類,但Any並不完全等同於java中的Object類,因為它只有equals(),hashCode()和toString()這三個方法。
7.6.1 open類
除了抽象類、介面預設可以被繼承(實現)外,我們也可以把一個類宣告為open的,這樣我們就可以繼承這個open類。
當我們想定義一個父類時,需要使用open關鍵字:
open class Base{
}
當然,抽象類是預設open的。
然後在子類中使用冒號:
進行繼承
class SubClass : Base(){
}
如果父類有建構函式,那麼必須在子類的主建構函式中進行繼承,沒有的話則可以選擇主建構函式或二級建構函式
//父類
open class Base(type:String){
}
//子類
class SubClass(type:String) : Base(type){
}
Kotlin中的override
重寫和java中也有所不同,因為Kotlin提倡所有的操作都是明確的,因此需要將希望被重寫的函式設為open:
open fun doSomething() {}
然後通過override標記實現重寫
override fun doSomething() {
super.doSomething()
}
同樣的,抽象函式以及介面中定義的函式預設都是open的。
override重寫的函式也是open的,如果希望它不被重寫,可以在前面增加final :
open class SubClass : Base{
constructor(type:String) : super(type){
}
final override fun doSomething() {
super.doSomething()
}
}
7.6.2 多重繼承
有些程式語言支援一個類擁有多個父類,例如C++。 我們將這個特性稱之為多重繼承(multiple inheritance)。多重繼承會有二義性和鑽石型繼承樹(DOD:Diamond Of Death)的複雜性問題。Kotlin跟Java一樣,沒有采用多繼承,任何一個子類僅允許一個父類存在,而在多繼承的問題場景下,使用實現多個interface 組合的方式來實現多繼承的功能。
程式碼示例:
package com.easy.kotlin
abstract class Animal {
fun doEat() {
println("Animal Eating")
}
}
abstract class Plant {
fun doEat() {
println("Plant Eating")
}
}
interface Runnable {
fun doRun()
}
interface Flyable {
fun doFly()
}
class Dog : Animal(), Runnable {
override fun doRun() {
println("Dog Running")
}
}
class Eagle : Animal(), Flyable {
override fun doFly() {
println("Eagle Flying")
}
}
// 始祖鳥, 能飛也能跑
class Archaeopteryx : Animal(), Runnable, Flyable {
override fun doRun() {
println("Archaeopteryx Running")
}
override fun doFly() {
println("Archaeopteryx Flying")
}
}
fun main(args: Array<String>) {
val d = Dog()
d.doEat()
d.doRun()
val e = Eagle()
e.doEat()
e.doFly()
val a = Archaeopteryx()
a.doEat()
a.doFly()
a.doRun()
}
上述程式碼類之間的關係,我們用圖示如下:
我們可以看出,Archaeopteryx繼承了Animal類,用了父類doEat()函式功能;實現了Runnable介面,擁有了doRun()函式規範;實現了Flyable介面,擁有了doFly()函式規範。
在這裡,我們通過實現多個介面,組合完成了的多個功能,而不是設計多個層次的複雜的繼承關係。
7.7 列舉類
Kotlin的列舉類定義如下:
public abstract class Enum<E : Enum<E>>(name: String, ordinal: Int): Comparable<E> {
companion object {}
public final val name: String
public final val ordinal: Int
public override final fun compareTo(other: E): Int
protected final fun clone(): Any
public override final fun equals(other: Any?): Boolean
public override final fun hashCode(): Int
public override fun toString(): String
}
我們可以看出,這個列舉類有兩個屬性:
public final val name: String
public final val ordinal: Int
分別表示的是列舉物件的值跟下標位置。
同時,我們可以看出列舉類還實現了Comparable介面。
7.7.1 列舉類基本用法
列舉類的最基本的用法是實現型別安全的列舉:
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
>>> val north = Direction.NORTH
>>> north.name
NORTH
>>> north.ordinal
0
>>> north is Direction
true
每個列舉常量都是一個物件。列舉常量用逗號分隔。
7.7.2 初始化列舉值
我們可以如下初始化列舉類的值:
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
>>> val red = Color.RED
>>> red.rgb
16711680
另外,列舉常量也可以宣告自己的匿名類:
enum class ActivtyLifeState {
onCreate {
override fun signal() = onStart
},
onStart {
override fun signal() = onStop
},
onStop {
override fun signal() = onStart
},
onDestroy {
override fun signal() = onDestroy
};
abstract fun signal(): ActivtyLifeState
}
>>> val s = ActivtyLifeState.onCreate
>>> println(s.signal())
onStart
7.7.3 使用列舉常量
我們使用enumValues()函式來列出列舉的所有值:
@SinceKotlin("1.1")
public inline fun <reified T : Enum<T>> enumValues(): Array<T>
每個列舉常量,預設都name
名稱和ordinal
位置的屬性(這個跟Java的Enum類裡面的類似):
val name: String
val ordinal: Int
程式碼示例:
enum class RGB { RED, GREEN, BLUE }
>>> val rgbs = enumValues<RGB>().joinToString { "${it.name} : ${it.ordinal} " }
>>> rgbs
RED : 0 , GREEN : 1 , BLUE : 2
我們直接聲明瞭一個簡單列舉類,我們使用遍歷函式enumValues<RGB>()
列出了RGB列舉類的所有列舉值。使用it.name
it.ordinal
直接訪問各個列舉值的名稱和位置。
另外,我們也可以自定義列舉屬性值:
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
>>> val colors = enumValues<Color>().joinToString { "${it.rgb} : ${it.name} : ${it.ordinal} " }
>>> colors
16711680 : RED : 0 , 65280 : GREEN : 1 , 255 : BLUE : 2
然後,我們可以直接使用it.rgb
訪問屬性名來得到對應的屬性值。
7.8 註解類
Kotlin 的註解與 Java 的註解完全相容。
7.8.1 宣告註解
annotation class 註解名
程式碼示例:
@Target(AnnotationTarget.CLASS,
AnnotationTarget.FUNCTION,
AnnotationTarget.EXPRESSION,
AnnotationTarget.FIELD,
AnnotationTarget.LOCAL_VARIABLE,
AnnotationTarget.TYPE,
AnnotationTarget.TYPEALIAS,
AnnotationTarget.TYPE_PARAMETER,
AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
@Repeatable
annotation class MagicClass
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
@Repeatable
annotation class MagicFunction
@Target(AnnotationTarget.CONSTRUCTOR)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
@Repeatable
annotation class MagicConstructor
在上面的程式碼中,我們通過向註解類新增元註解(meta-annotation)的方法來指定其他屬性:
- @Target :指定這個註解可被用於哪些元素(類, 函式, 屬性, 表示式, 等等.);
- @Retention :指定這個註解的資訊是否被儲存到編譯後的 class 檔案中, 以及在執行時是否可以通過反
射訪問到它; - @Repeatable:允許在單個元素上多次使用同一個註解;
- @MustBeDocumented : 表示這個註解是公開 API 的一部分, 在自動產生的 API 文件的類或者函式簽名中, 應該包含這個註解的資訊。
這幾個註解定義在kotlin/annotation/Annotations.kt
類中。
7.8.2 使用註解
註解可以用在類、函式、引數、變數(成員變數、區域性變數)、表示式、型別上等。這個由該註解的元註解@Target定義。
@MagicClass class Foo @MagicConstructor constructor() {
constructor(index: Int) : this() {
this.index = index
}
@MagicClass var index: Int = 0
@MagicFunction fun magic(@MagicClass name: String) {
}
}
註解在主構造器上,主構造器必須加上關鍵字 “constructor”
@MagicClass class Foo @MagicConstructor constructor() {
...
}
7.9 單例模式(Singleton)與伴生物件(companion object)
7.9.1 單例模式(Singleton)
單例模式很常用。它是一種常用的軟體設計模式。例如,Spring中的Bean預設就是單例。通過單例模式可以保證系統中一個類只有一個例項。即一個類只有一個物件例項。
我們用Java實現一個簡單的單例類的程式碼如下: