第4章 類與面向對象編程
在前面的章節中,我們學習了kotlin的語言基礎知識、類型系統等相關的知識。在本章節以及下一章中,我們將一起來學習kotlin對面向對象編程以及函數式編程的支持。
4.1 面向對象編程簡史
50年代后期,在用FORTRAN語言編寫大型程序時,由于沒有封裝機制,那個時候的變量都是“全局變量”,那么就會不可避免的經常出現變量名沖突問題。在ALGOL60中采用了以 Begin – End 為標識的程序塊,使塊內變量名是局部的,以避免它們與程序中塊外的同名變量相沖突。在編程語言中首次提供了封裝(保護)的機制。此后,程序塊結構廣泛用于Pascal 、Ada、C等高級語言之中。
60年代中后期,Simula語言在ALGOL基礎上研制開發,它將ALGOL的塊結構概念向前發展一步,提出了對象的概念,并使用了類,也支持類繼承。其后的發展簡史如下圖所示:

面向對象發展簡史
阿倫·凱(Alan Kay)是Smalltalk面向對象編程語言的發明人之一,也是面向對象編程思想的創始人之一,同時,他還是筆記本電腦最早的構想者和現代Windows GUI的建筑師。最早提出PC概念和互聯網的也是阿倫·凱,所以人們都尊稱他為“預言大師”。他是當今IT界屈指可數的技術天才級人物。
面向對象編程思想主要是復用性和靈活性(彈性)。復用性是面向對象編程的一個主要機制。靈活性主要是應對變化的特性,因為客戶的需求是不斷改變的,怎樣適應客戶需求的變化,這是軟件設計靈活性或者說是彈性的問題。
Java是一種面向對象編程語言,它基于Smalltalk語言,作為OOP語言,它具有以下五個基本特性:
1.萬物皆對象,每一個對象都會存儲數據,并且可以對自身執行操作。因此,每一個對象包含兩部分:成員變量和成員方法。在成員方法中可以改變成員變量的值。
2.程序是對象的集合,他們通過發送消息來告知彼此所要做的事情,也就是調用相應的成員函數。
3.每一個對象都有自己的由其他對象所構成的存儲,也就是說在創建新對象的時候可以在成員變量中使用已存在的對象。
4.每個對象都擁有其類型,每個對象都是某個類的一個實例,每一個類區別于其它類的特性就是可以向它發送什么類型的消息,也就是它定義了哪些成員函數。
5.某一個特定類型的所有對象都可以接受同樣的消息。另一種對對象的描述為:對象具有狀態(數據,成員變量)、行為(操作,成員方法)和標識(成員名,內存地址)。
面向對象語言其實是對現實生活中的實物的抽象。
每個對象能夠接受的請求(消息)由對象的接口所定義,而在程序中必須由滿足這些請求的代碼,這段代碼稱之為這個接口的實現。當向某個對象發送消息(請求)時,這個對象便知道該消息的目的(該方法的實現已定義),然后執行相應的代碼。
我們經常說一些代碼片段是優雅的或美觀的,實際上意味著它們更容易被人類有限的思維所處理。
對于程序的復合而言,好的代碼是它的表面積要比體積增長的慢。
代碼塊的“表面積”是是我們復合代碼塊時所需要的信息(接口API協議定義)。代碼塊的“體積”就是接口內部的實現邏輯(API背后的實現代碼)。
在面向對象編程中,一個理想的對象應該是只暴露它的抽象接口(純表面, 無體積),其方法則扮演箭頭的角色。如果為了理解一個對象如何與其他對象進行復合,當你發現不得不深入挖掘對象的實現之時,此時你所用的編程范式的原本優勢就蕩然無存了。
面向對象編程是一種編程思想,相比于早期的結構化程序設計,抽象層次更高,思考解決問題的方式上也更加貼近人類的思維方式。現代編程語言基本都支持面向對象編程范式。
計算機領域中的所有問題,都可以通過向上一層進行抽象封裝來解決.這里的封裝的本質概念,其實就是“映射”。從面向過程到面向對象,再到設計模式,架構設計,面向服務,sass/Pass/Iass等等的思想,各種軟件理論思想五花八門,但萬變不離其宗——
你要解決一個怎樣的問題?你的問題領域是怎樣的?你的模型(數據結構)是什么?你的算法是什么?你對這個世界的本質認知是怎樣的?你的業務領域的邏輯問題,流程是什么? 等等。
面向對象編程的以現實世界中的事物(對象)為中心來思考, 認識問題, 并根據這些事物的本質特征, 把它們抽象表示為系統中的類。其核心思想可以用下圖簡要說明:

