---
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) 表示法**
:::
> 
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()
}
```
> 
:::warning
* Java 無法調用 Koltin 的默認參數怎麼辦?
> 
**使用 `@JvmOverloads` 註解**,它會幫我們產生對應的重載函數讓 Java 調用
```kotlin=
@JvmOverloads // 使用註解
fun defaultParams(times : Int = 10, info : String = "TEST-Params") {
for (i in 0 until times) {
println("$info - $i")
}
}
```
> 
:::
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)
}
```
> 
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")}")
}
```
>
:::warning
* Kotlin 不能直接傳遞數組,所以必須使用 `*` 符號來解包
```kotlin=
fun main() {
val array : Array<String> = arrayOf("1", "2", "3")
val res = toList(*array)
println(res)
}
```
> 
* 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)
}
```
> 
:::
:::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)
// 同上,只是多了返回類型
```
> 
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("")
}
```
> 
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)}")
}
```
> 
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();
}
```
> 
### 類 - 建構函數
* 構造函數與 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")
}
}
```
> 
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()
}
```
> 
:::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")
}
```
> 
:::
### 類 - 屬性
* 類的「屬性」可以理解為類的成員,但是比起成員,我們可以對屬性添加而外的 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}")
}
```
> 
* **Backing field** 幕後字段:它是 Kotlin 屬性自動生成的字段,它只能在當前屬性的訪問器內使用 (拓展類不可使用)
:::danger
* 無法自己調用自己,會導致遞迴
```kotlin=
// Error
var paramValue : Int = 0
get() = paramValue
set(value) = this.paramValue = value
```
> 
:::
```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)
}
```
> 
### 抽象 - 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")
}
}
```
**--實做結果--**
> 
### 內部類 - 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);
}
}
```
**--實作結果--**
> 
### 類的可見性
* **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()
}
}
```
> 
## 類的應用
### 繼承 - 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 成員重複
> 
:::
**--實作結果--**
> 
### 介面實做 - 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()
}
```
**--實做結果--**
> 
## 類 - 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() // 靜態函數
}
```
> 
:::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();
}
}
```
> 
:::
### 物件聲明 - 單例 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() // 呼叫不使用 ()
}
```
> 
:::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()
}
```
**--實作結果--**
> 
:::
### 物件表達 - 匿名類
* 這種表達式類似於 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()
}
```
> 
## 數據類
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)}") // 同上
}
```
**--實做結果--**
> 
:::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")
}
```
> 
:::
### 密封類 - 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
>
> 
**--觀察結果 2--**
> 若是新增了一個 Unknow 就必須在 when 中賦予值 or 使用 else,否則會報錯
>
> 
## 再提及靜態
在 Kotlin 中要定義靜態變數、方法都比 Java 較麻煩一些(Java 只需要加 static 關鍵字即可)
> 
### 單例 - 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 創建一個新的類**,之後的呼叫方法如同我們預估 (請忽略報錯)
>
> 
### 偽靜態 - 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
}
```
**--實做結果--**
> 
:::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
}
```
**--實做結果--**
> 
:::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();
}
}
```
**--實做結果--**
> 
## 空指針檢查
在 ++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) 則會提示錯誤,若是不處理則會編譯無法通過
> 
* 若是需要空指針的操作,就必須 **在類名後方加入 ==?==,代表了該變數 ++可為空++**
> 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()
}
```
**--實作結果--**
> 
### 判空輔助符號
| 符號 | 說明 |
| -------- | -------- |
| <對象>==?.==<方法> | 判斷呼叫的對象是否為空,不為空才往下執行呼叫 |
| <表達式>==?:==<表達式> | 左邊表達式判斷若為空,就執行右邊表達式 |
| <對象>==!!== | 該對象是否為空由程序設計者自己判斷,也就是讓盼空系統失效 |
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!! // 也就是程式設計師自己負責空指針
}
```
> 
## 更多的 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`