Overview of Content
2017 年開始,Kotlin 正式成為 Android 開發的第一級語言,與 Java 平起平坐,而且漸漸開始超越,Android 官方網站現在所給予的 Demo 都是先以 Kotlin 為第一優先,所以得硬起來學習!
我們將探討 Kotlin 的各個主題和功能。
我們將從 Kotlin 函數開始,逐步深入到 Kotlin 物件導向設計、類的應用、單例、匿名類和伴生對象等主題
。接著,我們將研究數據類和空指針檢查等相關概念。最後,我們會回顧 Kotlin 中的靜態概念,包括單例、伴生對象以及 Kotlin 的真靜態功能。这些主題將有助於加深對 Kotlin 語言特性的理解,並幫助你更好地應用 Kotlin 開發應用程式。
以下參考,第一行代碼 (第三版), Kotlin 進階實戰
如有引用參考本文章請詳註出處,感謝 😀
Kotlin 函數特色
在 Kotlin 中函數是第一公民 (First-Class Citizen
),這導致 Kotlin 有以下特色
A. Function
可作為變數(可存取)
B. Function
可做為參數傳遞
C. Function
可以作為回傳(並非像是回傳類,它是回傳一個函數)
D. Function
可在 Runtime 時構造
E. Function
可表示為匿名字面值
Kotlin 函數:參數
● Kotlin 使用關鍵字 fun
,無論定義啥函數都必須以 fun
關鍵字開頭,之後才接上函數名稱、參數,最後使用 :
接上返回值 (若沒有返回值則可忽略)
這種參數表達是 使用了
Pascal
語言的 表示法
A. 默認參數:默認參數方便於不用過多的重載函數,也就是你不需重複寫相同函數名,但不同參數的函數(Overload Function)
fun defaultParams(times : Int = 10, info : String = "TEST-Params") {
for (i in 0 until times) {
println("$info - $i")
}
}
fun main() {
// 使用預設參數
defaultParams()
}
● Java 無法調用 Koltin 的默認參數怎麼辦?
使用
@JvmOverloads
註解,它會幫我們產生對應的重載函數讓 Java 調用@JvmOverloads // 使用註解 fun defaultParams(times : Int = 10, info : String = "TEST-Params") { for (i in 0 until times) { println("$info - $i") } }
B. 命名(指定)參數:可以直接指定參數做傳入,其中也可以配合預設參數
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)
}
C. 可變量參數:可變參數等同於 Java 中的 ...
,而在 Kotlin 中要使用 vararg
關鍵字
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")}")
}
● Kotlin 不能直接傳遞數組,所以必須使用
*
符號來解包fun main() { val array : Array<String> = arrayOf("1", "2", "3") val res = toList(*array) println(res) }
● Java 有規定可變常數必須放置最後一個參數,而 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)
}
● 函數 & 方法 ?
其實
函數 (function)
&方法(method)
是同一個概念,只是在 Java 中較常使用 method,Kotlin 中較常使用 function 來稱呼若再細分可以把方法歸類為,必須寫在類 (Class) 中的 function,而 Kotlin 可以把 funtion 寫在類之外 (Java 不行)
Kotlin 函數:返回值
A. 返回 Unit:Kotlin 沒有所謂的 void,但 Kotlin 可以返回 Unit
fun printlnMyInfo() : Unit { // 可被省略
println("Hello Kotlin")
}
fun main() {
printlnMyInfo()
}
● 從 Unit 源碼可以看出 Unit 返回的是一個單例對象
public object Unit { override fun toString() = "kotlin.Unit" }
B. 返回 Nothing:Nothing 與 Unit 很相像,不過 Nothing 則是代表進入該函數後,絕對不會返回
fun printlnMyInfoNothing() : Nothing {
while (true) {
println("Hello Kotlin Nothing")
}
}
● Kotlin 的源碼,可以看出它是一個類
public class Nothing private constructor()
Kotlin 函數表達式
● 以下說明 Kotlin 函數的特殊表達方式
A. 單表達式:若函數只有一行,則可以使用 =
符號,可省略 1. 大括號、2. return 關鍵字、3. 返回類型 (會自動推倒)
/** 函數 & 方法 都是指相同
* 函數 -> 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)
// 同上,只是多了返回類型
B. 局部函數(Local Function):在函數內再定義函數,如同 Python 一樣
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("")
}
C. 尾遞歸函數:其實這句話由兩個動作組成,1. 尾函數(在 Function 中最後呼叫的函數), 2. 遞歸 Recursively
● 以往我們在使用遞歸時總是會要注意遞歸深度問題,如果遞歸深度過身則會導致 StackOverflowError (範例程式如下)
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
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)}")
}
D. Top-level 函數:Java 中我們必須將函數定義在某個類中,不能寫在檔案頂層,而 Kotlin 可以
頂層函數默認是 public
可以被任意訪問 (當然也可以修改它的訪問權限)
private fun printlnHelloWorld() = println("Hello World")
● Java 可以訪頂層函數? 可以
A. 透過
<檔案名>Kt
訪問// Kt File package base.function fun printlnHelloWorld() = println("Hello World") // Java File public static void main(String[] args) { Function_SpecialKt.printlnHelloWorld(); }
B. 透過
@file:JvmName("自訂名稱")
註解,該註解一定要放在文件頂端! 並且不可與第一個方法混用// 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(); }
Kotlin 物件導向設計
其與 C 有最大的不同在於,物件導向是可以依照一個模板來創建類的 (可創建多個),我們可以把很多事物一起做封裝,用來描述事物(冰箱、電視、調查表...)
程式表達 | 描述 |
---|---|
字段 Field | 資料屬性 |
函數 Method | 行為動作 |
● Kotlin 的一般類(預設)都是
final
類 (修正 Java 的不安全、不強制,這有助於設計的完整性)
Kotlin 類:創建方式
● Kotlin 較特別的特點如下
A. 可以單獨創造類 (Class) 而不需實做
// 可以單獨創建該類,而不需要大括號
class MyClass
B. 省去 new
關鍵字
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();
}
Kotlin 類:建構函數
● 構造函數與 Java 較不同的在於,Kotlin 構造函數有分為 主構造函數、次構造函數;主構造函數沒有函數體,必須透過 init 結構
來完成
類似於 Java 的 static 區塊,但是 init 區塊是每次建構對象都會呼叫
A. 主構造函數:使用 init { }
,主構造函數可以省略 constructor 關鍵字
class MyClz_1 {
init { // 主構造函數
println("init - primary construct")
}
}
fun main() {
MyClz_1()
}
// ----------------------------------------------------------
// 同上
class MyClz_2 constructor() { // 主構造函數 (沒有參數時可以省略)
init {
println("init - primary construct")
}
}
B. 次構造函數:次構造函數必須呼叫主構造函數,使用 :this(...)
主構造函數可以使用 val
, var
修飾屬性,修飾屬性後就可以轉為該類的自身屬性
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()
}
●
init { }
區塊可以有多個區塊,該區塊會按照上到下的順序被調用,接著才會輪到次構造函數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") }
Kotlin 類:屬性
● 類的「屬性」可以理解為類的成員,但是比起成員,我們可以對屬性添加而外的 setter
/getter
操作;在 Kotlin 中可以透過對於屬性的設定限制屬性的存取,其格式如下
## var 屬性格式
var <propertyName> [: <PropertyType>] [= <Initializer>]
[<getter>]
[<setter>]
##-----------------------------------------------------##
## val 屬性格式
val <propertyName> [: <PropertyType>] [= <Initializer>]
[<getter>]
● Kotlin Property 範例
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 屬性自動生成的字段,它只能在當前屬性的訪問器內使用 (拓展類不可使用)
● 無法自己調用自己,會導致遞迴
// Error var paramValue : Int = 0 get() = paramValue set(value) = this.paramValue = value
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)
}
Kotlin 類抽象:abstract
● 抽象類跟 Java 很像,同樣使用 abstract
關鍵字
使用
abstract
關鍵字 就自動轉為open
型態的類
fun main() {
val p1 = Man()
p1.say()
}
interface IHello {
fun say()
}
abstract class Person : IHello {
}
/**
* 抽象則 "必須" 使用 ()
*/
class Man : Person() {
override fun say() {
println("Hello World")
}
}
--實做結果--
Kotlin 內部類:class / inner class 差別
● 內部 class 又分為 1. 靜態 class(與外部類較無關係)、2. 一般 class (必須要使用實例化的外部類才能創建,與外部類關係較大,但是可以直接使用外部元素)
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") //
}
}
}
Kotlin class 內部的 class 預設為
static final class
,它不能訪問外部引用
● 我們將上面的內部類轉為 Java 看看
A. class 靜態內部類,靜態內部類 static final class
類
// 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);
}
}
B. inner class 是一般內部類,而一般內部類為靜態 final class
類
// 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 類的可見性
● Kotlin 默認所有的參數皆為 public
修飾符 | Java | Kotlin |
---|---|---|
無 | 預設,同路靜下的類可見 | |
private | 當前類可見 | 當前類可見 |
protected | 同一個路徑包可見 | 同一個路徑包可見 |
public | 全部域可見 | 預設,全部域可見 |
● Kotlin 還有另外一些修飾符 internal、inner
A. internal:若是希望該類不會被外部調用則可以使用,就像是 Java 的內部類
internal
如果使用在外部類,其特別之處在於僅限「同模組內可調用」// java 版本 public class A { // internal 就像是私有內部類 private static class C { } } // kt 版本 class A { internal class C { // 使用 internal 關鍵字,內部不公開 } }
B. inner:一般非靜態內部類 (另個小節回說到),外部函數可使用
// java 版本 public class A { // inner 就像是內部類 public class B { } } // kt 版本 class A { inner class B { // 使用 inner 關鍵字,內部公開 } }
Kotlin Enum 類
● Kotlin 的 Enum 類與 Java 類似,這裡我們可以配合上面所學的主建構函數來為 Enum 類添加屬性
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()
}
}
Kotlin 類的應用
Kotlin 類繼承 - Class
● Kotlin 默認類是不可以繼承的,也就用是 final 描述類,若要繼承需要使用關鍵字 open
打開這個類;但若 Class 本來就是 abstruct
的類那就原本就是 open 的 (這也滿符合語意的)
● 在繼承中,若是沒有主構造函數,只有次構造函數,則必須使用 super
呼叫父類的建構函數
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()
}
● 在主構造函數裡面宣告的 val、var 字段,會自動轉為該類別的字段,所以繼承者不可再用 val、var 在主構造函數中宣告相同的屬性 (因為名稱重複會衝突,但是一般屬性可以)
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 成員重複
--實作結果--
Kotlin 介面 - interface
● Kotlin 也有 interface
(Dart 語言就沒有),使用方法如同 Java,內部就可以宣告需要實做的 function
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()
}
介面與繼承的順序可以隨意顛倒 (Java 則是有規定要先有繼承,才能有
interface
)
--實做結果--
Kotlin object 關鍵字:單例、匿名、伴生
object
關鍵字使用在物件聲明,物件表達,伴生類
靜態區塊(伴生類):companion object
Kotlin 中沒有 static 關鍵字,在其中也沒有靜態函數 & 屬性
● companion object {}
區塊相當於 Java 中的 static {}
靜態區塊,會在類加載成功後就存在,並且只會加載一次
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() // 靜態函數
}
● Java 類如何調用 Kotlin 中的 Companion 內容? 使用
@JvmField
(屬性),@JvmStatic
(方法)class Companion { companion object { @JvmField var info : String? = null @JvmStatic fun printInformation() { println(info) } } }
Java 類調用如下 (有註解才可以調用)
public class Java_Main { public static void main(String[] args) { Companion.info = "123"; Companion.printInformation(); } }
物件聲明:單例 object
● 先來看看 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 即可,不須私有化建構函數、也不需要靜態方法、同步;呼叫單例內的方法是不需使用 ()
object Singleton {
fun print() {
println("I am Kotlin Singleton");
}
}
fun main() {
// Singleton() // Error
Singleton.print() // 呼叫不使用 ()
}
若是 Singleton 要實例化則會錯誤
● 以下不使用 Kotlin#
object
關鍵字(Object 類是使用了 static 加載對象),使用 Kotlin 模仿 Java 的懶加載方式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() }
--實作結果--
物件表達:Kotlin 匿名類
● 這種表達式類似於 Java 中的匿名類,並且可以支持實現多個接口
object 它可實現多個方法
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()
}
Kotlin 數據類:data class、Selded
Koltin 有簡化數據類的使用,讓我們不用寫太多程式
Bean 類 - data
● 先來複習一下 Java 的 Bean 類寫法,必須覆寫二個方法(通常會寫三個),equals
、hashcode
、toString
(下面註釋會解釋)
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 函數)
A. equals
/ hashCode
B. toString
C. componentN
:有多少個屬性 N 就會是多少
D. copy
:可以複製全部或是部分屬性
// 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)}") // 同上
}
--實做結果--
● data class 的拷貝函數是 深拷貝 還是 淺拷貝? 淺拷貝
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") }
Kotlin 密封類 - Sealed
● 密封類:一般來說 Kotlin 並不會判斷 (使用 when 判斷) 是否是完全符合使用者的設定,若是不符合也不會警告,只會拋向 else,這會導致設計出的程式不構安全,Kotlin 也有解決的辦法
● Sealed 可以讓你寫出更健全的程式,它的功能類似於 Android 的 @IntDef
註解,並且功能更強大,若是 判斷沒有實作 Sealed 密封類的判斷編譯器會直接報錯!這是個非常棒的特點!!
Sealed 類是一個抽象類,子類可以在任意位置
先來做一個沒有密封的類,並對其做判斷,會發現 when 必須要使用 else 否則會錯誤 (編譯根本過不了),儘管不需要也要增加
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 加以描述
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 靜態
在 Kotlin 中要定義靜態變數、方法都比 Java 較麻煩一些(Java 只需要加 static 關鍵字即可)
單例 - object
● 透過 object 來描述可以讓該類變成單例類(不須 class 關鍵字),事實上 object 內部它並不是靜態方法,而是透過
object TestSingle {
fun say() {
println("Hello object")
}
}
fun main() {
// 實際上是調用了 TestSingle.INSTANCE.say()
TestSingle.say()
}
● 透過工具 Tools
-> Kotlin
-> Show Kotlin ByteCode
-> Decompile
可以看到反編譯的程式,內部就是使用餓漢加載的方式
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 創建一個新的類,之後的呼叫方法如同我們預估 (請忽略報錯)
Kotlin 偽靜態:Companion object
● 全名是 companion object,必須使用在類內,類之外不可以使用,並且實際上它並不是靜態類,而是該類的 伴生類
class MyCompanion {
fun sayHello() {
println("Hello~ outside class")
}
companion object {
fun sayHello() {
println("Hello~ companion object")
}
}
}
fun main() {
MyCompanion().sayHello() // normal
MyCompanion.sayHello() // companion
}
--實做結果--
● 反編譯 Kotlin 的 Companion 來觀察
以下是 byteCode 反編譯過後的程式,可以看到 companion object 是一個靜態內部類,並且在外部使用 static 直接加載,所以可以調用,也證明 companion 並不是真正的靜態,它是伴生類
@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. 頂層方法
A. 註解使用 @JvmStatic
註解:經過編譯過後就會讓這些方法成為真正的靜態方法,並且 該註解只能使用在 companion object 內
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 仍然在,但是外部產了了相同的函數,這就是衝突的原因
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();
}
}
}
B. 頂層方法:會發現 Kotlin 的方法並不需要定義在類 (Class) 內,可以定義在外部,在 外部 (不在類內) 的方法就是頂層方法
/**
* External.kt 檔案
*/
// 不存在類內's 方法
fun externalSayHello() {
println("Hello~ External top function")
}
Kt 會自動依照檔案名稱生成一個,檔名 + Kt.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 呼叫
class CallExternal {
public static void main(String...s) {
ExternalKt.externalSayHello();
}
}
--實做結果--
空引用、空指標檢查
在 Source 時期 的 空指針檢查是 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) 則會提示錯誤,若是不處理則會編譯無法通過
● 若是需要空指針的操作,就必須 在類名後方加入 ?,代表了該變數 可為空
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()
}
Ex: PrintWord (hello : String ?, time : int ?),表示這兩個變數可以為空指針,也就是呼叫時可以傳入 null
--實作結果--
判空輔助符號
符號 | 說明 |
---|---|
<對象>?.<方法> | 判斷呼叫的對象是否為空,不為空才往下執行呼叫 |
<表達式>?:<表達式> | 左邊表達式判斷若為空,就執行右邊表達式 |
<對象>!! | 該對象是否為空由程序設計者自己判斷,也就是讓盼空系統失效 |
A. ?.
判空符號
fun isEmpty(s: BookShop?) {
s?.getDescribe()
s?.workTime()
// 相當於下方程式
if(s != null) {
s.getDescribe()
}
if(s != null) {
s.workTime()
}
}
B. ?:
賦值符號
fun decide() : Int {
val a : Int? = null
val b : Int = 123
// return if(a != null) a else b
return a ?: b // 功能同上
}
C. !!
,非空斷言工具
fun notEmpty(s: BookShop?) {
s!!.getDescribe()
s!!.workTime()
}
/**
* 返回值 + ? 代表可能返回空
*/
fun returnNotEmpty() : BookShop? {
val s : BookShop? = null
return s!! // 也就是程式設計師自己負責空指針
}
更多的 Kotlin 語言相關文章
在這裡,我們提供了一系列豐富且深入的 Kotlin 語言相關文章,涵蓋了從基礎到進階的各個方面。讓我們一起來探索這些精彩內容!
Kotlin 語言基礎
● Kotlin 語言基礎:想要建立堅實的 Kotlin 基礎?以下這些文章將帶你深入探索 Kotlin 的關鍵基礎和概念,幫你打造更堅固的 Kotlin 語言基礎
Kotlin 特性、特點
● Kotlin 特性、特點:探索 Kotlin 的獨特特性和功能,加深對 Kotlin 語言的理解,並增強對於語言特性的應用
Kotlin 進階:協程、響應式、異步
● Kotlin 進階:協程、響應式、異步:若想深入學習 Kotlin 的進階主題,包括協程應用、Channel 使用、以及 Flow 的探索,請查看以下文章