在前面的章節(jié)中,我們學(xué)習(xí)了Kotlin的語言基礎(chǔ)知識(shí)、類型系統(tǒng)、集合類以及泛型相關(guān)的知識(shí)。在本章節(jié)以及下一章中,我們將一起來學(xué)習(xí)Kotlin對(duì)面向?qū)ο缶幊桃约昂瘮?shù)式編程的支持。
7.1 面向?qū)ο缶幊趟枷?/h2>
7.1.1 一切皆是映射
《易傳·系辭上傳》:“易有太極,是生兩儀,兩儀生四象,四象生八卦。” 如今的互聯(lián)網(wǎng)世界,其基石卻是01(陰陽),不得不佩服我華夏先祖的博大精深的智慧。
一切皆是映射
計(jì)算機(jī)領(lǐng)域中的所有問題,都可以通過向上一層進(jìn)行抽象封裝來解決.這里的封裝的本質(zhì)概念,其實(shí)就是“映射”。
就好比通過的電子電路中的電平進(jìn)行01邏輯映射,于是有了布爾代數(shù),數(shù)字邏輯電路系統(tǒng);
對(duì)01邏輯的進(jìn)一步封裝抽象成CPU指令集映射,誕生了匯編語言;
通過匯編語言的向上抽象一層編譯解釋器,于是有了pascal,fortran,C語言;
再對(duì)核心函數(shù)api進(jìn)行封裝形成開發(fā)包(Development Kit), 于是有了Java,C++ 。
從面向過程到面向?qū)ο螅俚皆O(shè)計(jì)模式,架構(gòu)設(shè)計(jì),面向服務(wù),Sass/Pass/Iass等等的思想,各種軟件理論思想五花八門,但萬變不離其宗。
- 你要解決一個(gè)怎樣的問題?
- 你的問題領(lǐng)域是怎樣的?
- 你的模型(數(shù)據(jù)結(jié)構(gòu))是什么?
- 你的算法是什么?
- 你對(duì)這個(gè)世界的本質(zhì)認(rèn)知是怎樣的?
- 你的業(yè)務(wù)領(lǐng)域的邏輯問題,流程是什么?
Grady Booch:我對(duì)OO編程的目標(biāo)從來就不是復(fù)用。相反,對(duì)我來說,對(duì)象提供了一種處理復(fù)雜性的方式。這個(gè)問題可以追溯到亞里士多德:您把這個(gè)世界視為過程還是對(duì)象?在OO興起運(yùn)動(dòng)之前,編程以過程為中心--例如結(jié)構(gòu)化設(shè)計(jì)方法。然而,系統(tǒng)已經(jīng)到達(dá)了超越其處理能力的復(fù)雜性極點(diǎn)。有了對(duì)象,我們能夠通過提升抽象級(jí)別來構(gòu)建更大的、更復(fù)雜的系統(tǒng)--我認(rèn)為,這才是面向?qū)ο缶幊踢\(yùn)動(dòng)的真正勝利。
最初, 人們使用物理的或邏輯的二進(jìn)制機(jī)器指令來編寫程序, 嘗試著表達(dá)思想中的邏輯, 控制硬件計(jì)算和顯示, 發(fā)現(xiàn)是可行的;
接著, 創(chuàng)造了助記符 —— 匯編語言, 比機(jī)器指令更容易記憶;
再接著, 創(chuàng)造了編譯器、解釋器和計(jì)算機(jī)高級(jí)語言, 能夠以人類友好自然的方式去編寫程序, 在犧牲少量性能的情況下, 獲得比匯編語言更強(qiáng)且更容易使用的語句控制能力:條件、分支、循環(huán), 以及更多的語言特性: 指針、結(jié)構(gòu)體、聯(lián)合體、枚舉等, 還創(chuàng)造了函數(shù), 能夠?qū)⒁幌盗兄噶罘庋b成一個(gè)獨(dú)立的邏輯塊反復(fù)使用;
逐漸地,產(chǎn)生了面向過程的編程方法;
后來, 人們發(fā)現(xiàn)將數(shù)據(jù)和邏輯封裝成對(duì)象, 更接近于現(xiàn)實(shí)世界, 且更容易維護(hù)大型軟件, 又出現(xiàn)了面向?qū)ο蟮木幊陶Z言和編程方法學(xué), 增加了新的語言特性: 繼承、 多態(tài)、 模板、 異常錯(cuò)誤。
為了不必重復(fù)開發(fā)常見工具和任務(wù), 人們創(chuàng)造和封裝了容器及算法、SDK, 垃圾回收器, 甚至是并發(fā)庫;
為了讓計(jì)算機(jī)語言更有力更有效率地表達(dá)各種現(xiàn)實(shí)邏輯, 消解軟件開發(fā)中遇到的沖突, 還在語言中支持了元編程、 高階函數(shù), 閉包 等有用特性。
為了更高效率地開發(fā)可靠的軟件和應(yīng)用程序, 人們逐漸構(gòu)建了代碼編輯器、 IDE、 代碼版本管理工具、公共庫、應(yīng)用框架、 可復(fù)用組件、系統(tǒng)規(guī)范、網(wǎng)絡(luò)協(xié)議、 語言標(biāo)準(zhǔn)等, 針對(duì)遇到的問題提出了許多不同的思路和解決方案, 并總結(jié)提煉成特定的技術(shù)和設(shè)計(jì)模式, 還探討和形成了不少軟件開發(fā)過程, 用來保證最終發(fā)布的軟件質(zhì)量。 盡管編寫的這些軟件和工具還存在不少 BUG ,但是它們都“奇跡般地存活”, 并共同構(gòu)建了今天蔚為壯觀的互聯(lián)網(wǎng)時(shí)代的電商,互聯(lián)網(wǎng)金融,云計(jì)算,大數(shù)據(jù),物聯(lián)網(wǎng),機(jī)器智能等等的“虛擬世界”。
7.1.2 二進(jìn)制01與易經(jīng)陰陽
二進(jìn)制數(shù)是用0和1兩個(gè)數(shù)碼來表示的數(shù)。它的基數(shù)為2,進(jìn)位規(guī)則是“逢二進(jìn)一”,借位規(guī)則是“借一當(dāng)二”,由18世紀(jì)德國數(shù)理哲學(xué)大師萊布尼茲發(fā)現(xiàn)。當(dāng)前的計(jì)算機(jī)系統(tǒng)使用的基本上是二進(jìn)制系統(tǒng)。
19世紀(jì)愛爾蘭邏輯學(xué)家B對(duì)邏輯命題的思考過程轉(zhuǎn)化為對(duì)符號(hào)0,1的某種代數(shù)演算,二進(jìn)制是逢2進(jìn)位的進(jìn)位制。0、1是基本算符。因?yàn)樗皇褂?、1兩個(gè)數(shù)字符號(hào),非常簡單方便,易于用電子方式實(shí)現(xiàn)。
二進(jìn)制的發(fā)現(xiàn)直接導(dǎo)致了電子計(jì)算器和計(jì)算機(jī)的發(fā)明,并讓計(jì)算機(jī)得到了迅速的普及,進(jìn)入各行各業(yè),成為人類生活和生產(chǎn)的重要工具。
二進(jìn)制的實(shí)質(zhì)是通過兩個(gè)數(shù)字“0”和“1”來描述事件。在人類的生產(chǎn)、生活等許多領(lǐng)域,我們可以通過計(jì)算機(jī)來虛擬地描述現(xiàn)實(shí)中存在的事件,并能通過給定的條件和參數(shù)模擬事件變化的規(guī)律。二進(jìn)制的計(jì)算機(jī)幾乎是萬能的,能將我們生活的現(xiàn)實(shí)世界完美復(fù)制,并且還能根據(jù)我們?nèi)祟惤o定的條件模擬在現(xiàn)實(shí)世界難以實(shí)現(xiàn)的各種實(shí)驗(yàn)。
但是,不論計(jì)算機(jī)能給我們?nèi)绾味嘧儭⑷绾瓮昝?、如何?fù)雜的畫面,其本源只是簡單的“0”和“1”?!?”和“1”在計(jì)算機(jī)中通過不同的組合與再組合,模擬出一個(gè)紛繁復(fù)雜、包羅萬象的虛擬世界。我們簡單圖示如下:

