From Java To Kotlin:空安全、擴展、函數、Lambda很詳細,這次終於懂了

来源:https://www.cnblogs.com/Seachal/archive/2023/05/20/17417132.html
-Advertisement-
Play Games

From Java To Kotlin, 空安全、擴展、函數、Lambda 概述(Summarize) * • Kotlin 是什麼? * • 可以做什麼? * • Android 官方開發語言從Java變為Kotlin,Java 有哪些問題? * • Kotlin的優點 * • Kotlin 特性 ...


From Java To Kotlin, 空安全、擴展、函數、Lambda

概述(Summarize)

  • • Kotlin 是什麼?

  • • 可以做什麼?

  • • Android 官方開發語言從Java變為Kotlin,Java 有哪些問題?

  • • Kotlin的優點

  • • Kotlin 特性(Features)


Kotlin 是什麼?

Kotlin 出自於捷克一家軟體研發公司 JetBrains ,這家公司開發出很多優秀的 IDE,如 IntelliJ IDEA、DataGrip 等都是它的傑作,包括 Google 官方的 Android IDE -- Android Studio ,也是 IntelliJ IDEA 的插件版。

Kotlin 源於 JetBrains 的聖彼得堡團隊,名稱取自聖彼得堡附近的一個小島 ( Kotlin Island ) ,和 Java一樣用島嶼命名,JetBrains 在 2010 年首次推出 Kotlin 編程語言,併在次年將之開源。

  • • Kotlin 是一種在 Java 虛擬機上運行的靜態類型編程語言,被稱之為 Android 世界的Swift。

  • • Kotlin 可以編譯成Java位元組碼。也可以編譯成 JavaScript,方便在沒有 JVM 的設備上運行。

  • • 在Google I/O 2017中,Google 宣佈 Kotlin 成為 Android 官方開發語言,替代 Java 語言


Kotlin 代碼會被編譯成Java位元組碼,所以和 Java 相容 圖片


可以做什麼?

  • • Android

  • • Server-side

  • • Multiplatform Mobile

    Kotlin Multiplatform Mobile is in Beta!

  • • Multiplatform libraries

    Create a multiPlatform library for JVM, JS, and Native platforms.

    圖片 可以做很多方向的開發!


Android 官方開發語言從Java變為Kotlin,Java 有哪些問題?

  • • 空引用(Null references):Java 中的 null 值是經常導致程式運行出錯的原因之一,因為 Java 不支持空安全。

  • • 更少的函數式編程特性:Java 語言在函數式編程方面的支持相對較弱,雖然 Java 8 引入了 Lambda 表達式和 Stream API,但是 Kotlin 語言在這方面的支持更加全面和友好。

  • • 不夠靈活,缺乏擴展能力:我們不能給 第三方 SDK 中的classes 或者 interfaces 增加新的方法。。

  • • 語法繁瑣,不夠簡潔:Java 語言比 Kotlin 語言更為冗長,需要寫更多的代碼來完成相同的任務,這可能會降低開發效率。

Kotlin的優點

Modern, concise and safe programming language

  • • 簡約:使用一行代碼創建一個包含 getters、 setters、 equals()、 hashCode()、 toString() 以及 copy() 的 POJO:

  • • 安全:徹底告別那些煩人的 NullPointerException

  • • 互操作性: Kotlin 可以與 Java 混合編程,Kotlin 和 Java 可以相互調用,目標是 100% 相容。


Kotlin 特性(Features)

  • • 空安全(Null safety)

  • • 類型推斷(Type inference)

  • • 數據類 (Data classes)

  • • 擴展函數 (Extension functions)

  • • 智能轉換(Smart casts)

  • • 字元串模板(String templates)

  • • 單例(Singletons)

  • • 函數類型 (Function Type )

  • • Lambda 表達式

  • • 高階函數(Primary constructors)

  • • 函數字面量和內聯函數(Function literals & inline functions)

  • • 類委托(Class delegation)

  • • 等等......


基本語法 (Basic Syntax )

  • • 變數(Variables)

  • • 基本數據類型( Basic Data Type )

  • • 空安全(Null Safety )

  • • 函數聲明( Define Function )

  • • 讓函數更好的調用( Making functions easier to call )

  • • 命名參數/具名參數 (Named arguments)

  • • 參數預設值(Default arguments)


變數(Variables)

在 Java/C 當中,如果我們要聲明變數,我們必須要聲明它的類型,後面跟著變數的名稱和對應的值,然後以分號結尾。就像這樣:

Integer price = 100;

而 Kotlin 則不一樣,我們要使用val或者是var這樣的關鍵字作為開頭,後面跟“變數名稱”,接著是“變數類型”和“賦值語句”,最後是分號結尾。就像這樣:

/*
關鍵字     變數類型
 ↓          ↓           */
var price: Int = 100;   /*
     ↑            ↑
   變數名        變數值   */

在 Kotlin 裡面,代碼末尾的分號省略不寫,就像這樣:

var price = 100 // 預設推導類型為: Int

另外,由於 Kotlin 支持類型推導,大部分情況下,我們的變數類型可以省略不寫,就像這樣:


var price = 100 // 預設推導類型為: Int

var 聲明的變數,我們叫做可變變數,它對應 Java 里的普通變數。

val 聲明的變數,我們叫做只讀變數,它相當於 Java 裡面的 final 變數。

var price = 100
price = 101

val num = 1
num = 2 // 編譯器報錯

var, val 反編譯成 Java :

圖片


我們已經知道了 val 屬性只有 getter,只能保證引用不變,不能保證內容不變。例如,下麵的代碼:

class PersonZ {
    var name = "zhang"
    var age = 30
    val nickname: String
        get() {
            return if (age > 30) "laozhang" else "xiaozhang"
        }
    fun grow() {
        age += 1
    }

屬性 nickname 的值並非不可變,當調用 grow() 方法時,它的值會從 "xiaozhang" 變為 "laozhang",

不過因為沒有 setter,所以無法直接給 nickname 賦值

編譯時常量

const 只能修飾沒有自定義 getter 的 val 屬性,而且它的值必須在編譯時確定

val time = System.currentTimeMillis()
// 這種會報錯
const val constTime = System.currentTimeMillis()

基本數據類型( Basic Data Type )

Kotlin 的基本數值類型包括 Byte、Short、Int、Long、Float、Double 等。

類型 位寬度 備註
Double 64 Kotlin 沒有 double
Float 32 Kotlin 沒有 float
Long 64 Kotlin 沒有 long
Int 32 Kotlin 沒有 int/Intege
Short 16 Kotlin 沒有 short
Byte 8 Kotlin 沒有 byte

在 Kotlin 語言體系當中,是沒有原始類型這個概念的。這也就意味著,在 Kotlin 里,一切都是對象。


空安全(Null Safety )

既然 Kotlin 中的一切都是對象,那麼對象就有可能為空。如果我寫這樣的代碼:

val i: Double = null // 編譯器報錯

以上的代碼並不能通過 Kotlin 編譯。 圖片

這是因為 Kotlin 強制要求開發者在定義變數的時候,指定這個變數是否可能為 null

對於可能為 null 的變數,我們需要在聲明的時候,在變數類型後面加一個問號“?”:

val i: Double = null // 編譯器報錯
val j: Double? = null // 編譯通過

並且由於 Kotlin 對可能為空的變數類型做了強制區分,這就意味著,“可能為空的變數”無法直接賦值給“不可為空的變數”,反過來 “不可為空的變數” 可以賦值給“可能為空的變數” 。


var i: Double = 1.0
var j: Double? = null

i = j  // 編譯器報錯
j = i  // 編譯通過

這麼設計的原因是,從集合邏輯上:可能為空 包含 不可為空

而如果我們實在有這樣的需求,也不難實現,只要做個判斷即可:

var i: Double = 1.0
val j: Double? = null

if (j != null) {
    i = j  // 編譯通過
}

圖片


圖片


圖片


圖片


圖片


函數聲明( Define Function )

在 Kotlin 當中,函數的聲明與 Java 不太一樣。 Java:

   public String helloFunction(@NotNull String name) {
      return "Hello " + name + " !";
   }

Kotlin :

/*
關鍵字    函數名          參數類型   返回值類型
 ↓        ↓                ↓       ↓      */
fun helloFunction(name: String): String {
    return "Hello $name !"
}/*   ↑
   花括弧內為:函數體
*/
  • • 使用了 fun 關鍵字來定義函數;

  • • 返回值類型,緊跟在參數的後面,這點和 Java 不一樣。


如果函數體中只有一行代碼,可以簡寫

  • • return可以省略

  • • { } 花括弧可以省略

  • • 直接用 = 連接,變成一種類似 變數賦值的 函數形式

fun helloFunton(name:String):String = "Hello $name !"

我們稱之為單表達式函數

由於Kotlin支持類型推導,返回值類型可以省略:

fun helloFunton(name:String):= "Hello $name !"

這樣看起來就更簡潔了。


讓函數更好的調用( Making functions easier to call )

命名參數/具名參數 (Named arguments)

以前面的函數為例子,我們調用它:

helloFunction("Kotlin")

和 Java 一樣。

不過,Kotlin 提供了一些新的特性,如命名函數參數 舉個例子,現在有一個函數:


fun createUser(
    name: String,
    age: Int,
    gender: Int,
    friendCount: Int,
    feedCount: Int,
    likeCount: Long,
    commentCount: Int
) {
    //..
}

如果像 Java 那樣調用:

createUser("Tom", 30, 1, 78, 2093, 10937, 3285)

就要嚴格按照參數順序傳參:

  • • 參數順序調換,參數就傳錯了,不好維護

  • • 當參數是一堆數字,很難知道數字對應的形參,可讀性不高

Kotlin 參數調用:

createUser(
    name = "Tom",
    age = 30,
    gender = 1,
    friendCount = 78,
    feedCount = 2093,
    likeCount = 10937,
    commentCount = 3285
)

我們把函數的形參加了進來,形參和實參用 = 連接,建立了兩者的對應關係。這樣可讀性更強。

如果想修改某個參數例如feedCount也可以很方便的定位到參數。 這樣易維護


參數預設值(Default arguments)

fun createUser(
    name: String,
    age: Int,
    gender: Int = 1,
    friendCount: Int = 0,
    feedCount: Int = 0,
    likeCount: Long = 0L,
    commentCount: Int = 0
) {
    //..
}

gender、likeCount 等參數被賦予了預設值,當我們調用時,有些有預設值的參數就可以不傳參,Kotlin編譯器自動幫我們填上預設值。


createUser(
    name = "Tom",
    age = 30,
    friendCount = 50
)

在 Java 當中要實現類似的邏輯,我們就必須手動定義新的“3 個參數的 createUser 函數”,或者是使用 Builder 設計模式。



Classes and Objects

  • • 類 (Class)

  • • 抽象類 (Abstract Class)

  • • 繼承(Extend)

  • • 介面和實現 (Interface and implements)

  • • 嵌套類和內部類( Nested and Inner Classes )

  • • 數據類(Data Class )

  • • object 關鍵字

  • • object:匿名內部類

  • • object:單例模式

  • • object:伴生對象

  • • 擴展 (Extension)

  • • 什麼是擴展函數和擴展屬性?

