--- title: 'Kotlin 函數 & 類' disqus: kyleAlien --- Kotlin 函數 & 類 === ## Overview of Content 2017 年開始,Kotlin 正式成為 Android 開發的第一級語言,與 Java 平起平坐,而且漸漸開始超越,Android 官方網站現在所給予的 Demo 都是先以 Kotlin 為第一優先,所以得硬起來學習... :smile: 以下參考,第一行代碼 (第三版), Kotlin 進階實戰 如有引用參考本文章請詳註出處,感謝 :smile: :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**Kotlin 函數、物件導向 | DataClass、Sealed、Object 關鍵字 | Enum、Companion、NPE**](https://devtechascendancy.com/kotlin-functions_oop_dataclass_sealed_object/) ::: [TOC] ## Kotlin 函數 在 Kotlin 中函數是第一公民 (`First-Class Citizen`),這導致 Kotlin 有以下特色 1. `Function` 可作為變數(可存取) 2. `Function` 可做為參數傳遞 3. `Function` 可以作為回傳(並非像是回傳類,它是回傳一個函數) 4. `Function` 可在 Runtime 時構造 5. `Function` 可表示為匿名字面值 ### 函數 - 參數 * Kotlin 使用關鍵字 `fun`,無論定義啥函數都必須以 `fun` 關鍵字開頭,之後才接上函數名稱、參數,最後使用 `:` 接上返回值 (若沒有返回值則可忽略) :::info 這種參數表達是 **使用了 [`Pascal` 語言的](https://zh.wikipedia.org/wiki/Pascal%E8%AA%9E%E8%A8%80) 表示法** ::: > ![](https://i.imgur.com/Wx0Lo5a.png) 1. **默認參數**:默認參數方便於不用過多的重載函數,也就是你不需重複寫相同函數名,但不同參數的函數(Overload Function) ```kotlin= fun defaultParams(times : Int = 10, info : String = "TEST-Params") { for (i in 0 until times) { println("$info - $i") } } fun main() { // 使用預設參數 defaultParams() } ``` > ![](https://i.imgur.com/kr0v9g3.png) :::warning * Java 無法調用 Koltin 的默認參數怎麼辦? > ![](https://i.imgur.com/LaRX3Te.png) **使用 `@JvmOverloads` 註解**,它會幫我們產生對應的重載函數讓 Java 調用 ```kotlin= @JvmOverloads // 使用註解 fun defaultParams(times : Int = 10, info : String = "TEST-Params") { for (i in 0 until times) { println("$info - $i") } } ``` > ![](https://i.imgur.com/YT0yCc4.png) ::: 2. **命名(指定)參數**:可以直接指定參數做傳入,其中也可以配合預設參數 ```kotlin= fun namedParams(first: Int, second : Int, info: String = "Hello Kotlin") { for(i in 0 until first) { for(j in 0 until second) { println("First: $i, Second: $j, Info: $info") } } } fun main() { // 指定參數 namedParams(second = 3, first = 2) } ``` > ![](https://i.imgur.com/LeBbOeC.png) 3. **可變量參數**:可變參數等同於 Java 中的 `...` ,而在 Kotlin 中要使用 `vararg` 關鍵字 ```kotlin= fun <T> toList(vararg items : T): ArrayList<T> { val res = ArrayList<T>() for(i in items) { res.add(i) } return res } fun main() { println("${toList("Hello", "Kotlin", "123")}") } ``` >![](https://i.imgur.com/DJhwxU0.png) :::warning * Kotlin 不能直接傳遞數組,所以必須使用 `*` 符號來解包 ```kotlin= fun main() { val array : Array<String> = arrayOf("1", "2", "3") val res = toList(*array) println(res) } ``` > ![](https://i.imgur.com/K3xbILq.png) * Java 有規定可變常數必須放置最後一個參數,而 Kotlin 則沒有這個規範,但 **如果放置在非低一參數,則需要使用命令參數指定** ```kotlin= fun <T> toList_2(vararg items : T, other : T): ArrayList<T> { val res = ArrayList<T>() for(i in items) { res.add(i) } res.add(other) return res } fun main() { val list = toList_2("1", "2", "3", other = "HelloWorld") println(list) } ``` > ![](https://i.imgur.com/PB4vlk9.png) ::: :::info * **函數** & **方法** ? 其實 `函數 (function)` & `方法(method)` 是同一個概念,只是在 Java 中較常使用 method,Kotlin 中較常使用 function 來稱呼 > 若再細分可以把方法歸類為,必須寫在類中的 function,而 Kotlin 可以把 funtion 寫在類之外 (Java 不行) ::: ### 函數 - 返回值 1. 返回 Unit:Kotlin 沒有所謂的 void,但 Kotlin 可以返回 `Unit` ```kotlin= fun printlnMyInfo() : Unit { // 可被省略 println("Hello Kotlin") } fun main() { printlnMyInfo() } ``` :::success * 從 Unit 源碼可以看出 Unit 返回的是一個單例對象 ```kotlin= public object Unit { override fun toString() = "kotlin.Unit" } ``` ::: 2. 返回 Nothing:Nothing 與 Unit 很相像,不過 Nothing 則是代表進入該函數後,**絕對不會返回** ```kotlin= fun printlnMyInfoNothing() : Nothing { while (true) { println("Hello Kotlin Nothing") } } ``` :::success * Kotlin 的源碼,可以看出它是一個類 ```kotlin= public class Nothing private constructor() ``` ::: ### 函數表達式 * 以下說明 Kotlin 函數的特殊表達方式 1. **單表達式**:若函數只有一行,則可以使用 `=` 符號,可省略 ^1.^ 大括號、^2.^ return 關鍵字、^3.^ 返回類型 (會自動推倒) ```java= /** 函數 & 方法 都是指相同 * 函數 -> function (Kotlin 常用 * 方法 -> method (Java 常用 */ fun main() { printA() printB() println("compare result : ${compareMax(10, 20)}") println("compare result : ${compareMin(10, 20)}") } // 關鍵字 fun,使用 ':' 定義返回類型 fun printA() { // 返回 void,可以不用特別寫返回 println("Hello Kotlin!") } fun printB() : Unit { // 由於 Kotlin Function 返回不可使用 void,所以可以使用 Unit 替代 println("Hello Kotlin Function!") } fun compareMax(a: Int, b: Int) : Int { return max(a, b) } /** * fun 語法糖,當只有一行內容就可以省略以下敘述 * 1. 大括號 * 2. 若需要 return 則可直接省略 * 3. 不用顯式的聲明返回類型,返回類型可以推導 */ fun compareMin(a: Int, b: Int) = min(a, b) // fun compareMin(a: Int, b: Int) : Int = min(a, b) // 同上,只是多了返回類型 ``` > ![](https://i.imgur.com/F31F6jK.png) 2. **局部函數**(Local Function):在函數內再定義函數,如同 Python 一樣 ```kotlin= fun localFunction_print(string: String) { fun checkBlank() { if (string.isBlank()) throw IllegalArgumentException("Don't input blank.") } checkBlank() println("$string be print.") } fun main() { localFunction_print("LocalFunction") localFunction_print("") } ``` > ![](https://i.imgur.com/0lbspoT.png) 3. **尾遞歸函數**:其實這句話由兩個動作組成,^1.^ 尾函數(在 Function 中最後呼叫的函數), ^2.^ 遞歸 * 以往我們在使用遞歸時總是會要注意遞歸深度問題,如果遞歸深度過身則會導致 StackOverflowError (如下) ```kotlin= fun sumTimesNoTailrec(n: Int, result : Int) : Int = if(n < 0) result else sumTimes(n - 1, result + n) fun main() { println("NoTailrec Result: ${sumTimesNoTailrec(100000, 0)}") } ``` > * **使用 `tailrec` 關鍵字**:該關鍵字的主要目標是優化遞迴的程式,使其不會 StackOverflowError ```kotlin= tailrec fun sumTimes(n: Int, result : Int) : Int = if(n < 0) result else sumTimes(n - 1, result + n) fun main() { println("Result: ${sumTimes(100000, 0)}") } ``` > ![](https://i.imgur.com/LdtefWx.png) 4. **Top-level 函數**:Java 中我們必須將函數定義在某個類中,不能寫在檔案頂層,而 Kotlin 可以 頂層函數默認是 `public` 可以被任意訪問 (當然也可以修改它的訪問權限) ```kotlin= private fun printlnHelloWorld() = println("Hello World") ``` :::success * Java 可以訪頂層函數? 可以 1. 透過 `<檔案名>Kt` 訪問 ```java= // Kt File package base.function fun printlnHelloWorld() = println("Hello World") // Java File public static void main(String[] args) { Function_SpecialKt.printlnHelloWorld(); } ``` 2. 透過 `@file:JvmName("自訂名稱")` 註解,**==該註解一定要放在文件頂端==! 並且不可與第一個方法混用** ```java= // Kt File @file:JvmName("HelloUtils") package base.function fun printlnHelloWorld2() = println("Hello World 2") // Java File public static void main(String[] args) { Function_SpecialKt.printlnHelloWorld(); } ``` ::: ## 物件導向 其與 C 有最大的不同在於,物件導向是可以依照一個模板來創建類的 (可創建多個),我們可以把很多事物一起做封裝,用來描述事物(冰箱、電視、調查表...) | 程式表達 | 描述 | | -------- | -------- | | 字段 Field | 資料屬性 | | 函數 Method | 行為動作 | :::info * Kotlin 的一般類都是 `final` 類 (修正 Java 的不安全、不強制,這有助於設計的完整性) ::: ### 類 - 創建方式 * Kotlin 較特別的特點如下 1. **可以單獨創造類 (Class) 而不需實做** ```kotlin= // 可以單獨創建該類,而不需要大括號 class MyClass ``` 2. **省去 `new` 關鍵字** ```kotlin= class PC { var CPU = "Intel-core-i5"; var ram = 16; fun info() { println("Cpu: $CPU, ram: $ram") } } /** * 實例化一個類,不需要 new 關鍵字 */ fun main() { val p = PC() // 省略 new p.info(); } ``` > ![](https://i.imgur.com/Otniakb.png) ### 類 - 建構函數 * 構造函數與 Java 較不同的在於,**Kotlin 構造函數有分為 ++主構造函數++、++次構造函數++**;主構造函數沒有函數體,必須透過 `init 結構` 來完成 > 類似於 Java 的 static 區塊,但是 **init 區塊是每次建構對象都會呼叫** 1. **主構造函數**:使用 `init { }`,主構造函數可以省略 constructor 關鍵字 ```kotlin= class MyClz_1 { init { // 主構造函數 println("init - primary construct") } } fun main() { MyClz_1() } // ---------------------------------------------------------- // 同上 class MyClz_2 constructor() { // 主構造函數 (沒有參數時可以省略) init { println("init - primary construct") } } ``` > ![](https://i.imgur.com/lcM0MG6.png) 2. **次構造函數**:次構造函數必須呼叫主構造函數,使用 `:this(...)` 主構造函數可以使用 `val`, `var` 修飾屬性,修飾屬性後就可以轉為該類的自身屬性 ```kotlin= class MyClz_3 constructor(val str1: String){ // 主建構函數 init { println("init - primary construct, $str1") } var str2 : String? = null // 次構造函數 (必須呼叫主構造函數) constructor(str1 : String, str2 : String) : this(str1) { this.str2 = str2 } fun printInfo() = println("Str1: $str1, Str2: $str2") } fun main() { MyClz_3("Hello").printInfo() MyClz_3("Hello", "World").printInfo() } ``` > ![](https://i.imgur.com/xDCdjOZ.png) :::success * `init { }` 區塊可以有多個區塊,該區塊會按照上到下的順序被調用,接著才會輪到次構造函數 ```kotlin= class MyClz_4 constructor(val str1: String){ init { // 主構造函數 println("init - primary construct, $str1 + _1") } init { // 主構造函數 println("init - primary construct, $str1 + _2") } var str2 : String? = null constructor(str1 : String, str2 : String) : this(str1) { this.str2 = str2 println("Second construct") } init { // 主構造函數 println("init - primary construct, $str1 + _3") } } fun main() { MyClz_4("Hello", "Kotlin") } ``` > ![](https://i.imgur.com/FoxnCxH.png) ::: ### 類 - 屬性 * 類的「屬性」可以理解為類的成員,但是比起成員,我們可以對屬性添加而外的 setter/getter操作;在 Kotlin 中可以透過對於屬性的設定限制屬性的存取,其格式如下 ```shell= ## var 屬性格式 var <propertyName> [: <PropertyType>] [= <Initializer>] [<getter>] [<setter>] ##-----------------------------------------------------## ## val 屬性格式 val <propertyName> [: <PropertyType>] [= <Initializer>] [<getter>] ``` * Kotlin Property 範例 ```kotlin= class HttpResponse { var resCode = -1 val isPass : Boolean get() = resCode == 200 } fun main() { val res = HttpResponse() res.resCode = 200 println("res: ${res.isPass}") } ``` > ![](https://i.imgur.com/s1H8gY6.png) * **Backing field** 幕後字段:它是 Kotlin 屬性自動生成的字段,它只能在當前屬性的訪問器內使用 (拓展類不可使用) :::danger * 無法自己調用自己,會導致遞迴 ```kotlin= // Error var paramValue : Int = 0 get() = paramValue set(value) = this.paramValue = value ``` > ![](https://i.imgur.com/mkFCeGg.png) ::: ```kotlin= class BackingField { var paramValue : Int = 0 get() { // 自動產生 field 字段 println("Get: $field") return field + 1 } set(value) { // 自動產生 field 字段 println("Set: $value") field = value - 3 } } fun main() { val bf = BackingField() bf.paramValue = 10 println(bf.paramValue) } ``` > ![](https://i.imgur.com/XozWtv7.png) ### 抽象 - abstract * 抽象類跟 Java 很像,同樣使用 `abstract` 關鍵字 :::success 使用 `abstract` 關鍵字 就自動轉為 `open` 型態的類 ::: ```kotlin= package class_2 fun main() { val p1 = Man() p1.say() } interface IHello { fun say() } abstract class Person : IHello { } /** * 抽象則 "必須" 使用 () */ class Man : Person() { override fun say() { println("Hello World") } } ``` **--實做結果--** > ![](https://i.imgur.com/MZK1NxE.png) ### 內部類 - class / inner class 差別 * 內部 class 又分為 ^1.^ 靜態 class(與外部類較無關係)、^2.^ 一般 class (必須要使用實例化的外部類才能創建,與外部類關係較大,但是可以直接使用外部元素) ```kotlin= fun main() { val t = Test() val t1 = Test.TestInner() val t2 = t.TestInner2() t1.show() t2.show() } class Test { val A = 1234 companion object { // 相當於靜態區塊 static{ } val B = 5678 } /** * 相當於靜態內部類 static class */ class TestInner { fun show() { // println("A is $A") // Error println("static class, B is $B") // 可以獲取靜態外部元素 } } /** * 必須要使用 inner 關鍵字才能讓內部類 & 外部類產生關係 */ inner class TestInner2 { fun show() { println("inner class, A is $A") // } } } ``` :::info Kotlin class 內部的 class 預設為 `static final class`,它不能訪問外部引用 ::: * 我們將上面的內部類轉為 Java 看看 1. class 靜態內部類,靜態內部類 `static final class` 類 ```java= // Java 的靜態內部類 public static final class TestInner public final void show() { // 無法直接取得外部引用 String var1 = "static class, B is " + Test.Companion.getB(); boolean var2 = false; System.out.println(var1); } } ``` 2. **inner class 是一般內部類**,而一般內部類為靜態 `final class` 類 ```java= // Java 的一般內部類 public final class TestInner2 { public final void show() { // 可以使用 this 取得外部引用 String var1 = "inner class, A is " + Test.this.getA(); boolean var2 = false; System.out.println(var1); } } ``` **--實作結果--** > ![](https://i.imgur.com/ZfOArkE.png) ### 類的可見性 * **Kotlin 默認所有的參數皆為 public** | 修飾符 | Java | Kotlin | | -------- | -------- | -------- | | 無 | **預設**,同路靜下的類可見 | | | private | 當前類可見 | 當前類可見 | | protected | 同一個路徑包可見 | 同一個路徑包可見 | | public | 全部域可見 | **預設**,全部域可見 | :::success * Kotlin 還有另外一些修飾符 **internal、inner** 1. **internal**:若是希望該類不會被外部調用則可以使用,就像是 Java 的內部類 > `internal` 如果使用在外部類,其特別之處在於僅限「同模組內可調用」 ```java= // java 版本 public class A { // internal 就像是私有內部類 private static class C { } } // kt 版本 class A { internal class C { // 使用 internal 關鍵字,內部不公開 } } ``` 2. **inner**:一般非靜態內部類 (另個小節回說到),外部函數可使用 ```java= // java 版本 public class A { // inner 就像是內部類 public class B { } } // kt 版本 class A { inner class B { // 使用 inner 關鍵字,內部公開 } } ``` ::: ### Enum 類 * Kotlin 的 Enum 類與 Java 類似,這裡我們可以配合上面所學的主建構函數來為 Enum 類添加屬性 ```kotlin= enum class EnumClz(val describe: String, val number: Int) { PAN("Pan", 1), KYLE("Kyle", 2), ALIEN("Alien", 3); fun printInfo() = println("describe: $describe, number: $number") } fun main() { for (i in EnumClz.values()) { i.printInfo() } } ``` > ![](https://i.imgur.com/HVshr7F.png) ## 類的應用 ### 繼承 - Class * **==Kotlin 默認類是不可以繼承的,也就用是 final 描述類==,若要繼承需要使用關鍵字 ==`open`== 打開這個類**;但若 Class 本來就是 `abstruct` 的類那就原本就是 open 的 (這也滿符合語意的) * 在繼承中,若是沒有主構造函數,**只有次構造函數**,則必須使用 **`super`** 呼叫父類的建構函數 ```kotlin= package class_2 /** * open 關鍵字 * 是由於 class 預設是 final class,也就是不可繼承 * 使用 open 關鍵字就可以解開 final class 的預設 (抽象 class 本身就沒有 final 關鍵字) */ open class Info // 屬性跟類都可以使用 private 修飾 (var id: Long, private var name: String) { fun showInfo() { println("id: $id, name: $name") } } /** * 繼承使用 `:` 符號,並且父類必須加 `()`,這有關係到主、次 construct * 主建構函數,要呼叫必須使用 init {} 結構 * 次建構函數,使用 constructor(),並且可以函數重載 (Dart 就不行) */ class Boy(name: String) : Info(123, name) { // 可以選擇繼承的構造函數,這裡選次建構函數 constructor() : this("Boy") // this 呼叫自身 constructor(id: Long) : this("Boy") { // this 呼叫自身 this.id = id } constructor(id: Long, name: String) : this(name) { // this 呼叫自身 this.id = id } } class Girl : Info { // 若是沒有主構造函數,只有次構造函數,則必須使用 super 呼叫父類的建構函數 // super 呼叫 Parent constructor constructor(id: Long) : super(id, "Girl") // 可以呼叫次 or 主 construct } fun main() { // 實例化不需要 new 關鍵字 val m : Info = Boy() m.showInfo(); Boy().showInfo() Boy(11).showInfo() Boy(22, "Pan").showInfo() Girl(33).showInfo() } ``` :::info * **在主構造函數裡面宣告的 val、var 字段,會自動轉為該類別的字段**,所以繼承者不可再用 val、var 在主構造函數中宣告相同的屬性 (因為名稱重複會衝突,但是一般屬性可以) ```kotlin= package class_2 /** * 建構函數參數,必須要定義型態 */ open class Shape(val width: Int, val height: Int) { // width、height 是成員元素 fun printSize() { println("width: $width, height: $height") } } class Circle(radius: Int, width: Int) : Shape(radius, radius) { // 上面 okay 的原因在於 width 還沒有成為該類屬性 // width 變數重複 // class Circle(radius: Int, var width: Int) : Shape(radius, radius) fun CircleSize() { // 這邊所指的是父類的 (Shape 類的 width) println("width: $width, height: $height") } } fun main() { val c = Circle(10, 1) c.printSize() } ``` 以下演示錯誤,width 成員重複 > ![](https://i.imgur.com/iIULYJr.png) ::: **--實作結果--** > ![](https://i.imgur.com/cGWc2c2.png) ### 介面實做 - interface * Kotlin 也有 `interface` (Dart 語言就沒有),使用方法如同 Java,內部就可以宣告需要實做的 function > 介面與繼承的順序可以隨意顛倒 (Java 則是有規定要先有繼承,才能有 `interface`) ```kotlin= interface IInfo { fun printInfo() } open class Person(var name: String, var age: Int) { } // 接口與繼承的順序可以顛倒 class Student(name: String, age: Int) : IInfo, Person(name, age) { // Kotlin override 是放置在函數前 override fun printInfo() { println("name: $name, age: $age") } } fun main() { Student("Alien", 18).printInfo() } ``` **--實做結果--** > ![](https://i.imgur.com/qTOfbdB.png) ## 類 - object 關鍵字 object 關鍵字使用在物件聲明,物件表達,伴生類 ### 靜態區塊(伴生類) - companion object :::warning Kotlin 中沒有 static 關鍵字,在其中也沒有靜態函數 & 屬性 ::: * **`companion object {}` 區塊相當於 Java 中的 `static {}` 靜態區塊,會在類加載成功後就存在**,並且只會加載一次 ```cpp= class Person(var name: String, var age: Int) { companion object { var a : Int = 10 fun hello() : Unit { println("Person: $a") } } } fun main() { Person.hello() // 靜態函數 Person.a = 100; Person.hello() // 靜態函數 } ``` > ![](https://i.imgur.com/MUtNHCg.png) :::success * **Java 類如何調用 Kotlin 中的 Companion 內容?** 使用 `@JvmField`(屬性), `@JvmStatic`(方法) ```kotlin= class Companion { companion object { @JvmField var info : String? = null @JvmStatic fun printInformation() { println(info) } } } ``` Java 類調用如下 (有註解才可以調用) ```java= public class Java_Main { public static void main(String[] args) { Companion.info = "123"; Companion.printInformation(); } } ``` > ![](https://i.imgur.com/MxYixTf.png) ::: ### 物件聲明 - 單例 object * 先來看看 Java 的懶加載實現方式,並考慮到多線程問題 ```java= // Java 的 DCL 單例模式 public class Singleton { private volatile static Singleton instance; // 加載覽 private Singleton() {} public synchronized static Singleton getInstance() { if(instance == null) { instance = new Singleton(); } return instance; } } ``` * 在 Kotlin 中加更簡單,只需要**將 class 改為 object** 即可,不須私有化建構函數、也不需要靜態方法、同步;**呼叫單例內的方法是不需使用 `()`** > **若是 Singleton 要實例化則會錯誤** ```kotlin= object Singleton { fun print() { println("I am Kotlin Singleton"); } } fun main() { // Singleton() // Error Singleton.print() // 呼叫不使用 () } ``` > ![](https://i.imgur.com/csKe2WG.png) :::success * 以下不使用 Kotlin#`object` 關鍵字(Object 類是使用了 static 加載對象),使用 Kotlin 模仿 Java 的懶加載方式 ```kotlin= class MySingleton { companion object { // 必須使用 private 修飾 private var instance: MySingleton ?= null fun getInstance() : MySingleton { if(instance == null) { instance = MySingleton() } return instance!! // !! 代表自身負責 } fun printInfo() { println("Hello Singleton") } } fun printInfo() { println("Hello Singleton Working") } } fun main() { MySingleton.printInfo() MySingleton.getInstance().printInfo() } ``` **--實作結果--** > ![](https://i.imgur.com/GCBpJHP.png) ::: ### 物件表達 - 匿名類 * 這種表達式類似於 Java 中的匿名類,並且可以支持實現多個接口 :::info object 它可實現多個方法 ::: ```kotlin= interface OnClick { fun onClick() fun onCancel() } class View constructor(val onClick: OnClick){ fun touch() = onClick.onClick() fun cancel() = onClick.onCancel() } fun main() { val view = View(object : OnClick { override fun onClick() { println("View be click.") } override fun onCancel() { println("View ve cancel") } }) view.touch() Thread.sleep(1000) view.cancel() } ``` > ![](https://i.imgur.com/Xu4sqVY.png) ## 數據類 Koltin 有簡化數據類的使用,讓我們不用寫太多程式 ### Bean 類 - data * 先來複習一下 Java 的 Bean 類寫法,必須覆寫二個方法(通常會寫三個),`equals`、`hashcode`、`toString` (下面註釋會解釋) ```java= public class DataBean { private String name; private long id; public String getName() { return name; } public void setName(String name) { this.name = name; } public long getId() { return id; } public void setId(long id) { this.id = id; } /** * 必須重寫否則無法正常使用 hashMap、hashSet */ @Override public int hashCode() { return name.hashCode() + (int)id; } /** * hashCode & equals 是配套 * 1. 比較類型 * 2. 數據內容 */ @Override public boolean equals(@Nullable Object obj) { if(obj instanceof DataBean) { DataBean bean = (DataBean) obj; return bean.name.equals(this.name) && bean.id == id; } return false; } @NonNull @Override public String toString() { return "DataBean= name: " + name + ", id: " + id; } } ``` * Kotlin 更加的簡單,只需要在類前加上 `data` 關鍵字即可,並在主建構函數建立必須的參數(並使用 `val` 字段描述),**它會幫我們把以上三個函數做完**,不需要手動覆寫 (也包括 clone 函數) 1. equals / hashCode 2. toString 3. componentN:有多少個屬性 N 就會是多少 4. copy:可以複製全部或是部分屬性 ```kotlin= // Params 必須使用 val 描述 data class DataBean(val name: String, val id: Long) // 不可使用大括號 {} fun main() { val dataBean1 = DataBean("Alien", 9527); val dataBean2 = DataBean("Alien", 9527); println("dataBean1: $dataBean1") println("dataBean1 == dataBea: ${dataBean1 == dataBean2}") // println("dataBean1 == dataBea: ${dataBean1.equals(dataBean2)}") // 同上 } ``` **--實做結果--** > ![](https://i.imgur.com/DmtYb8Q.png) :::warning * data class 的拷貝函數是 深拷貝 還是 淺拷貝? **淺拷貝** ```kotlin= data class PersonInfo(val name: String, val id: Long) fun main() { val p1 = PersonInfo("Alien", 123) println("origin: $p1") // 全部複製 val p1_copy = p1.copy() println("copy: $p1_copy") // 淺拷貝判斷 println("origin name === copy name? ${p1.name === p1_copy.name}") // 指定複製 val p2_copy = p1.copy(name = "Kyle") println("copy 2: $p2_copy") } ``` > ![](https://i.imgur.com/7nuHWhN.png) ::: ### 密封類 - Sealed * **密封類**:一般來說 Kotlin 並不會判斷 (使用 when 判斷) 是否是完全符合使用者的設定,若是不符合也不會警告,只會拋向 else,這會導致設計出的程式不構安全,Kotlin 也有解決的辦法 * Sealed 可以讓你寫出更健全的程式,它的功能類似於 Android 的 `@IntDef` 註解,並且功能更強大,**若是 ++判斷沒有實作 Sealed 密封類的判斷編譯器會直接報錯++** :::warning **Sealed 類是一個抽象類**,子類可以在任意位置 ::: 先來做一個沒有密封的類,並對其做判斷,會發現 when 必須要使用 else 否則會錯誤 (編譯根本過不了),儘管不需要也要增加 ```kotlin= interface Result // 接口 // 實作 Result 接口 (不須 {}) class Success(val msg: String) : Result class Failure(val err: Exception) : Result fun getResultMeg(result: Result) : String? = when(result) { is Success -> result.msg is Failure -> result.err.message // 必須添加 else,盡管不需要 else -> throw IllegalArgumentException() // 不夠密封,會有一些危險拋出 } ``` 再來做一個密封類,**使用 ^1.^ 密封類必須要使用在 class,^2.^ sealed 描述的類不需要使用 open 才能繼承,^3^ 不需使用 abstruct 加以描述** ```kotlin= sealed class Result2 // 抽象繼承 class Success2(val msg: String) : Result2() // 繼承必須要有括號 (預設建構函數) class Failure2(val err: Exception) : Result2() fun getResultMeg2(result2: Result2) : String? = when(result2) { is Success2 -> result2.msg is Failure2 -> result2.err.message // 不須使用 else // else -> throw IllegalArgumentException() // 多餘 throw IllegalArgumentException() // 同上 } ``` **--觀察結果 1--** > when is exhaustive(詳盡的) so 'else' is redundant(多餘) here > > ![](https://i.imgur.com/LcI7v2c.png) **--觀察結果 2--** > 若是新增了一個 Unknow 就必須在 when 中賦予值 or 使用 else,否則會報錯 > > ![](https://i.imgur.com/kJvYlUU.png) ## 再提及靜態 在 Kotlin 中要定義靜態變數、方法都比 Java 較麻煩一些(Java 只需要加 static 關鍵字即可) > ![](https://i.imgur.com/5ePzR1P.png) ### 單例 - object * 透過 object 來描述可以讓該類變成單例類(不須 class 關鍵字),事實上 object 內部它並不是靜態方法,而是透過 ```kotlin= object TestSingle { fun say() { println("Hello object") } } fun main() { // 實際上是調用了 TestSingle.INSTANCE.say() TestSingle.say() } ``` * 透過工具 `Tools` -> `Kotlin` -> `Show Kotlin ByteCode` -> `Decompile` 可以看到反編譯的程式,內部就是使用餓漢加載的方式 ```java= public final class TestSingle { public static final TestSingle INSTANCE; public final void say() { String var1 = "Hello object"; boolean var2 = false; System.out.println(var1); } private TestSingle() { } static { TestSingle var0 = new TestSingle(); INSTANCE = var0; } } ``` **--實做結果--** > **會透過該檔案名稱 +Kt 創建一個新的類**,之後的呼叫方法如同我們預估 (請忽略報錯) > > ![](https://i.imgur.com/1Rj4JUL.png) ### 偽靜態 - Companion object * 全名是 companion object,**必須使用在類內,類之外不可以使用**,並且**實際上它並不是靜態類,而是該類的 ==伴生類==** ```kotlin= class MyCompanion { fun sayHello() { println("Hello~ outside class") } companion object { fun sayHello() { println("Hello~ companion object") } } } fun main() { MyCompanion().sayHello() // normal MyCompanion.sayHello() // companion } ``` **--實做結果--** > ![](https://i.imgur.com/ZpQm8qS.png) :::info * **反編譯 Kotlin 的 Companion 來觀察** 以下是 byteCode 反編譯過後的程式,可以看到 **companion object 是一個靜態內部類,並且在外部使用 static 直接加載,所以可以調用**,也證明 companion 並不是真正的靜態,它是伴生類 ```kotlin= @Metadata( mv = {1, 1, 16}, bv = {1, 0, 3}, k = 1, d1 = {"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0002\b\u0002\u0018\u0000 \u00052\u00020\u0001:\u0001\u0005B\u0005¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004¨\u0006\u0006"}, d2 = {"Lclass_05/MyCompanion;", "", "()V", "sayHello", "", "Companion", "LearnClass1"} ) public final class MyCompanion { // 使用 static 直接加載 public static final MyCompanion.Companion Companion = new MyCompanion.Companion((DefaultConstructorMarker)null); public final void sayHello() { String var1 = "Hello~ outside class"; boolean var2 = false; System.out.println(var1); } @Metadata( mv = {1, 1, 16}, bv = {1, 0, 3}, k = 1, d1 = {"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\b\u0086\u0003\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004¨\u0006\u0005"}, d2 = {"Lclass_05/MyCompanion$Companion;", "", "()V", "sayHello", "", "LearnClass1"} ) // 靜態內部類 public static final class Companion { public final void sayHello() { String var1 = "Hello~ companion object"; boolean var2 = false; System.out.println(var1); } private Companion() { } // $FF: synthetic method public Companion(DefaultConstructorMarker $constructor_marker) { this(); } } } ``` ::: ### Kotlin 的真靜態 * Kotline 有兩種實現真正靜態的方式,^1.^ `@JvmStatic` 註解、^2.^ 頂層方法 1. **註解使用 ==`@JvmStatic` 註解==:經過編譯過後就會讓這些方法成為真正的靜態方法,並且 ++該註解只能使用在 companion object 內++** ```kotlin= class TestStatic { // fun sayHello() { // Err. 會引發衝突 (原因在下方說明) // println("Hello~ outside class") // } companion object { @JvmStatic // 只能使用在 companion object 內 fun sayHello() { println("Hello~ companion object") } } } fun main() { TestStatic.sayHello() // companion } ``` **--實做結果--** > ![](https://i.imgur.com/KTUmHeX.png) :::danger * 為何與外部同名方法衝突 反編譯代碼,**可以發現 compation object 仍然在,但是外部產了了相同的函數**,這就是衝突的原因 ```kotlin= public final class TestStatic { // 還是使用靜態類 public static final TestStatic.Companion Companion = new TestStatic.Companion((DefaultConstructorMarker)null); // 外部產生相同的名稱的函數 @JvmStatic public static final void sayHello() { Companion.sayHello(); } @Metadata( mv = {1, 1, 16}, bv = {1, 0, 3}, k = 1, d1 = {"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\b\u0086\u0003\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002J\b\u0010\u0003\u001a\u00020\u0004H\u0007¨\u0006\u0005"}, d2 = {"Lclass_05/TestStatic$Companion;", "", "()V", "sayHello", "", "LearnClass1"} ) public static final class Companion { @JvmStatic public final void sayHello() { String var1 = "Hello~ companion object & JvmStatic"; boolean var2 = false; System.out.println(var1); } private Companion() { } // $FF: synthetic method public Companion(DefaultConstructorMarker $constructor_marker) { this(); } } } ``` ::: 2. **頂層方法**:會發現 Kotlin 的方法並不需要定義在類 (Class) 內,可以定義在外部,在 **外部 (不在類內) 的方法就是頂層方法** ```kotlin= /** * External.kt 檔案 */ // 不存在類內's 方法 fun externalSayHello() { println("Hello~ External top function") } ``` Kt 會自動依照檔案名稱生成一個,`檔名 + Kt.java` 的 Java 檔案,以下為反編譯結果 ```java= @Metadata( mv = {1, 1, 16}, bv = {1, 0, 3}, k = 2, d1 = {"\u0000\b\n\u0000\n\u0002\u0010\u0002\n\u0000\u001a\u0006\u0010\u0000\u001a\u00020\u0001¨\u0006\u0002"}, d2 = {"externalSayHello", "", "LearnClass1"} ) public final class ExternalKt { public static final void externalSayHello() { String var0 = "Hello~ External top function"; boolean var1 = false; System.out.println(var0); } } ``` 使用 java 呼叫 ```java= class CallExternal { public static void main(String...s) { ExternalKt.externalSayHello(); } } ``` **--實做結果--** > ![](https://i.imgur.com/LCxsmAx.png) ## 空指針檢查 在 ++Source 時期++ 的 **空指針檢查是 Kotlin 的一大特色**,排除了大部分的空指針操作,因為以前空指針只能靠程式設計師自己察覺 (狀態不好就會一堆空指針) ```kotlin= fun main() { customShow(null) // 其實 Kotlin 不允許空傳空,下會會說到 } class BookShop { fun getDescribe() : String { return "This is Book Shop" } fun workTime() { println("Open At 9:00 till 21:00") } } /** * 以往都需要在執行前判斷,但這會導致過多不需要的程式 * ( 判空看起來不爽,但是出 Null Pointer Exception 會更不爽 */ fun customShow(s: BookShop) { if(s != null) { s.getDescribe() s.workTime() } } ``` ### Kotlin 空類型限制 * **Kotlin 預設是不允許空類型的 (所有的參數、變量)**,若是傳入空類型 (null) 則會提示錯誤,若是不處理則會編譯無法通過 > ![](https://i.imgur.com/haqVfor.png) * 若是需要空指針的操作,就必須 **在類名後方加入 ==?==,代表了該變數 ++可為空++** > Ex: PrintWord (hello : String ?, time : int ?),表示這兩個變數可以為空指針,也就是呼叫時可以傳入 null ```java= fun main() { customShow(null) // 其實 Kotlin 不允許空傳空,下會會說到 } class BookShop { fun getDescribe() : String { return "This is Book Shop" } fun workTime() { println("Open At 9:00 till 21:00") } } /** * 以往都需要在執行前判斷,但這會導致過多不需要的程式 * ( 判空看起來不爽,但是出 Null Point 會更不爽 */ fun customShow(s: BookShop ?) { // 引數加上 ? 代表了同意為空操作 s.getDescribe() // 會警告可能會空指針,也就編譯不過 s.workTime() } ``` **--實作結果--** > ![](https://i.imgur.com/3AlKDqo.png) ### 判空輔助符號 | 符號 | 說明 | | -------- | -------- | | <對象>==?.==<方法> | 判斷呼叫的對象是否為空,不為空才往下執行呼叫 | | <表達式>==?:==<表達式> | 左邊表達式判斷若為空,就執行右邊表達式 | | <對象>==!!== | 該對象是否為空由程序設計者自己判斷,也就是讓盼空系統失效 | 1. **`?.` 判空符號** ```java= fun isEmpty(s: BookShop?) { s?.getDescribe() s?.workTime() // 相當於下方程式 if(s != null) { s.getDescribe() } if(s != null) { s.workTime() } } ``` 2. **`?:` 賦值符號** ```kotlin= fun decide() : Int { val a : Int? = null val b : Int = 123 // return if(a != null) a else b return a ?: b // 功能同上 } ``` 3. **`!!`,非空斷言工具** ```kotlin= fun notEmpty(s: BookShop?) { s!!.getDescribe() s!!.workTime() } /** * 返回值 + ? 代表可能返回空 */ fun returnNotEmpty() : BookShop? { val s : BookShop? = null return s!! // 也就是程式設計師自己負責空指針 } ``` > ![](https://i.imgur.com/MGLiVDn.png) ## 更多的 Kotlin 語言相關文章 在這裡,我們提供了一系列豐富且深入的 Kotlin 語言相關文章,涵蓋了從基礎到進階的各個方面。讓我們一起來探索這些精彩內容! ### Kotlin 語言基礎 * **Kotlin 語言基礎**:想要建立堅實的 Kotlin 基礎?以下這些文章將帶你深入探索 Kotlin 的關鍵基礎和概念,幫你打造更堅固的 Kotlin 語言基礎 :::info * [**Kotlin 函數、類、屬性 | DataClass、Sealed、Object 關鍵字 | Enum、Companion、NPE**](https://devtechascendancy.com/kotlin-functions_oop_dataclass_sealed_object/) * [**深入探究 Kotlin 與 Java 泛型:擦除、取得泛型類型、型變、投影 | 協變、逆變**](https://devtechascendancy.com/explore-kotlin-java-generics_type_erasure/) * [**深入 Kotlin 函數特性:Inline、擴展、標準函數全解析 | 提升程式碼效能與可讀性**](https://devtechascendancy.com/kotlin_inline_extensions_standards-func/) * [**Kotlin DSL、操作符、中綴表達式 Infix | DSL 詳解 | DSL 設計與應用**](https://devtechascendancy.com/kotlin-dsl-operators-infix-explained/) ::: ### Kotlin 特性、特點 * **Kotlin 特性、特點**:探索 Kotlin 的獨特特性和功能,加深對 Kotlin 語言的理解,並增強對於語言特性的應用 :::warning * [**Kotlin 代理與懶加載機制:使用、lazy 深度解析**](https://devtechascendancy.com/kotlin-delegate_java-proxy_lateinit_lazy/) * [**Kotlin Lambda 編程 & Bytecode | Array & Collections 集合 | 集合函數式 API**](https://devtechascendancy.com/kotlin-lambda-bytecode-array-collections-functional/) * [**深入理解 Kotlin:智能推斷與 Contact 規則**](https://devtechascendancy.com/kotlin-smart-inference-contract-rules-guide/) ::: ### Kotlin 進階:協程、響應式、異步 * **Kotlin 進階:協程、響應式、異步**:若想深入學習 Kotlin 的進階主題,包括協程應用、Channel 使用、以及 Flow 的探索,請查看以下文章 :::danger * [**應用 Kotlin 協程:對比 Thread、創建協程、任務掛起 | Dispatcher、CoroutineContext、CoroutineScope**](https://devtechascendancy.com/applied-kotlin-coroutines-in-depth-guide/) * [**Kotlin Channel 使用介紹 | Select、Actor | 生產者消費者**](https://devtechascendancy.com/kotlin-channel_select_actor_cs/) * [**探索 Kotlin Flow:基本使用、RxJava 對比、背壓機制 | Flow 細節**](https://devtechascendancy.com/kotlin-flow-usage_compare-rx_backpressure/) ::: ## Appendix & FAQ :::info ::: ###### tags: `Kotlin`