二進(jìn)制的“0”和“1”通過計(jì)算機(jī)里能夠創(chuàng)造出一個(gè)虛擬的、紛繁的世界。自然界中的陰陽形成了現(xiàn)實(shí)世界的萬事萬物。
所以自然世界的“陰”“陽”作為基礎(chǔ)切實(shí)地造就了復(fù)雜的現(xiàn)實(shí)世界,計(jì)算機(jī)的“0”和“1”形象地模擬現(xiàn)實(shí)世界的一切現(xiàn)象,易學(xué)中的“卦”和“陰陽爻”抽象地揭示了自然界存在的事件和其變化規(guī)律。
所以說,編程的本質(zhì)跟大自然創(chuàng)造萬物的本質(zhì)是一樣的。
7.1.3 從面向過程到面向?qū)ο?/h3>
從IBM公司的約翰·巴庫斯在1957年開發(fā)出世界上第一個(gè)高級(jí)程序設(shè)計(jì)語言Fortran至今,高級(jí)程序設(shè)計(jì)語言的發(fā)展已經(jīng)經(jīng)歷了整整半個(gè)世紀(jì)。在這期間,程序設(shè)計(jì)語言主要經(jīng)歷了從面向過程(如C和Pascal語言)到面向?qū)ο螅ㄈ鏑++和Java語言),再到面向組件編程(如.NET平臺(tái)下的C#語言),以及面向服務(wù)架構(gòu)技術(shù)(如SOA、Service以及最近很火的微服務(wù)架構(gòu))等。
面向過程編程
結(jié)構(gòu)化編程思想的核心:功能分解(自頂向下,逐層細(xì)化)。
1971年4月份的 Communications of ACM上,尼古拉斯·沃斯(Niklaus Wirth,1934年2月15日—, 結(jié)構(gòu)化編程思想的創(chuàng)始人。因發(fā)明了Euler、Alogo-W、Modula和Pascal等一系列優(yōu)秀的編程語言并提出了結(jié)構(gòu)化編程思想而在1984年獲得了圖靈獎(jiǎng)。)發(fā)表了論文“通過逐步求精方式開發(fā)程序’(Program Development by Stepwise Refinement),首次提出“結(jié)構(gòu)化程序設(shè)計(jì)”(structure programming)的概念。
不要求一步就編制成可執(zhí)行的程序,而是分若干步進(jìn)行,逐步求精。
第一步編出的程序抽象度最高,第二步編出的程序抽象度有所降低…… 最后一步編出的程序即為可執(zhí)行的程序。
用這種方法編程,似乎復(fù)雜,實(shí)際上優(yōu)點(diǎn)很多,可使程序易讀、易寫、易調(diào)試、易維護(hù)、易保證其正確性及驗(yàn)證其正確性。
結(jié)構(gòu)化程序設(shè)計(jì)方法又稱為“自頂向下”或“逐步求精”法,在程序設(shè)計(jì)領(lǐng)域引發(fā)了一場革命,成為程序開發(fā)的一個(gè)標(biāo)準(zhǔn)方法,尤其是在后來發(fā)展起來的軟件工程中獲得廣泛應(yīng)用。有人評(píng)價(jià)說Wirth的結(jié)構(gòu)化程序設(shè)計(jì)概念“完全改變了人們對(duì)程序設(shè)計(jì)的思維方式”,這是一點(diǎn)也不夸張的。
尼古拉斯· 沃思教授在編程界提出了一個(gè)著名的公式:
程序 = 數(shù)據(jù)結(jié)構(gòu) + 算法
面向?qū)ο缶幊?/h4>
面向?qū)ο缶幊趟枷氲暮诵模簯?yīng)對(duì)變化,提高復(fù)用。
面向?qū)ο缶幊趟枷氲暮诵模簯?yīng)對(duì)變化,提高復(fù)用。
阿倫·凱(Alan Kay):面向?qū)ο缶幊趟枷氲膭?chuàng)始人。2003年因在面向?qū)ο缶幊躺纤龅木薮筘暙I(xiàn)而獲得圖靈獎(jiǎng)。
The best way to predict the future is to invent it,預(yù)測未來最好的方法是創(chuàng)造它?。ˋlan Kay)
阿倫·凱是Smalltalk面向?qū)ο缶幊陶Z言的發(fā)明人之一,也是面向?qū)ο缶幊趟枷氲膭?chuàng)始人之一,同時(shí),他還是筆記本電腦最早的構(gòu)想者和現(xiàn)代Windows GUI的建筑師。最早提出PC概念和互聯(lián)網(wǎng)的也是阿倫·凱,所以人們都尊稱他為“預(yù)言大師”。他是當(dāng)今IT界屈指可數(shù)的技術(shù)天才級(jí)人物。
面向?qū)ο缶幊趟枷胫饕菑?fù)用性和靈活性(彈性)。復(fù)用性是面向?qū)ο缶幊痰囊粋€(gè)主要機(jī)制。靈活性主要是應(yīng)對(duì)變化的特性,因?yàn)榭蛻舻男枨笫遣粩喔淖兊?,怎樣適應(yīng)客戶需求的變化,這是軟件設(shè)計(jì)靈活性或者說是彈性的問題。
Java是一種面向?qū)ο缶幊陶Z言,它基于Smalltalk語言,作為OOP語言,它具有以下五個(gè)基本特性:
- 萬物皆對(duì)象,每一個(gè)對(duì)象都會(huì)存儲(chǔ)數(shù)據(jù),并且可以對(duì)自身執(zhí)行操作。因此,每一個(gè)對(duì)象包含兩部分:成員變量和成員方法。在成員方法中可以改變成員變量的值。
- 程序是對(duì)象的集合,他們通過發(fā)送消息來告知彼此所要做的事情,也就是調(diào)用相應(yīng)的成員函數(shù)。
- 每一個(gè)對(duì)象都有自己的由其他對(duì)象所構(gòu)成的存儲(chǔ),也就是說在創(chuàng)建新對(duì)象的時(shí)候可以在成員變量中使用已存在的對(duì)象。
- 每個(gè)對(duì)象都擁有其類型,每個(gè)對(duì)象都是某個(gè)類的一個(gè)實(shí)例,每一個(gè)類區(qū)別于其它類的特性就是可以向它發(fā)送什么類型的消息,也就是它定義了哪些成員函數(shù)。
- 某一個(gè)特定類型的所有對(duì)象都可以接受同樣的消息。另一種對(duì)對(duì)象的描述為:對(duì)象具有狀態(tài)(數(shù)據(jù),成員變量)、行為(操作,成員方法)和標(biāo)識(shí)(成員名,內(nèi)存地址)。
面向?qū)ο笳Z言其實(shí)是對(duì)現(xiàn)實(shí)生活中的實(shí)物的抽象。
每個(gè)對(duì)象能夠接受的請(qǐng)求(消息)由對(duì)象的接口所定義,而在程序中必須由滿足這些請(qǐng)求的代碼,這段代碼稱之為這個(gè)接口的實(shí)現(xiàn)。當(dāng)向某個(gè)對(duì)象發(fā)送消息(請(qǐng)求)時(shí),這個(gè)對(duì)象便知道該消息的目的(該方法的實(shí)現(xiàn)已定義),然后執(zhí)行相應(yīng)的代碼。
我們經(jīng)常說一些代碼片段是優(yōu)雅的或美觀的,實(shí)際上意味著它們更容易被人類有限的思維所處理。
對(duì)于程序的復(fù)合而言,好的代碼是它的表面積要比體積增長的慢。
代碼塊的“表面積”是是我們復(fù)合代碼塊時(shí)所需要的信息(接口API協(xié)議定義)。代碼塊的“體積”就是接口內(nèi)部的實(shí)現(xiàn)邏輯(API背后的實(shí)現(xiàn)代碼)。
在面向?qū)ο缶幊讨校粋€(gè)理想的對(duì)象應(yīng)該是只暴露它的抽象接口(純表面, 無體積),其方法則扮演箭頭的角色。如果為了理解一個(gè)對(duì)象如何與其他對(duì)象進(jìn)行復(fù)合,當(dāng)你發(fā)現(xiàn)不得不深入挖掘?qū)ο蟮膶?shí)現(xiàn)之時(shí),此時(shí)你所用的編程范式的原本優(yōu)勢(shì)就蕩然無存了。
面向組件和面向服務(wù)
- 面向組件
我們知道面向?qū)ο笾С种赜茫侵赜玫膯卧苄?,一般是類;而面向組件則不同,它可以重用多個(gè)類甚至一個(gè)程序。也就是說面向組件支持更大范圍內(nèi)的重用,開發(fā)效率更高。如果把面向?qū)ο蟊茸髦赜昧慵?,那么面向組件則是重用部件。
- 面向服務(wù)
將系統(tǒng)進(jìn)行功能化,每個(gè)功能提供一種服務(wù)?,F(xiàn)在非常流行微服務(wù)MicroService技術(shù)以及SOA(面向服務(wù)架構(gòu))技術(shù)。
面向過程(Procedure)→面向?qū)ο螅∣bject)→ 面向組件(Component) →面向服務(wù)(Service)
正如解決數(shù)學(xué)問題通常我們會(huì)談“思想”,諸如反證法、化繁為簡等,解決計(jì)算機(jī)問題也有很多非常出色的思想。思想之所以稱為思想,是因?yàn)椤八枷搿庇型卣剐耘c引導(dǎo)性,可以解決一系列問題。
解決問題的復(fù)雜程度直接取決于抽象的種類及質(zhì)量。過將結(jié)構(gòu)、性質(zhì)不同的底層實(shí)現(xiàn)進(jìn)行封裝,向上提供統(tǒng)一的API接口,讓使用者覺得就是在使用一個(gè)統(tǒng)一的資源,或者讓使用者覺得自己在使用一個(gè)本來底層不直接提供、“虛擬”出來的資源。
計(jì)算機(jī)中的所有問題 , 都可以通過向上抽象封裝一層來解決。同樣的,任何復(fù)雜的問題, 最終總能夠回歸最本質(zhì),最簡單。
面向?qū)ο缶幊淌且环N自頂向下的程序設(shè)計(jì)方法。萬事萬物都是對(duì)象,對(duì)象有其行為(方法),狀態(tài)(成員變量,屬性)。OOP是一種編程思想,而不是針對(duì)某個(gè)語言而言的。當(dāng)然,語言影響思維方式,思維依賴語言的表達(dá),這也是辯證的來看。
所謂“面向?qū)ο笳Z言”,其實(shí)經(jīng)典的“過程式語言”(比如Pascal,C),也能體現(xiàn)面向?qū)ο蟮乃枷?。所謂“類”和“對(duì)象”,就是C語言里面的抽象數(shù)據(jù)類型結(jié)構(gòu)體(struct)。
而面向?qū)ο蟮亩鄳B(tài)是唯一相比struct多付出的代價(jià),也是最重要的特性。這就是SmallTalk、Java這樣的面向?qū)ο笳Z言所提供的特性。
回到一個(gè)古老的話題:程序是什么?
在面向?qū)ο蟮木幊淌澜缋铮旅娴倪@個(gè)公式
程序 = 算法 + 數(shù)據(jù)結(jié)構(gòu)
可以簡單重構(gòu)成:
程序 = 基于對(duì)象操作的算法 + 以對(duì)象為最小單位的數(shù)據(jù)結(jié)構(gòu)
封裝總是為了減少操作粒度,數(shù)據(jù)結(jié)構(gòu)上的封裝導(dǎo)致了數(shù)據(jù)的減少,自然減少了問題求解的復(fù)雜度;對(duì)代碼的封裝使得代碼得以復(fù)用,減少了代碼的體積,同樣使問題簡化。這個(gè)時(shí)候,算法操作的就是一個(gè)抽象概念的集合。
在面向?qū)ο蟮某绦蛟O(shè)計(jì)中,我們便少不了集合類容器。容器就用來存放一類有共同抽象概念的東西。這里說有共同概念的東西(而沒有說對(duì)象),其實(shí),就是我們上一個(gè)章節(jié)中講到的泛型。這樣對(duì)于一個(gè)通用的算法,我們就可以最大化的實(shí)現(xiàn)復(fù)用,作用于的集合。
面向?qū)ο蟮谋举|(zhì)就是讓對(duì)象有多態(tài)性,把不同對(duì)象以同一特性來歸組,統(tǒng)一處理。至于所謂繼承、虛表、等等概念,只是其實(shí)現(xiàn)的細(xì)節(jié)。
在遵循這些面向?qū)ο笤O(shè)計(jì)原則基礎(chǔ)上,前輩們總結(jié)出一些解決不同問題場景的設(shè)計(jì)模式,以GOF的23中設(shè)計(jì)模式最為知名。
我們用一幅圖簡單概括一下面向?qū)ο缶幊痰闹R(shí)框架:

講了這么多思考性的思想層面的東西,我們下面來開始Kotlin的面向?qū)ο缶幊痰膶W(xué)習(xí)。Kotlin對(duì)面向?qū)ο缶幊淌峭耆С值摹?/p>
7.2 類與構(gòu)造函數(shù)
Kotlin和Java很相似,也是一種面向?qū)ο蟮恼Z言。下面我們來一起學(xué)習(xí)Kotlin的面向?qū)ο蟮奶匦浴H绻煜ava或者C++、C#中的類,您可以很快上手。同時(shí),您也將看到Kotlin與Java中的面向?qū)ο缶幊痰囊恍┎煌奶匦浴?/p>
Kotlin中的類和接口跟Java中對(duì)應(yīng)的概念有些不同,比如接口可以包含屬性聲明;Kotlin的類聲明,默認(rèn)是final和public的。
另外,嵌套類并不是默認(rèn)在內(nèi)部的。它們不包含外部類的隱式引用。
在構(gòu)造函數(shù)方面,Kotlin簡短的主構(gòu)造函數(shù)在大多數(shù)情況下都可以滿足使用,當(dāng)然如果有稍微復(fù)雜的初始化邏輯,我們也可以聲明次級(jí)構(gòu)造函數(shù)來完成。
我們還可以使用 data 修飾符來聲明一個(gè)數(shù)據(jù)類,使用 object 關(guān)鍵字來表示單例對(duì)象、伴生對(duì)象等。
Kotlin類的成員可以包含:
- 構(gòu)造函數(shù)和初始化塊
- 屬性
- 函數(shù)
- 嵌套類和內(nèi)部類
- 對(duì)象聲明
7.2.1 聲明類
和大部分語言類似,Kotlin使用class作為類的關(guān)鍵字,當(dāng)我們聲明一個(gè)類時(shí),直接通過class加類名的方式來實(shí)現(xiàn):
class World
這樣我們就聲明了一個(gè)World類。
7.2.2 構(gòu)造函數(shù)
在 Kotlin 中,一個(gè)類可以有
- 一個(gè)主構(gòu)造函數(shù)(primary constructor)
- 一個(gè)或多個(gè)次構(gòu)造函數(shù)(secondary constructor)
主構(gòu)造函數(shù)
主構(gòu)造函數(shù)是類頭的一部分,直接放在類名后面:
open class Student constructor(var name: String, var age: Int) : Any() {
...
}
如果主構(gòu)造函數(shù)沒有任何注解或者可見性修飾符,可以省略這個(gè) constructor 關(guān)鍵字。如果構(gòu)造函數(shù)有注解或可見性修飾符,這個(gè) constructor 關(guān)鍵字是必需的,并且這些修飾符在它前面:
annotation class MyAutowired
class ElementaryStudent public @MyAutowired constructor(name: String, age: Int) : Student(name, age) {
...
}
與普通屬性一樣,主構(gòu)造函數(shù)中聲明的屬性可以是可變的(var)或只讀的(val)。
主構(gòu)造函數(shù)不能包含任何的代碼。初始化的代碼可以放到以 init 關(guān)鍵字作為前綴的初始化塊(initializer blocks)中:
open class Student constructor(var name: String, var age: Int) : Any() {
init {
println("Student{name=$name, age=$age} created!")
}
...
}
主構(gòu)造的參數(shù)可以在初始化塊中使用,也可以在類體內(nèi)聲明的屬性初始化器中使用。
次構(gòu)造函數(shù)
在類體中,我們也可以聲明前綴有 constructor 的次構(gòu)造函數(shù),次構(gòu)造函數(shù)不能有聲明 val 或 var :
class MiddleSchoolStudent {
constructor(name: String, age: Int) {
}
}
如果類有一個(gè)主構(gòu)造函數(shù),那么每個(gè)次構(gòu)造函數(shù)需要委托給主構(gòu)造函數(shù), 委托到同一個(gè)類的另一個(gè)構(gòu)造函數(shù)用 this 關(guān)鍵字即可:
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
}
...
}
如果一個(gè)非抽象類沒有聲明任何(主或次)構(gòu)造函數(shù),它會(huì)有一個(gè)生成的不帶參數(shù)的主構(gòu)造函數(shù)。構(gòu)造函數(shù)的可見性是 public。
私有主構(gòu)造函數(shù)
我們?nèi)绻M@個(gè)構(gòu)造函數(shù)是私有的,我們可以如下聲明:
class DontCreateMe private constructor() {
}
這樣我們?cè)诖a中,就無法直接使用主構(gòu)造函數(shù)來實(shí)例化這個(gè)類,下面的寫法是不允許的:
val dontCreateMe = DontCreateMe() // cannot access it
但是,我們可以通過次構(gòu)造函數(shù)引用這個(gè)私有主構(gòu)造函數(shù)來實(shí)例化對(duì)象:
7.2.2 類的屬性
我們?cè)俳o這個(gè)World類加入兩個(gè)屬性。我們可能直接簡單地寫成:
class World1 {
val yin: Int
val yang: Int
}
在Kotlin中,直接這樣寫語法上是會(huì)報(bào)錯(cuò)的:

意思很明顯,是說這個(gè)類的屬性必須要初始化,或者如果不初始化那就得是抽象的abstract屬性。
我們把這兩個(gè)屬性都給初始化如下:
class World1 {
val yin: Int = 0
val yang: Int = 1
}
我們?cè)賮硎褂脺y試代碼來看下訪問這兩個(gè)屬性的方式:
>>> class World1 {
... val yin: Int = 0
... val yang: Int = 1
... }
>>> val w1 = World1()
>>> w1.yin
0
>>> w1.yang
1
上面的World1類的代碼,在Java中等價(jià)的寫法是:
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中的類的字段自動(dòng)帶有g(shù)etter方法和setter方法。而且寫起來比Java要簡潔的多。
7.2.3 函數(shù)(方法)
我們?cè)賮斫o這個(gè)World1類中加上一個(gè)函數(shù):
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 抽象類的定義
含有抽象函數(shù)的類(這樣的類需要使用abstract修飾符來聲明),稱為抽象類。
下面是一個(gè)抽象類的例子:
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 抽象函數(shù)
在上面的這個(gè)抽象類中,不僅可以有抽象函數(shù)abstract fun doEat()
abstract fun doWalk()
,同時(shí)可以有具體實(shí)現(xiàn)的函數(shù)fun doSwim()
, 這個(gè)函數(shù)默認(rèn)是final的。也就是說,我們不能重寫這個(gè)doSwim函數(shù):