面向對象編程
面向對象編程基于類編程,更加貼近人類解決問題的習慣方法。讓軟件世界更像現實世界。面向對象編程通過抽象出關鍵的問題域來分解系統。對象不僅能表示具體的事物,還能表示抽象的規則、計劃或事件。關于面向對象編程的核心的概念如下圖所示

面向對象編程的核心的概念
4.2 聲明類
本節介紹Kotlin中類和構造函數的聲明。
4.2.1 空類
使用class關鍵字聲明類。我們可以聲明一個什么都不干的類
代碼語言:Javascript代碼運行次數:0運行復制
class AnEmptyClassfun main(args: Array<string>) { val anEmptyClass = AnEmptyClass() // Kotlin中不需要使用new println(anEmptyClass) println(anEmptyClass is AnEmptyClass) // 對象實例是AnEmptyClass類型 println(anEmptyClass::class)}</string>
輸出
代碼語言:JavaScript代碼運行次數:0運行復制
com.easy.kotlin.AnEmptyClass@2626b418trueclass com.easy.kotlin.AnEmptyClass (Kotlin reflection is not available)
4.2.2 聲明類和構造函數
在Kotlin中, 我們可以在聲明類的時候同時聲明構造函數,語法格式是在類的后面使用括號包含構造函數的參數列表
代碼語言:javascript代碼運行次數:0運行復制
class Person(var name: String, var age: Int, var sex: String) { // 聲明類和構造函數 override fun toString(): String { // override關鍵字,重寫toString() return "Person(name='$name', age=$age, sex='$sex')" }}
使用這樣的簡潔語法,可以通過主構造器來定義屬性并初始化屬性值(這里的屬性值可以是var或val)。
在代碼中這樣使用Person類
代碼語言:javascript代碼運行次數:0運行復制
val person = Person("Jack", 29, "M")println("person = ${person}")
輸出
代碼語言:javascript代碼運行次數:0運行復制
person = Person(name='Jack', age=29, sex='M')
另外,我們也可以先聲明屬性,等到構造實例對象的時候再去初始化屬性值,那么我們的Person類可以聲明如下
代碼語言:javascript代碼運行次數:0運行復制
class Person1 { lateinit var name: String // lateinit 關鍵字表示該屬性延遲初始化 var age: Int = 0 // lateinit 關鍵字不能修飾 primitive 類型 lateinit var sex: String override fun toString(): String { return "Person1(name='$name', age=$age, sex='$sex')" }}
我們可以在代碼中這樣創建Person1的實例對象
代碼語言:javascript代碼運行次數:0運行復制
val person1 = Person1() person1.name = "Jack" person1.age = 29 person1.sex = "M" println("person1 = ${person1}")
輸出
代碼語言:javascript代碼運行次數:0運行復制
person1 = Person1(name='Jack', age=29, sex='M')
如果我們想聲明一個具有多種構造方式的類,可以使用 constructor 關鍵字聲明構造函數,示例代碼如下
代碼語言:javascript代碼運行次數:0運行復制
class Person2() { // 無參的主構造函數 lateinit var name: String var age: Int = 0 lateinit var sex: String constructor(name: String) : this() { // this 關鍵字指向當前類對象實例 this.name = name } constructor(name: String, age: Int) : this(name) { this.name = name this.age = age } constructor(name: String, age: Int, sex: String) : this(name, age) { this.name = name this.age = age this.sex = sex } override fun toString(): String { return "Person1(name='$name', age=$age, sex='$sex')" }}
上面的寫法,總體來看也有些樣板代碼,其實在idea中,我們寫上面的代碼,只需要寫下面的3行,剩下的就交給IDEA自動生成了
代碼語言:javascript代碼運行次數:0運行復制
class Person2 { lateinit var name: String var age: Int = 0 lateinit var sex: String}
自動生成構造函數的操作示意圖
1.在當前類中“右擊”鼠標操作,選擇Generate (在Mac上的快捷鍵是 Command + N)

右擊鼠標操作
點擊之后,跳出對話框:生成次級構造函數

選擇Generate
選擇構造函數的參數

生成次級構造函數
選中相應的屬性,點擊OK,即可生成。
一個屬性都不選,生成
代碼語言:javascript代碼運行次數:0運行復制
constructor()
選擇一個 name 屬性,生成
代碼語言:javascript代碼運行次數:0運行復制
constructor(name: String) { this.name = name }
選擇name,age屬性生成
代碼語言:javascript代碼運行次數:0運行復制
constructor(name: String, age: Int) : this(name) { this.name = name this.age = age }
3個屬性都選擇,生成
代碼語言:javascript代碼運行次數:0運行復制
constructor(name: String, age: Int, sex: String) : this(name, age) { this.name = name this.age = age this.sex = sex }
最后,我們可以在代碼中這樣創建Person2的實例對象
代碼語言:javascript代碼運行次數:0運行復制
val person21 = Person2() person21.name = "Jack" person21.age = 29 person21.sex = "M" println("person21 = ${person21}") val person22 = Person2("Jack", 29) person22.sex = "M" println("person22 = ${person22}") val person23 = Person2("Jack", 29, "M") println("person23 = ${person23}")
實際上,我們在編程實踐中用到最多的構造函數,還是這個
代碼語言:javascript代碼運行次數:0運行復制
class Person(var name: String, var age: Int, var sex: String)
而當確實需要通過比較復雜的邏輯來構建一個對象的時候,可采用構建者(Builder)模式來實現。
4.3 抽象類與接口
抽象類表示“is-a”的關系,而接口所代表的是“has-a”的關系。
抽象類用來表征問題領域的抽象概念。所有編程語言都提供抽象機制。機器語言是對機器的模仿抽象,匯編語言是對機器語言的高層次抽象,高級語言(Fortran,C,Basic等)是對匯編的高層次抽象。而我們這里所說的面向對象編程語言是對過程函數的高層次封裝。這個過程如下圖所示

編程語言的抽象機制
抽象類和接口是Kotlin語言中兩種不同的抽象概念,他們的存在對多態提供了非常好的支持。這個機制跟Java相同。
4.3.1 抽象類與抽象成員
抽象是相對于具象而言。例如設計一個圖形編輯軟件,問題領域中存在著長方形(Rectangle)、圓形(Circle)、三角形(Triangle)等這樣一些具體概念,它們是具象。但是它們又都屬于形狀(Shape)這樣一個抽象的概念。它們的關系如下圖所示

形狀Shape的抽象繼承關系
對應的Kotlin代碼如下
代碼語言:javascript代碼運行次數:0運行復制
package com.easy.kotlinabstract class Shapeclass Rectangle : Shape() // 繼承類的語法是使用冒號 : , 父類需要在這里使用構造函數初始化class Circle : Shape()class Triangle : Shape()
因為抽象的概念在問題領域中沒有對應的具體概念,所以抽象類是不能夠實例化的。下面的代碼編譯器會報錯
代碼語言:javascript代碼運行次數:0運行復制
val s = Shape() // 編譯不通過!不能實例化抽象類
我們只能實例化它的繼承子類。代碼示例如下
代碼語言:javascript代碼運行次數:0運行復制
val r = Rectangle()println(r is Shape) // true
現在我們有了抽象類,但是沒有成員。通常一個類的成員有屬性和函數。抽象類的成員也必須是抽象的,需要使用abstract 關鍵字修飾。下面我們聲明一個抽象類Shape,并帶有width ,heigth,radius屬性和 area() 函數, 代碼如下
代碼語言:javascript代碼運行次數:0運行復制
abstract class Shape { abstract var width: Double abstract var heigth: Double abstract var radius: Double abstract fun area(): Double}
這個時候,繼承抽象類Shape的方法如下
代碼語言:javascript代碼運行次數:0運行復制
class Rectangle(override var width: Double, override var heigth: Double, override var radius: Double) : Shape() { // 聲明類的同時也聲明了構造函數 override fun area(): Double { return heigth * width }}class Circle(override var width: Double, override var heigth: Double, override var radius: Double) : Shape() { override fun area(): Double { return 3.14 * radius * radius }}
其中,override 是覆蓋寫父類屬性和函數的關鍵字。
在代碼中這樣調用具體實現的類的函數
代碼語言:javascript代碼運行次數:0運行復制
fun main(args: Array<string>) { val r = Rectangle(3.0, 4.0, 0.0) println(r.area()) // 12.0 val c = Circle(0.0, 0.0, 4.0) println(c.area()) // 50.24}</string>
抽象類中可以有帶實現的函數,例如我們在抽象類Shape中添加一個函數onClick()
代碼語言:javascript代碼運行次數:0運行復制
abstract class Shape { ... fun onClick() { // 默認是final的,不可被覆蓋重寫 println("I am Clicked!") }}
那么,我們在所有的子類中都可以直接調用這個onClick()函數
代碼語言:javascript代碼運行次數:0運行復制
val r = Rectangle(3.0, 4.0, 0.0) r.onClick() // I am Clicked! val c = Circle(0.0, 0.0, 4.0) c.onClick() // I am Clicked!
父類Shape中的onClick()函數默認是final的,不可被覆蓋重寫。如果想要開放給子類重新實現這個函數,我們可以在前面加上open 關鍵字
代碼語言:javascript代碼運行次數:0運行復制
abstract class Shape { ... open fun onClick() { println("I am Clicked!") }}
在子類中這樣覆蓋重寫
代碼語言:javascript代碼運行次數:0運行復制
class Rectangle(override var width: Double, override var heigth: Double, override var radius: Double) : Shape() { override fun area(): Double { return heigth * width } override fun onClick(){ println("${this::class.simpleName} is Clicked!") }}fun main(args: Array<string>) { val r = Rectangle(3.0, 4.0, 0.0) println(r.area()) r.onClick()}</string>
其中,this::class.simpleName 是Kotlin中的反射的API,在gradle工程的build.gradle中需要添加依賴 compile “org.jetbrains.kotlin:kotlin-reflect:$kotlin_version” ,我們將在后面的章節中詳細介紹。
上面的代碼運行輸出
代碼語言:javascript代碼運行次數:0運行復制
12.0Rectangle is Clicked!
當子類繼承了某個類之后,便可以使用父類中的成員變量,但是并不是完全繼承父類的所有成員變量。具體的原則如下:
1.能夠繼承父類的public和protected成員變量;不能夠繼承父類的private成員變量;
2.對于父類的包訪問權限成員變量,如果子類和父類在同一個包下,則子類能夠繼承;否則,子類不能夠繼承;
3.對于子類可以繼承的父類成員變量,如果在子類中出現了同名稱的成員變量,則會發生隱藏現象,即子類的成員變量會屏蔽掉父類的同名成員變量。如果要在子類中訪問父類中同名成員變量,需要使用super關鍵字來進行引用。
4.3.2 接口
接口是一種比抽象類更加抽象的“類”。接口本身代表的是一種“類型”的概念。但在語法層面,接口本身不是類,不能實例化接口,我們只能實例化它的實現類。
接口是用來建立類與類之間的協議。實現該接口的實現類必須要實現該接口的所有方法。在Java 8 和Kotlin中,接口可以實現一些通用的方法。
接口是抽象類的延伸,Kotlin跟Java一樣,不支持同時繼承多個父類,也就是說繼承只能存在一個父類(單繼承)。但是接口不同,一個類可以同時實現多個接口(多組合),不管這些接口之間有沒有關系。這樣可以實現多重繼承。
和Java類似,Kotlin使用interface作為接口的關鍵詞:
代碼語言:javascript代碼運行次數:0運行復制
Interface ProjectService
Kotlin 的接口與 Java 8 的接口類似。與抽象類相比,他們都可以包含抽象的方法以及方法的實現:
代碼語言:javascript代碼運行次數:0運行復制
interface ProjectService { val name: String val owner: String fun save(project: Project) fun print() { println("I am project") }}
接口是沒有構造函數的。我們使用冒號: 語法來實現一個接口,如果有多個用,逗號隔開:
代碼語言:javascript代碼運行次數:0運行復制
class ProjectServiceImpl : ProjectService // 跟繼承抽象類語法一樣,使用冒號class ProjectMilestoneServiceImpl : ProjectService, MilestoneService // 實現多個接口使用逗號( ,) 隔開
在重寫print()函數時,因為我們實現的ProjectService、MilestoneService都有一個print()函數,當我們直接使用super.print()時,編譯器是無法知道我們想要調用的是那個里面的print函數的,這個我們叫做覆蓋沖突,如下圖所示

覆蓋沖突
這個時候,我們可以使用下面的語法來調用:
代碼語言:javascript代碼運行次數:0運行復制
super<projectservice>.print()super<milestoneservice>.print()</milestoneservice></projectservice>
4.4 Object對象
單例模式很常用。它是一種常用的軟件設計模式。例如,spring中的Bean默認就是單例。通過單例模式可以保證系統中一個類只有一個實例。即一個類只有一個對象實例。
Kotlin中沒有 靜態屬性和方法,但是可以使用關鍵字 object 聲明一個object 單例對象:
代碼語言:javascript代碼運行次數:0運行復制
package com.easy.kotlinobject User { val username: String = "admin" val password: String = "admin" fun hello() { println("Hello, object !") }}fun main(args: Array<string>) { println(User.username) // 跟Java的靜態類一樣的調用形式 println(User.password) User.hello()}</string>
Kotlin中還提供了 伴生對象 ,用companion object關鍵字聲明:
代碼語言:javascript代碼運行次數:0運行復制
class DataProcessor { companion object DataProcessor { fun process() { println("I am processing data ...") } }}fun main(args: Array<string>) { DataProcessor.process() // I am processing data ...}</string>
一個類只能有1個伴生對象。
4.5 數據類
顧名思義,數據類就是只存儲數據,不包含操作行為的類。Kotlin的數據類可以為我們節省大量樣板代碼(Java 中強制我們要去寫一堆getter、setter,而實際上這些方法都是“不言自明”的),這樣最終代碼更易于理解和便于維護。
使用關鍵字為 data class 創建一個只包含數據的類:
代碼語言:javascript代碼運行次數:0運行復制
data class LoginUser(val username: String, val password: String)
在IDEA中提供了方便的Kotlin工具箱,我們可以把上面的代碼反編譯成等價的Java代碼。步驟如下
1.菜單欄選擇:Tools -> Kotlin -> Show Kotlin Bytecode

菜單欄選擇:Tools -> Kotlin -> Show Kotlin Bytecode
點擊Decompile

點擊Decompile
反編譯之后的Java代碼

反編譯之后的Java代碼
上面這段反編譯之后的完整的Java代碼是
代碼語言:javascript代碼運行次數:0運行復制
public final class LoginUser { @NotNull private final String username; @NotNull private final String password; @NotNull public final String getUsername() { return this.username; } @NotNull public final String getPassword() { return this.password; } public LoginUser(@NotNull String username, @NotNull String password) { Intrinsics.checkParameterIsNotNull(username, "username"); Intrinsics.checkParameterIsNotNull(password, "password"); super(); this.username = username; this.password = password; } @NotNull public final String component1() { return this.username; } @NotNull public final String component2() { return this.password; } @NotNull public final LoginUser copy(@NotNull String username, @NotNull String password) { Intrinsics.checkParameterIsNotNull(username, "username"); Intrinsics.checkParameterIsNotNull(password, "password"); return new LoginUser(username, password); } // $FF: synthetic method // $FF: bridge method @NotNull public static LoginUser copy$default(LoginUser var0, String var1, String var2, int var3, Object var4) { if ((var3 & 1) != 0) { var1 = var0.username; } if ((var3 & 2) != 0) { var2 = var0.password; } return var0.copy(var1, var2); } public String toString() { return "LoginUser(username=" + this.username + ", password=" + this.password + ")"; } public int hashCode() { return (this.username != null ? this.username.hashCode() : 0) * 31 + (this.password != null ? this.password.hashCode() : 0); } public boolean equals(Object var1) { if (this != var1) { if (var1 instanceof LoginUser) { LoginUser var2 = (LoginUser)var1; if (Intrinsics.areEqual(this.username, var2.username) && Intrinsics.areEqual(this.password, var2.password)) { return true; } } return false; } else { return true; } }}
編譯器會從主構造函數中聲明的屬性,自動創建以下函數:
equals() / hashCode() 函數toString() 格式為”LoginUser(username=” + this.username + “, password=” + this.password + “)”component1(),component2() 函數返回對應下標的屬性值,按聲明順序排列copy() 函數: 根據舊對象屬性重新 new LoginUser(username, password) 一個對象出來
如果這些函數在類中已經被明確定義了,或者從超類中繼承而來,編譯器就不再生成。
數據類有如下限制:
主構造函數至少包含一個參數參數必須標識為val 或者 var不能為 abstract, open, sealed 或者 inner不能繼承其它類 (但可以實現接口)
另外,數據類可以在解構聲明中使用:
代碼語言:javascript代碼運行次數:0運行復制
package com.easy.kotlindata class LoginUser(val username: String, val password: String)fun main(args: Array<string>) { val loginUser = LoginUser("admin", "admin") val (username, password) = loginUser println("username = ${username}, password = ${password}") // username = admin, password = admin}</string>
Kotlin 標準庫提供了 Pair 和 Triple數據類 。
4.6 注解
注解是將元數據附加到代碼中。元數據信息由注解 kotlin.Metadata定義。
代碼語言:javascript代碼運行次數:0運行復制
@Retention(AnnotationRetention.RUNTIME)@Target(AnnotationTarget.CLASS)internal annotation class Metadata
這個@Metadata信息存在于由 Kotlin 編譯器生成的所有類文件中, 并由編譯器和反射讀取。例如,我們使用Kotlin聲明一個注解
代碼語言:javascript代碼運行次數:0運行復制
annotation class Suspendable // Java中使用的是@interface Suspendable
那么,編譯器會生成對應的元數據信息
代碼語言:javascript代碼運行次數:0運行復制
@Retention(RetentionPolicy.RUNTIME)@Metadata( mv = {1, 1, 7}, bv = {1, 0, 2}, k = 1, d1 = {"u0000nnu0002u0018u0002nu0002u0010u001bnu0000bu0086u0002u0018u00002u00020u0001Bu0000¨u0006u0002"}, d2 = {"Lcom/easy/kotlin/Suspendable;", "", "production sources for module kotlin_tutorials_main"})public @interface Suspendable {}
Kotlin 的注解完全兼容 Java 的注解。例如,我們在Kotlin中使用Spring Data Jpa
代碼語言:javascript代碼運行次數:0運行復制
interface ImageRepository : PagingAndSortingRepository<image long> { @Query("SELECT a from #{#entityName} a where a.isDeleted=0 and a.isFavorite=1 and a.category like %:searchText% order by a.gmtModified desc") fun searchFavorite(@Param("searchText") searchText: String, pageable: Pageable): Page<image> @Throws(Exception::class) @Modifying @Transactional @Query("update #{#entityName} a set a.isFavorite=1,a.gmtModified=now() where a.id=?1") fun addFavorite(id: Long)}</image></image>
用起來跟Java的注解基本一樣。再舉個Kotlin使用Spring mvc注解的代碼實例
代碼語言:javascript代碼運行次數:0運行復制
@Controllerclass MeituController { @Autowired lateinit var imageRepository: ImageRepository @RequestMapping(value = *arrayOf("/", "meituView"), method = arrayOf(RequestMethod.GET)) fun meituView(model: Model, request: HttpServletRequest): ModelAndView { model.addAttribute("requestURI", request.requestURI) return ModelAndView("meituView") }}
從上面的例子,我們可以看出Kotlin使用java框架非常簡單方便。
4.7 枚舉
Kotlin中使用 enum class 關鍵字來聲明一個枚舉類。例如
代碼語言:javascript代碼運行次數:0運行復制
enum class Direction { NORTH, SOUTH, WEST, EAST // 每個枚舉常量都是一個對象, 用逗號分隔}
相比于字符串常量,使用枚舉能夠實現類型安全。枚舉類有兩個內置的屬性:
代碼語言:javascript代碼運行次數:0運行復制
public final val name: String public final val ordinal: Int
分別表示的是枚舉對象的值跟下標位置。例如上面的Direction枚舉類,它的枚舉對象的信息如下
代碼語言:javascript代碼運行次數:0運行復制
>>> val north = Direction.NORTH>>> north.nameNORTH>>> north.ordinal0>>> north is Directiontrue
每一個枚舉都是枚舉類的實例,它們可以被初始化:
代碼語言:javascript代碼運行次數:0運行復制
enum class Color(val rgb: Int) { RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF)}
枚舉Color的枚舉對象的信息如下
代碼語言:javascript代碼運行次數:0運行復制
>>> val c = Color.GREEN>>> cGREEN>>> c.rgb65280>>> c.ordinal1>>> c.nameGREEN
4.8 內部類4.8.1 普通嵌套類
Kotlin中,類可以嵌套。一個類可以嵌套在其他類中,而且可以嵌套多層。
代碼語言:javascript代碼運行次數:0運行復制
class NestedClassesDemo { class Outer { private val zero: Int = 0 val one: Int = 1 class Nested { fun getTwo() = 2 class Nested1 { val three = 3 fun getFour() = 4 } } }}
測試代碼:
代碼語言:javascript代碼運行次數:0運行復制
val one = NestedClassesDemo.Outer().one val two = NestedClassesDemo.Outer.Nested().getTwo() val three = NestedClassesDemo.Outer.Nested.Nested1().three val four = NestedClassesDemo.Outer.Nested.Nested1().getFour()
我們可以看出,代碼中 NestedClassesDemo.Outer.Nested().getTwo() 訪問嵌套類的方式是直接使用 類名.來訪問, 有多少層嵌套,就用多少層類名來訪問。
普通的嵌套類,沒有持有外部類的引用,所以是無法訪問外部類的變量的:
代碼語言:javascript代碼運行次數:0運行復制
class NestedClassesDemo {class Outer { private val zero: Int = 0 val one: Int = 1 class Nested { fun getTwo() = 2 fun accessOuter() = { println(zero) // error, cannot access outer class println(one) // error, cannot access outer class } }}}
4.8.2 嵌套內部類
如果一個類Inner想要訪問外部類Outer的成員,可以在這個類前面添加修飾符 inner。內部類會帶有一個對外部類的對象的引用:
代碼語言:javascript代碼運行次數:0運行復制
package com.easy.kotlinclass NestedClassesDemo { class Outer { private val zero: Int = 0 val one: Int = 1 inner class Inner { fun accessOuter() = { println(zero) // works println(one) // works } } }}fun main(args: Array<string>) { val innerClass = NestedClassesDemo.Outer().Inner().accessOuter()}</string>
我們可以看到,當訪問inner class Inner的時候,我們使用的是Outer().Inner(), 這是持有了Outer的對象引用。跟普通嵌套類直接使用類名訪問的方式區分。
4.8.3 匿名內部類
匿名內部類,就是沒有名字的內部類。既然是內部類,那么它自然也是可以訪問外部類的變量的。
我們使用對象表達式創建一個匿名內部類實例:
代碼語言:javascript代碼運行次數:0運行復制
class NestedClassesDemo { class AnonymousInnerClassDemo { var isRunning = false fun doRun() { Thread(object : Runnable { // 匿名內部類 override fun run() { isRunning = true println("doRun : i am running, isRunning = $isRunning") } }).start() } }}
如果對象是函數式 Java 接口,即具有單個抽象方法的 Java 接口的實例,例如上面的例子中的Runnable接口:
代碼語言:javascript代碼運行次數:0運行復制
@functionalInterfacepublic interface Runnable { public abstract void run();}
我們可以使用Lambda表達式創建它,下面的幾種寫法都是可以的:
代碼語言:javascript代碼運行次數:0運行復制
fun doStop() { var isRunning = true Thread({ isRunning = false println("doStop: i am not running, isRunning = $isRunning") }).start() } fun doWait() { var isRunning = true val wait = Runnable { isRunning = false println("doWait: i am waiting, isRunning = $isRunning") } Thread(wait).start() } fun doNotify() { var isRunning = true val wait = { isRunning = false println("doNotify: i notify, isRunning = $isRunning") } Thread(wait).start() }
更多關于Lambda表達式以及函數式編程相關內容,我們將在下一章節中介紹。
本章小結
本章我們介紹了Kotlin面向對象編程的特性: 類與構造函數、抽象類與接口、繼承與組合等知識,同時介紹了Kotlin中的注解類、枚舉類、數據類、嵌套類、內部類、匿名內部類、單例object對象等特性類。
總的來說,在面向對象編程范式的支持上,Kotlin相比于Java增加不少有趣的功能與特性支持,這使得我們代碼寫起來更加方便快捷了。
我們知道,在Java 8 中,引進了對函數式編程的支持:Lambda表達式、Function接口、stream API等,而在Kotlin中,對函數式編程的支持更加全面豐富,代碼寫起來也更加簡潔優雅。下一章中,我們來一起學習Kotlin的函數式編程。