  • • 擴展函數在 Android 中的案例


類 (Class)

Java

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 屬性 name 沒有 setter
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

Class

Kotlin

class Person(val name: String, var age: Int)

Kotlin 定義類,同樣使用 class 關鍵字。

Kotlin 定義的類在預設情況下是 public 的。

編譯器會幫我們生成“構造函數”,

對於類當中的屬性,Kotlin 編譯器也會根據實際情況,自動生成 getter 和 setter。

和Java相比 Kotlin 定義一個類足夠簡潔。


抽象類與繼承

抽象類 (Abstract Class)

abstract class Person(val name: String) {
    abstract fun walk()
    // 省略
}

繼承(Extend)

//                      Java 的繼承
//                           ↓
public class MainActivity extends Activity {
    @Override
    void onCreate(){ ... }
}

//              Kotlin 的繼承
//                 ↓
class MainActivity : AppCompatActivity() {
    override fun onCreate() { ... }
}


* * *

介面和實現 (Interface and implements)
--------------------------------

Kotlin 當中的介面(interface),和 Java 也是大同小異的,它們都是通過 interface 這個關鍵字來定義的。

interface Behavior {
    fun walk()
}

class Person(val name: String): Behavior {
    override fun walk() {
        // walk
    }
    // ...
}


可以看到在以上的代碼中,我們定義了一個新的介面 Behavior,它裡面有一個需要被實現的方法 walk,然後我們在 Person 類當中實現了這個介面。

**Kotlin 的繼承和介面實現語法基本上是一樣的。**

* * *

Kotlin 的介面,跟 Java 最大的差異就在於,介面的方法可以有預設實現,同時,它也可以有屬性。

interface Behavior {
    // 介面內的可以有屬性
    val canWalk: Boolean
    // 介面方法的預設實現
    fun walk() {
        if (canWalk) {
            // do something
        }
    }
}
class Person(val name: String): Behavior {
    // 重寫介面的屬性
    override val canWalk: Boolean
        get() = true
}


我們在介面方法當中,為 walk() 方法提供了預設實現,如果 canWalk 為 true,才執行 walk 內部的具體行為。

Kotlin 當中的介面,被設計得更加強大了。

在 Java 1.8 版本當中,Java介面也引入了類似的特性。

* * *

嵌套類和內部類( Nested and Inner Classes )
-----------------------------------

Java 當中,最常見的嵌套類分為兩種:**非靜態內部類**、**靜態內部類**。Kotlin 當中也有一樣的概念。

class A {
    class B {
    }
}


以上代碼中,B 類,就是 A 類裡面的嵌套類。

**註意:** 無法在 B 類當中訪問 A 類的屬性和成員方法。

因為Kotlin 預設嵌套類(B類)是一個靜態內部類

Kotlin 嵌套類反編譯成 Java 代碼:

![圖片](https://mmbiz.qpic.cn/mmbiz_jpg/RlsGJDiaDGWAfxv2W9t2s7ziccdBdDy4fMltLficicfCIKtsbpg9xibwdyY9y9GykTG94bhJAlfLEI7cia6DftOVcbGg/640?wx_fmt=jpeg)

* * *

public class JavaOuterInnerClass2 {
   // 內部類
    public  class InnerClass {
    }
    // 靜態內部類
    public  static  final   class  StaticInnerClass{
    }
}


通過 javac 命令 編譯成 class 文件後:

*   • InnerClass
    
    ![圖片](https://mmbiz.qpic.cn/mmbiz_jpg/RlsGJDiaDGWAfxv2W9t2s7ziccdBdDy4fMia6b5yobibOQVfmySInEdfBH1Wp8ibeuzWx7wVRSLmHSMZhvoV8osPhrw/640?wx_fmt=jpeg "null")
    
*   • StaticInnerClass
    
    ![圖片](https://mmbiz.qpic.cn/mmbiz_jpg/RlsGJDiaDGWAfxv2W9t2s7ziccdBdDy4fMiaxSIGCr4OR4oK4sSXSLHPPNibpac3FfgnzNdKMtqsiaxP5iaY0biazCv3Q/640?wx_fmt=jpeg "null")
    

通過.class 可以發現,

`$InnerClass` 持有外部類的引用。

`$StaticInnerClass` 不持有外部類的引用。

Java 當中的嵌套類,預設情況下,沒有 **static關鍵字** 時,它就是一個**內部類**,這樣的內部類是會**持有外部類的引用的**。 所以,這樣的設計在 Java 當中會非常容易出現**記憶體泄漏!** 而我們之所以會犯這樣的錯誤,往往只是因為忘記加`static`關鍵字。

Kotlin 則恰好**相反**,在預設情況下,**嵌套類變成了靜態內部類**,而這種情況下的嵌套類是**不會持有外部類引用的**。只有當我們真正需要訪問外部類成員的時候,我們才會加上 **inner 關鍵字**。這樣一來,預設情況下,開發者是不會犯錯的,只有手動加上 `inner` 關鍵字之後,才可能會出現記憶體泄漏,而當我們加上 inner 之後,其實往往也就能夠意識到記憶體泄漏的風險了。

* * *

數據類(Data Class )
----------------

Koltin 數據類 ,就是用於存放數據的類,等價於 **POJO** (Plain Ordinary Java Object)。要定義一個數據類,我們只需要在普通的類前面加上一個關鍵字 `data`,就可以把它變成一個"數據類"。

// 數據類當中,最少要有一個屬性
                   ↓
data class Person(val name: String, val age: Int)


編譯器會為數據類自動生成一些 POJO 常用的方法

*   • getter()
    
*   • setter()
    
*   • equals();
    
*   • hashCode();
    
*   • toString();
    
*   • componentN() 函數;
    
*   • copy()。
    

* * *

Koltin 數據類反編譯成 Java代碼:

![圖片](https://mmbiz.qpic.cn/mmbiz_jpg/RlsGJDiaDGWAfxv2W9t2s7ziccdBdDy4fMtzkvQxUHib0fx1gich2r8tmKYRaFFftcXEfnpURic2P4uAOR2RleQDNibw/640?wx_fmt=jpeg)

* * *

object 關鍵字
----------

`fun` 關鍵字代表了定義函數,`class` 關鍵字代表了定義類,這些都是固定的,`object` 關鍵字,卻有三種迥然不同的語義,分別可以定義:

*   • 匿名內部類;
    
*   • 單例模式;
    
*   • 伴生對象。
    

之所以會出現這樣的情況,是因為 Kotlin 的設計者認為:

這三種語義**本質**上都是在**定義一個類的同時還創建了對象**。

在這樣的情況下,與其分別定義三種不同的關鍵字,還不如將它們統一成 object 關鍵字。

* * *

### object:匿名內部類

在 Java 開發當中,我們經常需要寫類似這樣的代碼:

public interface Runnable {
      void run();
  }
  public static void main(String[] args) {
      // 創建Runnable對象並使用匿名內部類重寫run方法
      Runnable runnable = new Runnable() {
          public void run() {
              System.out.println("Runnable is running");
          }
      };
      // 創建Thread對象並將Runnable作為參數傳入
      Thread thread = new Thread(runnable);
      // 啟動線程
      thread.start();
  }


這是典型的匿名內部類寫法。

在 Kotlin 當中,我們會使用 `object` 關鍵字來創建匿名內部類。

interface Runnable {
        fun run()
    }
    
    @JvmStatic
    fun main(args: Array) {
        // 創建Runnable對象並使用匿名內部類重寫run方法
        val runnable: Runnable = object : Runnable {
            override fun run() {
                println("Runnable is running")
            }
        }
        // 創建Thread對象並將Runnable作為參數傳入
        val thread: Thread = Thread(runnable)
        // 啟動線程
        thread.start()
    }


* * *

### object:單例模式

在 Kotlin 當中,要實現單例模式其實非常簡單,我們直接用 object 修飾類即可:

object UserManager {
    fun login() {}
}


可以看出,Kotlin 生成單例,代碼量非常少

反編譯後的 Java 代碼:

public final class UserManager {

public static final UserManager INSTANCE;

static {
      UserManager var0 = new UserManager();
      INSTANCE = var0;
   }

private UserManager() {}

public final void login() {}
}


Kotlin 編譯器會將其**轉換成靜態代碼塊的單例模式**。

雖然具有簡潔的優點,但同時也存在兩個缺點。

*   • 不支持懶載入。
    
*   • 不支持傳參構造單例。
    

### object:伴生對象

Kotlin 當中**沒有** static 關鍵字,所以我們沒有辦法直接定義靜態方法和靜態變數。不過,Kotlin 還是為我們提供了伴生對象,來幫助實現靜態方法和變數。

Kotlin 伴生:

companion object {
        const val LEARNING_FRAGMENT_INDEX = 0
       
        fun jumpToMe(context: Context, index: Int) {
            context.startActivity(Intent(context, TrainingHomeActivity::class.java).apply {
                putExtra(FRAGMENT_INDEX, index)
            })
        }
    }


反編譯後的 Java 代碼:

private Companion() { }
   public static final Companion Companion = new Companion((DefaultConstructorMarker)null);
   
   public static final int LEARNING_FRAGMENT_INDEX = 0;
  
   public static final class Companion {
      public final void jumpToMe(@NotNull Context context, int index) {
      
      }
 }


可以看到jumpToMe()並不是靜態方法,它實際上是通過調用單例 Companion 的實例上的方法實現的。

* * *

擴展 (Extension)
--------------

Kotlin 的擴展(Extension),主要分為兩種語法:

第一個是**擴展函數**,

第二個是**擴展屬性**。

從語法上看,擴展**看起來**就像是我們從類的外部為它擴展了新的成員。

場景:假如我們想修改 JDK 當中的 String,想在它的基礎上增加一個方法“lastElement()”來獲取末尾元素,如果使用 Java,我們是無法通過常規手段實現的,因為我們沒辦法修改 JDK 的源代碼。**任何第三方提供的 SDK,我們都無權修改**。

不過,藉助 Kotlin 的擴展函數,我們就完全可以在**語義層面**,來為第三方 SDK 的類**擴展**新的成員方法和成員屬性。

### 擴展函數

擴展函數,就是從類的外部擴展出來的一個函數,這個函數看起來就像是類的成員函數一樣

Extension.kt
 /*
 ①    ②      ③            ④
 ↓     ↓       ↓            ↓   */     
fun String.lastElement(): Char? {
    //   ⑤
    //   ↓
    if (this.isEmpty()) {
        return null
    }

return this[length - 1]
}

// 使用擴展函數
fun main() {
    val msg = "Hello Wolrd"
    // lastElement就像String的成員方法一樣可以直接調用
    val last = msg.lastElement() // last = d
}


*   • 註釋①,fun關鍵字,代表我們要定義一個函數。也就是說,不管是定義普通 Kotlin 函數,還是定義擴展函數,我們都需要 fun 關鍵字。
    
*   • 註釋②,“String.”,代表我們的擴展函數是為 String 這個類定義的。在 Kotlin 當中,它有一個名字,叫做接收者(Receiver),也就是擴展函數的接收方。
    
*   • 註釋③,lastElement(),是我們定義的擴展函數的名稱。
    
*   • 註釋④,“Char?”,代表擴展函數的返回值是可能為空的 Char 類型。
    
*   • 註釋⑤,“this.”,代表“具體的 String 對象”,當我們調用 msg.lastElement() 的時候,this 就代表了 msg。
    

* * *

擴展函數反編譯成 Java 代碼:

public final class StringExtKt {
   @Nullable
   public static final Character lastElement(@NotNull String \(this\)lastElement) {
      // 省略
   }
}


而如果我們將上面的 StringExtKt 修改成 StringUtils,它就變成了典型的 Java 工具類

public final class StringUtils {

public static final Character lastElement(String $this) {
     // 省略
   }
}
public static final void main() {
  Character last = StringUtils.lastElement(msg);
}


所以 Kotlin 擴展函數 **本質** 上和 Java靜態方法 是一樣的。

只是編譯器幫我們做了很多事情, 讓代碼寫起來更簡潔。

* * *

### 擴展屬性

而擴展屬性,則是在類的外部為它定義一個新的成員屬性。

// 接收者類型
//     ↓
val String.lastElement: Char?
    get() = if (isEmpty()) {
            null
        } else {
            get(length - 1)
        }

fun main() {
    val msg = "Hello Wolrd"
    // lastElement就像String的成員屬性一樣可以直接調用
    val last = msg.lastElement // last = d
}


* * *

擴展函數/擴展屬性對比

![圖片](https://mmbiz.qpic.cn/mmbiz_jpg/RlsGJDiaDGWAfxv2W9t2s7ziccdBdDy4fMO8vFSWibtkiaMT52kE0YxaFrQbFe7YAsIjkXEhDsdS77LnapBvqjbEiaQ/640?wx_fmt=jpeg "null")

轉換成Java代碼後,擴展函數和擴展屬性代碼一致,

和 `StringUtils.lastElement(msg); }` 用法是一樣的。

擴展最主要的用途,就是用來取代 Java 當中的各種工具類,比如StringUtils、DateUtils 等等。

* * *

### 擴展函數在 Android 中的案例

**用擴展函數簡化Toast的用法:**

這是Toast的標準用法,在界面上彈出一段文字提示,代碼很長。

Toast.makeText(context, "This is Toast",Toast.LENGTH_SHORT).show()


還容易忘記調show()函數,造成Toast 沒有彈出。

**用擴展函數改寫後:**

fun String.showToast(context: Context) {   
    Toast.makeText(context, this, Toast.LENGTH_SHORT).show() 
}


調用時,只需要在要展示的內容後面調一下showToast(),這樣就簡潔了很多。

"This is Toast".showToast(context)


* * *

函數與 Lambda 表達式
==============

*   • 函數類型(Function Type)
    
*   • 函數引用 (Function reference)
    
*   • 高階函數(Higher-order function)
    
*   • 匿名函數 (Anonymous function)
    
*   • Lambda Expressions
    
*   • 函數式(SAM)介面
    
*   • SAM 轉換
    
*   • 高階函數應用
    

* * *

函數類型(Function Type)
-------------------

函數類型(Function Type)就是**函數的**

**類型**, 在 Kotlin 的世界里,函數是一等公民 既然變數可以有類型,函數也可以有類型。

//         (Int,  Int) ->Float 這就是 add 函數的類型
//           ↑     ↑      ↑
fun add(a: Int, b: Int): Float { return (a+b).toFloat() }


將第三行代碼里的“ **Int** **Int** **Float**”抽出來,就可以確定該函數的類型。

將函數的“參數類型”和“返回值類型”抽象出來後,加上`()`,`->` 符號加工後,就得到了“函數類型”。

`(Int, Int) ->Float` 就代表了參數類型是兩個 Int,返回值類型為 Float 的函數類型。

* * *

函數引用(Function reference)
------------------------

普通的變數有引用的概念,我們可以將一個變數賦值給另一個變數,這一點,在函數上也是同樣適用的,函數也有引用,並且也可以賦值給變數。

前面定義的 add 函數,賦值給另一個函數變數時,不能直接用的,

![圖片](https://mmbiz.qpic.cn/mmbiz_jpg/RlsGJDiaDGWAfxv2W9t2s7ziccdBdDy4fMiczZiahOtgCqvUVSiacLpaSejeoWy81wXt0YibLIYuR9tficgDbic6KMUyOA/640?wx_fmt=jpeg "null")

需要使用::操作符 , 後跟要引用的函數名,獲得函數引用後才可以去賦值。

fun add(a: Int, b: Int): Float { return (a+b).toFloat() }

//   變數     函數類型               函數引用        
//    ↑         ↑                     ↑
val function: (Int, Int) -> Float = ::add
 println(function(2, 3)) // 輸出 5


加了雙冒號:: , 這個函數才變成了一個**對象**,只有對象才能被賦值給變數。

* * *

* * *

fun add(a: Int, b: Int): Float { return (a+b).toFloat() } 
   
   fun testGaojie() {
     println( ::add )
     println( (::add)(2, 3) )// 輸出 5.0
    }


通過反編譯成 Java 代碼,可以看出。

`::add` 等價於 `Function2 var1 = new Function2(...)` ,

是一個FunctionN 類型的對象。

反編譯成 Java代碼:

public final void testGaojie() {
 //  println( ::add )
      Function2 var1 = new Function2((GaojieFunTest)this) {
         public Object invoke(Object var1, Object var2) {
            return this.invoke(((Number)var1).intValue(), ((Number)var2).intValue());
         }
         public final float invoke(int p1, int p2) {
            return ((GaojieFunTest)this.receiver).add(p1, p2);
         }
      };
      System.out.println(var1);
//  println( (::add)(2, 3) )
      float var2 = ((Number)((Function2)(new Function2((GaojieFunTest)this) {
         public Object invoke(Object var1, Object var2) {
            return this.invoke(((Number)var1).intValue(), ((Number)var2).intValue());
         }
         public final float invoke(int p1, int p2) {
            return ((GaojieFunTest)this.receiver).add(p1, p2);
         }
      })).invoke(2, 3)).floatValue();
      System.out.println(var2);
   }


* * *

* * *

fun add(a: Int, b: Int): Float { return (a+b).toFloat() } 
   
   fun testGaojie() {
     println(  add(2, 3)  )// 輸出 5.0
     val function: (Int, Int) -> Float = ::add
     println( function(2, 3) ) // 輸出 5.0
     println(  function.invoke(2, 3)  )  // 輸出 5.0
    }


將 testGaojie()轉換成 Java 代碼。可以看到在 Java 里, **函數類型**被聲明為**普通的介面**:一個函數類型的變數是FunctionN介面的一個實現。Kotlin標準庫定義了一系列的**介面**,這些介面對應於**不同參數數量**的**函數**:`Function0<R>`(沒有參數的函數)、`Function2<P1,P2,R>`(2個參數的函數)...`Function22<P1,P2 ... R>`。每個介面定義了一個`invoke()`方法,調用這個方法就會執行函數。一個**函數類型的變數**就是實現了對應的FunctionN介面的**實現類**的**實例**。實現類的`invoke()`方法包含了 **函數引用**對應的**函數**的**函數體**

反編譯成 Java代碼:

public final void testGaojie() {
 // println(  add(2, 3)  )
      float var1 = this.add(2, 3);
      System.out.println(var1);
//  val function: (Int, Int) -> Float = ::add     
      Function2 function = (Function2)(new Function2((GaojieFunTest)this) {
         // \(FF: synthetic method          // \)FF: bridge method
         public Object invoke(Object var1, Object var2) {
            return this.invoke(((Number)var1).intValue(), ((Number)var2).intValue());
         }

public final float invoke(int p1, int p2) {
            return ((GaojieFunTest)this.receiver).add(p1, p2);
         }
      });
// println( function(2, 3) ) // 輸出 5.0      
      float var2 = ((Number)function.invoke(2, 3)).floatValue();
      System.out.println(var2);
//  println(  function.invoke(2, 3)  )  // 輸出 5.0     
      var2 = ((Number)function.invoke(2, 3)).floatValue();
      System.out.println(var2);
   }


* * *

高階函數 (Higher-order function)
----------------------------

高階函數的定義:高階函數是將函數用作**參數**或者**返回值**的函數。

如果一個函數的**參數類型**是**函數類型**或者**返回值類型**是**函數類型**,那麼這個函數就是就是高階函數 。

或者說,如果一個函數的**參數**或者**返回值**,其中有一個是**函數**,那麼這個函數就是高階函數。

//                            函數類型的變數   函數類型
    //                                 ↓            ↓
    fun  higherOrderAdd( a:Int,b: Int,block: (Int, Int) -> Float):Float{
//                   函數類型的變數
//                       ↓
        var  result = block.invoke(a,b) 
//                   函數類型的變數
//                       ↓
        var  result2 = block(a,b)
        println("result:$result")
        return result
    }


higherOrderAdd 有一個參數是函數類型,所以它是高階函數

* * *

匿名函數
----

匿名函數看起來跟普通函數很相似,除了它的**名字**和**參數類型**被省略了外。 匿名函數示例如下:

fun(a :Int, b :Int) = a + b


上面的匿名函數是沒法直接調用的,賦值給變數後才可以調用

val anonymousFunction = fun(a :Int, b :Int) = a + b

  fun anonymousFunctionTest() {
        higherOrderAdd(2,2,::add) // 函數引用
        higherOrderAdd(2,2,anonymousFunction) // 函數變數
        higherOrderAdd(2,2,
            fun (a:Int,b:Int):Float{ return (a+b).toFloat()}) // 匿名函數
    }
```

匿名函數**本質**上也是函數類型的對象,所以可以賦值給變數。

* * *

* * *

匿名函數不能單獨聲明在 ()外面,因為匿名函數是(**函數的聲明**與**函數引用**合二為一)

![圖片](https://mmbiz.qpic.cn/mmbiz_jpg/RlsGJDiaDGWAfxv2W9t2s7ziccdBdDy4fMJFmXtFY4LwpRdEgznOQkDAcfYNRN5yLptBE2QKvq2h4vgFm8yCgU6A/640?wx_fmt=jpeg "null")

// 具名函數不能直接賦值給變數,因為它不是對象

![圖片](https://mmbiz.qpic.cn/mmbiz_jpg/RlsGJDiaDGWAfxv2W9t2s7ziccdBdDy4fMBf0Rnztc6eMogvT1U4ycxpf8k2ZmSJMdaPoXDP0PEF2k5lxbfSj4zg/640?wx_fmt=jpeg "null")

// 函數()內不能直接 聲明 具名函數,因為它不是對象

![圖片](https://mmbiz.qpic.cn/mmbiz_jpg/RlsGJDiaDGWAfxv2W9t2s7ziccdBdDy4fMiaedgf1MJJiaNCyAEnXH9sfNpOOmeVQqQQW2Z8y1SVhKxJxOcdXz74PA/640?wx_fmt=jpeg "null")

這幾個個報錯是因為,匿名函數是把**函數的聲明**與**函數引用**合二為一了,所以在需要匿名函數的地方,聲明一個具名函數是報錯的,正確的做法是改用**具名函數引用** 例如:

```
  higherOrderAdd(2,2,::add) // 函數引用
```

* * *

* * *

Lambda
------

Java 在 Java8中引入的Lambda。

Java Lambda 的基本語法是

```
(parameters) -> expression 
```

或(請註意語句的花括弧)

```
  (parameters) -> { statements; }
```

Kotlin 語言的是可以用 Lambda 表達式作為函數參數的,Lambda就是**一小段**可以作為**參數**傳遞的**代碼**,那麼到底多少代碼才算一小段代碼呢?Kotlin對此並沒有進行限制,但是通常不建議在Lambda 表達式中編寫太長的代碼,否則可能會影響代碼的**可讀性**。

Lambda也可以理解為是**匿名函數**的**簡寫**。

我們來看一下Lambda表達式的語法結構:

```
{參數名1: 參數類型, 參數名2: 參數類型 -> 函數體}
```

首先最外層是一對花括弧{ },如果有參數傳入到Lambda表達式中的話,我們還需要聲明**參數列表**,參數列表的結尾使用一個 '->' 符號 ,表示參數列表的結束以及函數體的開始,函數體中可以編寫任意行代碼,並且**最後一行代碼**會自動作為Lambda表達式的**返回值**。

* * *

```
    fun  higherOrderAdd( a:Int,b: Int,block: (Int, Int) -> Float):Float{
        var  result = block(a,b)
        println("result:$result")
        return result
    }
      @Test
    fun anonymousFunctionTest() {
        higherOrderAdd(2,2,::add) // 函數引用
        higherOrderAdd(3,3,
            fun (a:Int,b:Int):Float{ return (a+b).toFloat()}) // 匿名函數
        higherOrderAdd(4,4,
             { a:Int,b:Int ->  (a+b).toFloat()}) //    Lambda表達式
        println(
            fun (a:Int,b:Int):Float{ return (a+b).toFloat()}(5,5) ) // 匿名函數直接調用
        println(
            { a:Int,b:Int ->  (a+b).toFloat()}(5,5)) // Lambda表達式調用
    }   

```

相比匿名函數,lambda 表達式定義與引用函數更 **簡潔** 。

* * *

函數式(SAM)介面
----------

SAM 是 Single Abstract Method 的縮寫,只有一個抽象方法的介面稱為**函數式介面**或 **SAM(單一抽象方法)介面**。函數式介面可以有多個非抽象成員,但**只能有一個抽象成員**。

在Java 中可以用註解@FunctionalInterface 聲明一個函數式介面:

```
@FunctionalInterface
public interface Runnable {
    void run();
}
```

在 Kotlin 中可以用 fun 修飾符在 Kotlin 中聲明一個函數式介面:

```
// 註意 interface 前的 fun
fun interface KRunnable {
   fun invoke()
}
```

* * *

* * *

SAM 轉換
------

對於函數式介面,可以通過 lambda 表達式實現 SAM 轉換,從而使代碼更簡潔、更有可讀性。

使用 lambda 表達式可以替代手動創建 實現函數式介面的類。 通過 SAM 轉換, Kotlin 可以將 簽名與介面的單個抽象方法的**簽名匹配**的任何 **lambda 表達式**,轉換成實現該介面的類的**實例**。

```
// 註意需用fun 關鍵字聲明
fun  interface  Action{
    fun run(str:String)
}
fun  runAction(action: Action){
     action.run("this  run")
}

fun main() {
//      創建一個 實現函數式介面 的類 的實例(匿名內部類)
    val action = object :Action{
        override fun run(str: String) {
            println(str)
        }
    }
    //   傳入實例,不使用 SAM 轉換
    runAction(action)
//    利用 Kotlin 的 SAM 轉換,可以改為以下等效代碼:
//    使用 Lambda表達式替代手動創建 實現函數式介面的類
    runAction({
            str-> println(str)
    })
}


* * *

* * *

fun  interface  InterfaceApi{
    fun run(str:String)
}
fun  runInterface(interfaceApi: InterfaceApi){
    interfaceApi.run("this  run")
}
//  函數類型替代介面定義
fun  factionTypeReplaceInterface(block:(String)->Unit){
     block("this block run")
}
//===Test
// 普通函數,參數是函數式介面對象,傳 函數類型對象 也是可以的
fun  testFactionTypeReplaceInterface(){
    val function:(String)->Unit = { println(it) }
    runInterface(function) //普通函數,參數是函數式介面對象,傳 函數類型對象 也是可以的
    factionTypeReplaceInterface(function)
}
// 高階函數, 參數是函數類型對象,傳 是函數式介面對象 是不可以的。
fun  testInterface(){
    val interfaceApi:InterfaceApi = object :InterfaceApi{
        override fun run(str: String) {
            println(str)
        }
    }
    runInterface(interfaceApi)
    factionTypeReplaceInterface(interfaceApi)// 高階函數, 參數是函數類型對象,傳 是函數式介面對象 是不可以的。
}


![圖片](https://mmbiz.qpic.cn/mmbiz_jpg/RlsGJDiaDGWAfxv2W9t2s7ziccdBdDy4fMyadFZ6LWUKiaCQjnPlIIeOFvd6BvFVa59M4GcOXqoebypmYJP9Uwgzg/640?wx_fmt=jpeg "null")

普通函數,參數是函數式介面對象,傳 函數類型對象 也是可以的

反過來不可以:

高階函數, 參數是函數類型對象,傳 是函數式介面對象 是不可以的。

前面說的都是函數傳不同的參數類型。

![圖片](https://mmbiz.qpic.cn/mmbiz_jpg/RlsGJDiaDGWAfxv2W9t2s7ziccdBdDy4fMCnQxksiaeMqWq5tc9ZzbxC4IUFbY2oLMEfxLe8Pf5S2RabTbCwcXEgw/640?wx_fmt=jpeg)

這張圖中的三處報錯都是,**類型不匹配**。

**說明:**

作為函數實參時, 函數類型對象 單向代替 函數式介面對象。

但是在創建對象時, 函數類型、函數式介面兩種類型是涇渭分明的。

高階函數應用
------

在Android開發時,我們經常會遇到給自定義View綁定點擊事件的場景。以往通常的做法如下:

// CustomView.java

// 成員變數
private OnContextClickListener mOnContextClickListener;

// 監聽手指點擊內容事件
public void setOnContextClickListener(OnContextClickListener l) {
    mOnContextClickListener = l;
}

// 為傳遞這個點擊事件,專門定義了一個介面
public interface OnContextClickListener {
    void onContextClick(View v);
}


### \>

// 設置手指點擊事件
customView.setOnContextClickListener(new View.OnContextClickListener() {
    @Override
    public void onContextClick(View v) {
        gotoPreview();
    }
});


看完了這兩段代碼之後,你有沒有覺得這樣的代碼會很啰嗦?因為,真正邏輯只有一行代碼:gotoPreview(),而實際上我們卻寫了 6 行代碼。

* * *

### 用 Kotlin 高階函數 改寫後

//View.kt
//                     (View) -> Unit 就是「函數類型 」
//                       ↑        ↑ 
var mOnContextClickListener: ((View) -> Unit)? = null

// 高階函數
fun setOnContextClickListener(l: (View) -> Unit) {
    mOnClickListener = l;
}


如果我們將前面Java寫的例子的核心邏輯提取出來,會發現這樣才是最簡單明瞭的:

//                      { gotoPreview() } 就是 Lambda
//                             ↑
customView.setOnContextClickListener({ gotoPreview() })


Kotlin 語言的設計者是怎麼做的呢?實際上他們是分成了兩個部分:

*   • 用函數類型替代介面定義;
    
*   • 用 Lambda 表達式作為函數參數。
    

* * *

Kotlin 中引入高階函數會帶來幾個好處:一個是針對**定義方**,代碼中**減少**了介面類的定義;另一個是對於**調用方**來說,代碼也會更加**簡潔**。這樣一來,就大大減少了代碼量,提高了代碼可讀性,並通過減少類的數量,提高了代碼的性能。

|   
 | 不使用高階函數 | 使用高階函數 |
| --- | --- | --- |
| 定義方 | 需要額外定義介面 | 不需要額外定義介面 |
| 調用方 | 代碼繁瑣 | 代碼簡潔清晰 |
| 性能 | 差 | 藉助inline的情況,性能更高 |

* * *

最後總結
====

思考討論
----

本文主要分享了 空安全、擴展函數、高階函數、Lambda,

本文分享的Kotlin內容,您認為哪些特性是最有趣或最有用的?

* * *

參考文檔:
-----

*   • Kotlin 語言中文站
    
*   • 《Kotlin實戰》
    
*   • 《Kotlin核心編程》
    
*   • 《Kotlin編程權威指南》
    
*   • 《Java 8實戰》

作者:

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 類似魔獸世界,moba這種技能極其複雜,靈活性要求極高的技能系統,必須需要一套及其靈活的數值結構來搭配。數值結構設計好了,實現技能系統就會非常簡單,否則就是一場災難。比如魔獸世界,一個人物的數值屬性非常之多,移動速度,力量,怒氣,能量,集中值,魔法值,血量,最大血量,物理攻擊,物理防禦,法術攻擊,法 ...
  • Actor Location Actor模型只需要知道對方的InstanceId就能發送消息,十分方便,但是有時候我們可能無法知道對方的InstanceId,或者是一個Actor的InstanceId會發生變化。這種場景很常見,比如:很多游戲是分線的,一個玩家可能從1線換到2線,還有的游戲是分場景的 ...
  • Actor模型 Actor介紹 在討論Actor模型之前先要討論下ET的架構,游戲伺服器為了利用多核一般有兩種架構,單線程多進程跟單進程多線程架構。兩種架構本質上其實區別不大,因為游戲邏輯開發都需要用單線程,即使是單進程多線程架構,也要用一定的方法保證單線程開發邏輯。ET採用的是單線程多進程的架構, ...
  • 事件機制EventSystem ECS最重要的特性一是數據跟邏輯分離,二是數據驅動邏輯。什麼是數據驅動邏輯呢?不太好理解,我們舉個例子 一個moba游戲,英雄都有血條,血條會在人物頭上顯示,也會在左上方頭像UI上顯示。這時候服務端發來一個扣血消息。我們怎麼處理這個消息?第一種方法,在消息處理函數中修 ...
  • 一切皆實體 目前十分流行ECS設計,主要是守望先鋒的成功,引爆了這種技術。守望先鋒採用了狀態幀這種網路技術,客戶端會進行預測,預測不准需要進行回滾,由於組件式的設計,回滾可以只回滾某些組件即可。ECS最重要的設計是邏輯跟數據的完全分離。即EC是純數據,System實際上就是邏輯,由數據驅動邏輯。數據 ...
  • 目錄 一、DNS概念 二、功能變數名稱格式類型 三、查詢類型 四、解析類型 五、配置DNS 六、dns解析實驗 1.配置正向解析 2.反向解析 3.主從解析 一、DNS概念 概念:功能變數名稱和IP地址的相互映射的分散式資料庫,可以更好的訪問互聯網。 電腦只能訪問IP地址,但是IP地址不是方便記住,採用功能變數名稱解析出 ...
  • 一、環境準備 1、虛擬機:ubuntu18.04 64位 2、交叉編譯工具包:gcc-linaro-7.5.0-2019.12-i686_arm-linux-gnueabihf.tar 下載鏈接:https://releases.linaro.org/components/toolchain/bin ...
  • > 本文首發於公眾號:Hunter後端 > 原文鏈接:[es筆記三之term,match,match_phrase 等查詢方法介紹](https://mp.weixin.qq.com/s/3tzD8dEr592WNJFH_1bKRw) 首先介紹一下在 es 里有兩種存儲字元串的欄位類型,一個是 ke ...
一周排行
    -Advertisement-
    Play Games
  • Timer是什麼 Timer 是一種用於創建定期粒度行為的機制。 與標準的 .NET System.Threading.Timer 類相似,Orleans 的 Timer 允許在一段時間後執行特定的操作,或者在特定的時間間隔內重覆執行操作。 它在分散式系統中具有重要作用,特別是在處理需要周期性執行的 ...
  • 前言 相信很多做WPF開發的小伙伴都遇到過表格類的需求,雖然現有的Grid控制項也能實現,但是使用起來的體驗感並不好,比如要實現一個Excel中的表格效果,估計你能想到的第一個方法就是套Border控制項,用這種方法你需要控制每個Border的邊框,並且在一堆Bordr中找到Grid.Row,Grid. ...
  • .NET C#程式啟動閃退,目錄導致的問題 這是第2次踩這個坑了,很小的編程細節,容易忽略,所以寫個博客,分享給大家。 1.第一次坑:是windows 系統把程式運行成服務,找不到配置文件,原因是以服務運行它的工作目錄是在C:\Windows\System32 2.本次坑:WPF桌面程式通過註冊表設 ...
  • 在分散式系統中,數據的持久化是至關重要的一環。 Orleans 7 引入了強大的持久化功能,使得在分散式環境下管理數據變得更加輕鬆和可靠。 本文將介紹什麼是 Orleans 7 的持久化,如何設置它以及相應的代碼示例。 什麼是 Orleans 7 的持久化? Orleans 7 的持久化是指將 Or ...
  • 前言 .NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功 ...
  • 在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。 自定義 Pa ...
  • 在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式。 ...
  • 一、前言 這是一篇搭建許可權管理系統的系列文章。 隨著網路的發展,信息安全對應任何企業來說都越發的重要,而本系列文章將和大家一起一步一步搭建一個全新的許可權管理系統。 說明:由於搭建一個全新的項目過於繁瑣,所有作者將挑選核心代碼和核心思路進行分享。 二、技術選擇 三、開始設計 1、自主搭建vue前端和. ...
  • Csharper中的表達式樹 這節課來瞭解一下表示式樹是什麼? 在C#中,表達式樹是一種數據結構,它可以表示一些代碼塊,如Lambda表達式或查詢表達式。表達式樹使你能夠查看和操作數據,就像你可以查看和操作代碼一樣。它們通常用於創建動態查詢和解析表達式。 一、認識表達式樹 為什麼要這樣說?它和委托有 ...
  • 在使用Django等框架來操作MySQL時,實際上底層還是通過Python來操作的,首先需要安裝一個驅動程式,在Python3中,驅動程式有多種選擇,比如有pymysql以及mysqlclient等。使用pip命令安裝mysqlclient失敗應如何解決? 安裝的python版本說明 機器同時安裝了 ...