如果一個(gè)函數(shù)想要設(shè)計(jì)成能被重寫,例如fun doSleep()
,我們給它加上open關(guān)鍵字即可。然后,我們就可以在子類中重寫這個(gè)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 ... ")
// }
}
抽象函數(shù)是一種特殊的函數(shù):它只有聲明,而沒有具體的實(shí)現(xiàn)。抽象函數(shù)的聲明格式為:
abstract fun doEat()
關(guān)于抽象函數(shù)的特征,我們簡單總結(jié)如下:
- 抽象函數(shù)必須用abstract關(guān)鍵字進(jìn)行修飾
- 抽象函數(shù)不用手動(dòng)添加open,默認(rèn)被open修飾
- 抽象函數(shù)沒有具體的實(shí)現(xiàn)
- 含有抽象函數(shù)的類成為抽象類,必須由abtract關(guān)鍵字修飾。抽象類中可以有具體實(shí)現(xiàn)的函數(shù),這樣的函數(shù)默認(rèn)是final(不能被覆蓋重寫),如果想要重寫這個(gè)函數(shù),給這個(gè)函數(shù)加上open關(guān)鍵字。
7.3.3 抽象屬性
抽象屬性就是在var或val前被abstract修飾,抽象屬性的聲明格式為:
abstract var addr : String
abstract val weight : Float
關(guān)于抽象屬性,需要注意的是:
- 抽象屬相在抽象類中不能被初始化
- 如果在子類中沒有主構(gòu)造函數(shù),要對(duì)抽象屬性手動(dòng)初始化。如果子類中有主構(gòu)造函數(shù),抽象屬性可以在主構(gòu)造函數(shù)中聲明。
綜上所述,抽象類和普通類的區(qū)別有:
1.抽象函數(shù)必須為public或者protected(因?yàn)槿绻麨閜rivate,則不能被子類繼承,子類便無法實(shí)現(xiàn)該方法),缺省情況下默認(rèn)為public。
也就是說,這三個(gè)函數(shù)
abstract fun doEat()
abstract fun doWalk()
fun doSwim() {
println("I am Swimming ... ")
}
默認(rèn)的都是public的。
另外抽象類中的具體實(shí)現(xiàn)的函數(shù),默認(rèn)是final的。上面的三個(gè)函數(shù),等價(jià)的Java的代碼如下:
public abstract void doEat();
public abstract void doWalk();
public final void doSwim() {
String var1 = "I am Swimming ... ";
System.out.println(var1);
}
2.抽象類不能用來創(chuàng)建對(duì)象實(shí)例。也就是說,下面的寫法編譯器是不允許的:

