前言
以一個java老鳥的角度,如何去看 kotlin。 Java源代碼應該如何用Kotlin重構。 如何正確學習kotlin並且應用到實際開發中。本文將會探究。
本文分兩大塊,重難點和潛規則。
重難點:Kotlin中可以獨立出來講解的大塊知識點。提供單獨Demo。這部分大多數是Kotlin開創的新概念(相比於Java)。
潛規則:Kotlin是谷歌用來替換Java的,它和java百分百完全兼容,但是實際上java轉成kotlin之後,需要我們手動修改很多東西,甚至某些部分必須打散重構來達到最優編碼。其中,kotlin的某些特性和java不同,甚至完全反轉。這部分知識點比較零碎,單獨Demo不方便提供,就以小例子的形式來寫。
正文大綱
- 重難點lambda以及操作符高階函數以及操作符Kotlin泛型集合操作協程操作符重載
- 潛規則Kotlin 文件和類不存在一對一關係共生體繼承修飾符空指針問題
正文
重難點
協程
想了很久,關於協程的內容,在官網上確實有很多內容,基礎知識概念,基本使用,以及 流操作,通道,異常處理,併發處理等,不方便在這裡展開。具體的參照:Kotlin中文網
這裡具體去學習。本章節,只總結一下近期查閱資料並經本人驗證的知識點。
概念
英文 coroutines : /,kəuru:'ti:n/ 意: 協同程序。 簡稱協程。
Kotlin提出協程概念,是為了簡化異步編程,讓開發者更容易控制函數的執行流程。
協程和線程的聯繫和區別
在操作系統OS中,進程是資源分配的最小單位,線程是任務調度的最小單位。而協程則是處在線程內部的“微線程”,或者說輕量級線程。 由於線程在OS中是稀缺資源,所有OS平臺的線程數量都是有上限的,平時編程我們會用線程池來管理線程數量,熟悉線程池的同學應該知道,線程池管理線程,無論是核心線程還是非核心線程,都不會隨意去創建,除非迫不得已。
用線程解決異步問題
- 多線程同步編程可以通過加鎖解決數據的線程安全問題,但是加鎖會降低程序執行效率,並且鎖多了,會有死鎖隱患
- 線程的狀態轉換完全由內核控制,程序員開發者無法干涉
- 線程的是稀缺資源,不能隨意創建,使用線程解決異步問題,線程的初始化,上下文切換(CPU輪轉),線程狀態切換(sleep,yield...), 變量加鎖操作(synchronized關鍵字),都會使得線程的使用代價比較大
用協程解決異步問題
- 協程是運行在線程之上的優化產物,或稱“微線程”。協程依賴線程運行,複雜的底層邏輯被封裝在庫內,使用時無需關心所處線程狀態
- 使用協程,開發者可以自己控制協程的狀態(suspend掛起,resume恢復),而不會像線程那樣依賴底層調度,時間片爭奪。
- 一個線程可以跑多個協程,一個協程也可以分段在多個線程上執行
- 協程 是 非阻塞的,當前協程掛起之後,所在線程資源並不會浪費,它會去執行其他協程(如果有的話)
- 協程 相對於線程這種OS中的稀缺資源,它是極其輕量級的,就算你開一百萬個協程,對於系統的壓力也不會像大量線程那樣大(別說一百萬個,linux系統的線程數量上線是1000,超過這個值系統就無法正常運行).
- 總之一句話 : 協程的出現,讓程序開發者對程序邏輯的掌控提升到了一個新的境界,想象一下,一個函數正在執行,你想讓他在某個時刻暫停,然後在另一個時刻繼續。利用線程恐怕很難做到。協程中,輕而易舉。
基本使用
module級別的build.gradle 中 引入庫依賴,推薦使用最新版(目前穩定版是1.3.3)
<code>dependencies { //... implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3" // 如果你需要用到協程調度器Dispatchers的話,必須加上這個依賴}/<code>
協程的創建
創建協程有多種方式,全局/獨立,同步/異步,並且可以指定 "協程調度器"
- runBlocking
<code>fun main() { runBlocking { println("這是runBlocking協程...") }}/<code>
- launch
<code>fun main() { runBlocking { println("這是runBlocking協程...") launch { println("這是runBlocking內部的runBlocking協程...") delay(2000) println("延遲了2000MS之後,再打印") } }}/<code>
- GlobalScope.launch
<code>fun main() { println ("協程,相對於主線程來說,都是異步的,也就是說,你在這裡插入協程邏輯,主線程的邏輯並不會被阻塞") GlobalScope.launch { delay(3000) println("GlobalScope.launch 創建協程 ") } println("主線程繼續") Thread.sleep(5000)}/<code>
- Global.async
<code>fun main() { runBlocking { val async: Deferred<string> = GlobalScope.async { println("這是一個異步協程,他將返回一個Deferred") delay(2000) "異步任務返回值" } println("主線程繼續:" + async.await()) } Thread.sleep(5000) }/<string>/<code>
“騷操作”
關心協程的人一般都會十分關注它到底能給我們異步編程帶來怎樣的便利。這裡總結幾個不用協程實現起來很麻煩的
騷操作。- 如果有一個函數,它的返回值需要等到多個耗時的異步任務都執行完畢返回之後,組合所有任務的返回值作為 最終返回值
<code>fun test6(): String = runBlocking { var finalRes = "" coroutineScope { launch { delay(1000) finalRes = finalRes.plus("1") } launch { delay(2000) finalRes = finalRes.plus("2") } launch { delay(3000) finalRes = finalRes.plus("3") } } finalRes}fun main() { val test6 = test6() println("最終返回值是: $test6")}/<code>
最終返回結果為(延遲3秒之後打印):
<code>最終返回值是: 123/<code>
- 如果有一個函數,需要順序執行多個網絡請求,並且後一個請求依賴前一個請求的執行結果
<code>import kotlinx.coroutines.* suspend fun getToken(): String { for (i in 0..10) { println("異步請求正在執行:getToken :$i") delay(100) } return "ask" } suspend fun getResponse(token: String): String { for (i in 0..10) { println("異步請求正在執行:getResponse :$token $i") delay(100) } return "response" } fun setText(response: String) { println("setText 執行,時間: ${System.currentTimeMillis()}") } fun main() { GlobalScope.launch(Dispatchers.Unconfined) { var token = GlobalScope.async(Dispatchers.Unconfined) { return@async getToken() }.await() // 創建異步任務,並且 阻塞執行 await 是阻塞執行取得結果 var response = GlobalScope.async(Dispatchers.Unconfined) { return@async getResponse(token) }.await() // 創建異步任務,並且立即執行 setText(response) } Thread.sleep(20000) } /<code>
執行結果:
<code>異步請求正在執行:getToken :0異步請求正在執行:getToken :1異步請求正在執行:getToken :2 異步請求正在執行:getToken :3 異步請求正在執行:getToken :4 異步請求正在執行:getToken :5 異步請求正在執行:getToken :6 異步請求正在執行:getToken :7 異步請求正在執行:getToken :8 異步請求正在執行:getToken :9 異步請求正在執行:getToken :10 異步請求正在執行:getResponse :ask 0 異步請求正在執行:getResponse :ask 1異步請求正在執行:getResponse :ask 2 異步請求正在執行:getResponse :ask 3 異步請求正在執行:getResponse :ask 4 異步請求正在執行:getResponse :ask 5 異步請求正在執行:getResponse :ask 6 異步請求正在執行:getResponse :ask 7 異步請求正在執行:getResponse :ask 8 異步請求正在執行:getResponse :ask 9 異步請求正在執行:getResponse :ask 10setText 執行,時間: 1578904290520/<code>
- 當前正在執行一項異步任務,但是你突然不想要它執行了,隨時可以取消
<code>fun main() { // 協程任務 val job = GlobalScope.launch(Dispatchers.IO) { for (i in 0..100){// 每次掛起100MS,100次也就是10秒 println("協程正在執行 $i") delay(100) } } // 但是我在1秒之後就取消協程 Thread.sleep(1000) job?.cancel() println( "btn_right 結束協程") } /<code>
執行結果(本該執行100輪的打印,只持續了10輪):
<code>協程正在執行 0 協程正在執行 1 協程正在執行 2 協程正在執行 3 協程正在執行 4 協程正在執行 5協程正在執行 6協程正在執行 7 協程正在執行 8協程正在執行 9btn_right 結束協程 Process finished with exit code 0/<code>
- 如果你想讓一個任務最多執行3秒,超過3秒則自動取消
<code>import kotlinx.coroutines.* fun main() = runBlocking { println("限時任務中結果是:" getResFromTimeoutTask()) } suspend fun getResFromTimeoutTask(): String? { // 忘了,它會保證內部的協程代碼都執行完畢,所以不能這麼寫 return withTimeoutOrNull(1300) { for (i in 0..10) { println("I'm sleeping $i ...") delay(500) } "執行結束" } } 執行結果I'm sleeping 0 ... I'm sleeping 1 ... I'm sleeping 2 ... 限時任務中結果是:null Process finished with exit code 0/<code>
執行結果
<code>I'm sleeping 0 ... I'm sleeping 1 ... I'm sleeping 2 ... 限時任務中結果是:null Process finished with exit code 0/<code>
總結
協程作為kotlin 區別於java的新概念,它的出現是為了解決java不好解決的問題,比如層層回調導致代碼臃腫,比如 異步任務執行流程不好操控等。本章節篇幅有限,無法展開說明,但是對於新手而言,看完本章應該能對協程的作用有一個大概的認知。本人也是初步研究,後續有更深入的瞭解之後,再進行專文講解吧。
操作符重載
概念
說人話,像是一元操作符 ++自加,二元操作符 +相加 ,默認只支持數字類型,比如Int. 但是通過操作符的重載,我們可以讓任意類 都能 ++自加,且返回一個想要的對象。操作符執行的邏輯,完全看我們如何去設計。
分類
按元素級別
- 一元
- 二元
按實現方式
- 成員函數
- 擴展函數
栗子
看到上面的一大堆,肯定有點懵,看個例子解決疑問。上面我用兩種維度來對操作符重載進行了分類,那麼,先試試:成員函數的方式來重載一個一元操作符
<code>class A(i: Int, j: Int) { var i: Int = i var j: Int = j /** * 重載++操作 */ operator fun inc(): A { return A(i++, j++) } override fun toString(): String { return "[i=$i , j=$j]" }}/<code>
如上代碼,注意看:
<code> operator fun inc(): A { return A(i++, j++) }/<code>
Kotlin的操作符重載和 c++,dart語言內的操作符重載寫法完全不同,它不再是直接把操作符放到了 重寫的過程中,而是每一種支持重載的操作符都有一個對應的 函數名
正如:上表格中的 a++ 操作符,對應的函數就是 a.inc()
調用的時候:
<code>fun main() { var a = A(1, 2) println("a:$a") println("a++:${a++}")}/<code>
打印結果:
<code>a:[i=1 , j=2]a++:[i=2 , j=3]/<code>
再看一個二元運算符重載的栗子,這次我們不用成員函數,而是用擴展函數:
<code>class A(i: Int, j: Int) { var i: Int = i var j: Int = j override fun toString(): String { return "[i=$i , j=$j]" }}/*** 重載A類的 x+y 操作*/operator fun A.plus(a: A): A { return A(this.i + a.i, this.j + a.j)}fun main() { val x = A(1,1) val y = A(2,2) println(x+y)}/<code>
這裡演示的是 A類的 x+y 操作符重載。細節應該不用多說。
打印結果:
<code>[i=3 , j=3]/<code>
再來一個較為複雜的栗子, 重載 a[i]
<code>/** * 比如,B類中有一個成員,list,我想重載操作符,直接取到list中的元素 */class B { val list: MutableList= mutableListOf(1, 2, 3, 4, 5, 6, 7, 8, 9)}//a[i]operator fun B.get(i: Int): Int { return list[i]}fun main() { val b = B() println("${b[2]}")} /<code>
打印結果:
<code>3/<code>
最後一個栗子:a > b,對應函數為:a.compare(b)
<code>/** * 學生class */data class Student(val math: Int = 0, val chinese: Int = 0, val english: Int = 0)fun Student.toString():String{ return "[math:${math} chinese:${chinese} english:${english}]"}fun Student.totalScore(): Int { return math + chinese + english}/** * 比如,我們要直接比較兩個學生的總分 */operator fun Student.compareTo(s: Student): Int { return this.totalScore() - s.totalScore()//比較2個學生的總分}fun main() { val s1 = Student(math = 50, chinese = 90, english = 100) val s2 = Student(math = 80, chinese = 70, english = 60) println("s1:${s1}") println("s2:${s2}") //比如存在這兩個學生,我要知道他們的總分誰高誰低 println("學生s1,s2的總分:${if(s1 > s2) "s1比較高" else "s2比較高" }")}/<code>
打印結果:
<code>s1:Student(math=50, chinese=90, english=100)s2:Student(math=80, chinese=70, english=60)學生s1,s2的總分:s1比較高/<code>
總結
通過以上幾個栗子,應該能看出,kotlin的操作符重載,編碼和使用都十分簡單。重載之前需要確定兩件事
- 根據業務需求,確定要重載的是哪一個操作符,雖然操作符的最終執行邏輯完全由我們自定義,但是我們重載操作符的目的是為了 讓使用者更簡單的理解業務代碼,所以,要選擇原本意義更加符合業務需求的操作符。Kotlin支持的操作符在上述表格中都列舉出來了,支持大部分一元二元操作符,但是二元中不支持===的重載,不支持三元操作符bool?a:b這種 。
- 確定重載函數的 入參類型,個數,以及返回值類型,並且編寫操作符的執行邏輯。
Hi~ o(* ̄▽ ̄ *)ブ
疫情無情,人有情!
小編宅家期間整理了約 300G 關於BATJ大廠的面試資料
需要的小夥伴都可以轉發+私信回覆“1” 免費領取!
閱讀更多 享學課堂online 的文章