3.如果一個(gè)類繼承于一個(gè)抽象類,則子類必須實(shí)現(xiàn)父類的抽象方法。實(shí)現(xiàn)父類抽象函數(shù),我們使用override關(guān)鍵字來表明是重寫函數(shù):
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 ... ")
}
}
如果子類沒有實(shí)現(xiàn)父類的抽象函數(shù),則必須將子類也定義為為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函數(shù)沒有實(shí)現(xiàn)父類的抽象函數(shù),那么我們?cè)谧宇愔邪阉廊欢x為抽象函數(shù)。相應(yīng)地這個(gè)子類,也成為了抽象子類,需要使用abstract關(guān)鍵字來聲明。
如果抽象類中含有抽象屬性,在實(shí)現(xiàn)子類中必須將抽象屬性初始化,除非子類也為抽象類。例如我們聲明一個(gè)Teacher類繼承Person類:
class Teacher(name: String, age: Int) : Person(name, age) {
override var addr: String // error, 需要初始化,或者聲明為abstract
override val weight: Float // error, 需要初始化,或者聲明為abstract
...
}
這樣寫,編譯器會(huì)直接報(bào)錯(cuò):

解決方法是,在實(shí)現(xiàn)的子類中,我們將抽象屬性初始化即可:
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作為接口的關(guān)鍵詞:
interface ProjectService
Kotlin 的接口與 Java 8 的接口類似。與抽象類相比,他們都可以包含抽象的方法以及方法的實(shí)現(xiàn):
interface ProjectService {
val name: String
val owner: String
fun save(project: Project)
fun print() {
println("I am project")
}
}
7.4.2 實(shí)現(xiàn)接口
接口是沒有構(gòu)造函數(shù)的。我們使用冒號(hào):
語法來實(shí)現(xiàn)一個(gè)接口,如果有多個(gè)用,
逗號(hào)隔開:
class ProjectServiceImpl : ProjectService
class ProjectMilestoneServiceImpl : ProjectService, MilestoneService
我們也可以實(shí)現(xiàn)多個(gè)接口:
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")
}
}
當(dāng)子類繼承了某個(gè)類之后,便可以使用父類中的成員變量,但是并不是完全繼承父類的所有成員變量。具體的原則如下:
1.能夠繼承父類的public和protected成員變量;不能夠繼承父類的private成員變量;
2.對(duì)于父類的包訪問權(quán)限成員變量,如果子類和父類在同一個(gè)包下,則子類能夠繼承;否則,子類不能夠繼承;
3.對(duì)于子類可以繼承的父類成員變量,如果在子類中出現(xiàn)了同名稱的成員變量,則會(huì)發(fā)生隱藏現(xiàn)象,即子類的成員變量會(huì)屏蔽掉父類的同名成員變量。如果要在子類中訪問父類中同名成員變量,需要使用super關(guān)鍵字來進(jìn)行引用。
7.4.3 覆蓋沖突
在kotlin中, 實(shí)現(xiàn)繼承通常遵循如下規(guī)則:如果一個(gè)類從它的直接父類繼承了同一個(gè)函數(shù)的多個(gè)實(shí)現(xiàn),那么它必須重寫這個(gè)函數(shù)并且提供自己的實(shí)現(xiàn)(或許只是直接用了繼承來的實(shí)現(xiàn)) 為表示使用父類中提供的方法我們用 super 表示。
在重寫print()
時(shí),因?yàn)槲覀儗?shí)現(xiàn)的ProjectService、MilestoneService都有一個(gè)print()
函數(shù),當(dāng)我們直接使用super.print()
時(shí),編譯器是無法知道我們想要調(diào)用的是那個(gè)里面的print函數(shù)的,這個(gè)我們叫做覆蓋沖突:

這個(gè)時(shí)候,我們可以使用下面的語法來調(diào)用:
super<ProjectService>.print()
super<MilestoneService>.print()
7.4.4 接口中的屬性
在接口中聲明的屬性,可以是抽象的,或者是提供訪問器的實(shí)現(xiàn)。
在企業(yè)應(yīng)用中,大多數(shù)的類型都是無狀態(tài)的,如:Controller、ApplicationService、DomainService、Repository等。
因?yàn)榻涌跊]有狀態(tài), 所以它的屬性是無狀態(tài)的。
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 抽象類和接口的差異
概念上的區(qū)別
接口主要是對(duì)動(dòng)作的抽象,定義了行為特性的規(guī)約。
抽象類是對(duì)根源的抽象。當(dāng)你關(guān)注一個(gè)事物的本質(zhì)的時(shí)候,用抽象類;當(dāng)你關(guān)注一個(gè)操作的時(shí)候,用接口。
語法層面上的區(qū)別
接口不能保存狀態(tài),可以有屬性但必須是抽象的。
一個(gè)類只能繼承一個(gè)抽象類,而一個(gè)類卻可以實(shí)現(xiàn)多個(gè)接口。
類如果要實(shí)現(xiàn)一個(gè)接口,它必須要實(shí)現(xiàn)接口聲明的所有方法。但是,類可以不實(shí)現(xiàn)抽象類聲明的所有方法,當(dāng)然,在這種情況下,類也必須得聲明成是抽象的。
接口中所有的方法隱含的都是抽象的。而抽象類則可以同時(shí)包含抽象和非抽象的方法。
設(shè)計(jì)層面上的區(qū)別
抽象類是對(duì)一種事物的抽象,即對(duì)類抽象,而接口是對(duì)行為的抽象。抽象類是對(duì)整個(gè)類整體進(jìn)行抽象,包括屬性、行為,但是接口卻是對(duì)類局部(行為)進(jìn)行抽象。
繼承是 is a
的關(guān)系,而 接口實(shí)現(xiàn)則是 has a
的關(guān)系。如果一個(gè)類繼承了某個(gè)抽象類,則子類必定是抽象類的種類,而接口實(shí)現(xiàn)就不需要有這層類型關(guān)系。
設(shè)計(jì)層面不同,抽象類作為很多子類的父類,它是一種模板式設(shè)計(jì)。而接口是一種行為規(guī)范,它是一種輻射式設(shè)計(jì)。也就是說:
對(duì)于抽象類,如果需要添加新的方法,可以直接在抽象類中添加具體的實(shí)現(xiàn),子類可以不進(jìn)行變更;
而對(duì)于接口則不行,如果接口進(jìn)行了變更,則所有實(shí)現(xiàn)這個(gè)接口的類都必須進(jìn)行相應(yīng)的改動(dòng)。
實(shí)際應(yīng)用上的差異
在實(shí)際使用中,使用抽象類(也就是繼承),是一種強(qiáng)耦合的設(shè)計(jì),用來描述A is a B
的關(guān)系,即如果說A繼承于B,那么在代碼中將A當(dāng)做B去使用應(yīng)該完全沒有問題。比如在Android中,各種控件都可以被當(dāng)做View去處理。
如果在你設(shè)計(jì)中有兩個(gè)類型的關(guān)系并不是is a
,而是is like a
,那就必須慎重考慮繼承。因?yàn)橐坏┪覀兪褂昧死^承,就要小心處理好子類跟父類的耦合依賴關(guān)系。組合優(yōu)于繼承。
7.6 繼承
繼承是面向?qū)ο缶幊痰囊粋€(gè)重要的方式,因?yàn)橥ㄟ^繼承,子類就可以擴(kuò)展父類的功能。
在Kotlin中,所有的類會(huì)默認(rèn)繼承Any這個(gè)父類,但Any并不完全等同于java中的Object類,因?yàn)樗挥衑quals(),hashCode()和toString()這三個(gè)方法。
7.6.1 open類
除了抽象類、接口默認(rèn)可以被繼承(實(shí)現(xiàn))外,我們也可以把一個(gè)類聲明為open的,這樣我們就可以繼承這個(gè)open類。
當(dāng)我們想定義一個(gè)父類時(shí),需要使用open關(guān)鍵字:
open class Base{
}
當(dāng)然,抽象類是默認(rèn)open的。
然后在子類中使用冒號(hào):
進(jìn)行繼承
class SubClass : Base(){
}
如果父類有構(gòu)造函數(shù),那么必須在子類的主構(gòu)造函數(shù)中進(jìn)行繼承,沒有的話則可以選擇主構(gòu)造函數(shù)或二級(jí)構(gòu)造函數(shù)
//父類
open class Base(type:String){
}
//子類
class SubClass(type:String) : Base(type){
}
Kotlin中的override
重寫和java中也有所不同,因?yàn)镵otlin提倡所有的操作都是明確的,因此需要將希望被重寫的函數(shù)設(shè)為open:
open fun doSomething() {}
然后通過override標(biāo)記實(shí)現(xiàn)重寫
override fun doSomething() {
super.doSomething()
}
同樣的,抽象函數(shù)以及接口中定義的函數(shù)默認(rèn)都是open的。
override重寫的函數(shù)也是open的,如果希望它不被重寫,可以在前面增加final :
open class SubClass : Base{
constructor(type:String) : super(type){
}
final override fun doSomething() {
super.doSomething()
}
}
7.6.2 多重繼承
有些編程語言支持一個(gè)類擁有多個(gè)父類,例如C++。 我們將這個(gè)特性稱之為多重繼承(multiple inheritance)。多重繼承會(huì)有二義性和鉆石型繼承樹(DOD:Diamond Of Death)的復(fù)雜性問題。Kotlin跟Java一樣,沒有采用多繼承,任何一個(gè)子類僅允許一個(gè)父類存在,而在多繼承的問題場景下,使用實(shí)現(xiàn)多個(gè)interface 組合的方式來實(shí)現(xiàn)多繼承的功能。
代碼示例:
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()
}
上述代碼類之間的關(guān)系,我們用圖示如下:

我們可以看出,Archaeopteryx繼承了Animal類,用了父類doEat()函數(shù)功能;實(shí)現(xiàn)了Runnable接口,擁有了doRun()函數(shù)規(guī)范;實(shí)現(xiàn)了Flyable接口,擁有了doFly()函數(shù)規(guī)范。
在這里,我們通過實(shí)現(xiàn)多個(gè)接口,組合完成了的多個(gè)功能,而不是設(shè)計(jì)多個(gè)層次的復(fù)雜的繼承關(guān)系。
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
}
我們可以看出,這個(gè)枚舉類有兩個(gè)屬性:
public final val name: String
public final val ordinal: Int
分別表示的是枚舉對(duì)象的值跟下標(biāo)位置。
同時(shí),我們可以看出枚舉類還實(shí)現(xiàn)了Comparable<E>接口。
7.7.1 枚舉類基本用法
枚舉類的最基本的用法是實(shí)現(xiàn)類型安全的枚舉:
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
>>> val north = Direction.NORTH
>>> north.name
NORTH
>>> north.ordinal
0
>>> north is Direction
true
每個(gè)枚舉常量都是一個(gè)對(duì)象。枚舉常量用逗號(hào)分隔。
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()函數(shù)來列出枚舉的所有值:
@SinceKotlin("1.1")
public inline fun <reified T : Enum<T>> enumValues(): Array<T>
每個(gè)枚舉常量,默認(rèn)都name
名稱和ordinal
位置的屬性(這個(gè)跟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
我們直接聲明了一個(gè)簡單枚舉類,我們使用遍歷函數(shù)enumValues<RGB>()
列出了RGB枚舉類的所有枚舉值。使用it.name
it.ordinal
直接訪問各個(gè)枚舉值的名稱和位置。
另外,我們也可以自定義枚舉屬性值:
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
訪問屬性名來得到對(duì)應(yīng)的屬性值。
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 :指定這個(gè)注解可被用于哪些元素(類, 函數(shù), 屬性, 表達(dá)式, 等等.);
- @Retention :指定這個(gè)注解的信息是否被保存到編譯后的 class 文件中, 以及在運(yùn)行時(shí)是否可以通過反
射訪問到它; - @Repeatable:允許在單個(gè)元素上多次使用同一個(gè)注解;
- @MustBeDocumented : 表示這個(gè)注解是公開 API 的一部分, 在自動(dòng)產(chǎn)生的 API 文檔的類或者函數(shù)簽名中, 應(yīng)該包含這個(gè)注解的信息。
這幾個(gè)注解定義在kotlin/annotation/Annotations.kt
類中。
7.8.2 使用注解
注解可以用在類、函數(shù)、參數(shù)、變量(成員變量、局部變量)、表達(dá)式、類型上等。這個(gè)由該注解的元注解@Target定義。
@MagicClass class Foo @MagicConstructor constructor() {
constructor(index: Int) : this() {
this.index = index
}
@MagicClass var index: Int = 0
@MagicFunction fun magic(@MagicClass name: String) {
}
}
注解在主構(gòu)造器上,主構(gòu)造器必須加上關(guān)鍵字 “constructor”
@MagicClass class Foo @MagicConstructor constructor() {
...
}