主頁 > 移動端開發 > From Java To Kotlin:空安全、擴展、函式、Lambda很詳細,這次終于懂了

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

2023-05-28 08:42:44 移動端開發

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

  • 簡約:使用一行代碼創建一個包含 getterssettersequals()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 代碼:


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

通過 javac 命令 編譯成 class 檔案后:

  • InnerClass
  • StaticInnerClass

通過.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代碼:


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<String>) {
        // 創建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
}

擴展函式/擴展屬性對比

轉換成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 函式,賦值給另一個函式變數時,不能直接用的,

需要使用::運算子 , 后跟要參考的函式名,獲得函式參考后才可以去賦值,

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);
   }

總結

Kotlin中,函式參考和函式呼叫有以下區別:

  1. 函式參考可以視為函式型別的變數,它持有函式的參考,而函式呼叫則執行函式本身,因此,可以將函式參考傳遞給其他函式,并在需要時執行,
  2. 函式參考可以簡化呼叫代碼,避免冗長的代碼,而函式呼叫則需要撰寫完整的函式名稱、引數和引數型別,
  3. 函式參考不會立即執行函式代碼,只有在需要時才執行,而函式呼叫則立即執行函式代碼,
    例如,假設我們有一個名為“double”的函式,它接受一個整數并回傳它的兩倍,那么,函式參考和函式呼叫的代碼如下所示:
val doubleFunc: (Int) -> Int = ::double
 // 函式呼叫
val result = double(5) // 回傳 10

在這個例子中,我們定義了一個函式參考,它可以在需要時傳遞給其他函式,也可以在需要時執行,

第 2 行代碼我們還呼叫了函式“double”,它立即執行代碼并回傳結果,


高階函式 (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()}) // 匿名函式
    }

匿名函式本質上也是函式型別的物件,所以可以賦值給變數,


匿名函式不能單獨宣告在 ()外面,因為匿名函式是(函式的宣告函式參考合二為一)

// 具名函式不能直接賦值給變數,因為它不是物件

// 函式()內不能直接 宣告 具名函式,因為它不是物件

這幾個個報錯是因為,匿名函式是把函式的宣告函式參考合二為一了,所以在需要匿名函式的地方,宣告一個具名函式是報錯的,正確的做法是改用具名函式參考 例如:

  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)// 高階函式, 引數是函式型別物件,傳 是函式式介面物件 是不可以的,
}

普通函式,引數是函式式介面物件,傳 函式型別物件 也是可以的

反過來不可以:

高階函式, 引數是函式型別物件,傳 是函式式介面物件 是不可以的,

前面說的都是函式傳不同的引數型別,

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

說明:

作為函式實參時, 函式型別物件 單向代替 函式式介面物件,

但是在創建物件時, 函式型別、函式式介面兩種型別是涇渭分明的,

高階函式應用

在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實戰》

作者:Seachal
出處:http://www.cnblogs.com/ZhangSeachal
如果,您認為閱讀這篇博客讓您有些識訓,不妨點擊一下左下角的【好文要頂】與【收藏該文】
如果,您希望更容易地發現我的新博客,不妨點擊一下左下角的【關注我】
如果,您對我的博客內容感興趣,請繼續關注我的后續博客,我是【Seachal】

我的GitHub       我的CSDN 我的簡書

本博文為學習、筆記之用,以筆記記錄作者學習的知識與學習后的思考或感悟,學習程序可能參考各種資料,如覺文中表述過分參考,請務必告知,以便迅速處理,如有錯漏,不吝賜教!

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/553615.html

標籤:Android

上一篇:Flutter熱更新技術探索

下一篇:返回列表

標籤雲
其他(159854) Python(38173) JavaScript(25460) Java(18138) C(15231) 區塊鏈(8268) C#(7972) AI(7469) 爪哇(7425) MySQL(7214) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5873) 数组(5741) R(5409) Linux(5343) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4577) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2434) ASP.NET(2403) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) .NET技术(1977) 功能(1967) Web開發(1951) HtmlCss(1948) C++(1922) python-3.x(1918) 弹簧靴(1913) xml(1889) PostgreSQL(1878) .NETCore(1862) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • From Java To Kotlin:空安全、擴展、函式、Lambda很詳細,這次終于

    Kotlin 是一種靜態型別的編程語言,由 JetBrains 開發。它可以編譯成 Java 位元組碼,也可以編譯成 JavaScript 代碼。Kotlin 具有現代化的語法和功能,可以與 Java 互操作,并且可以在 Android 開發中使用。
    Kotlin 的語法簡潔、易讀、易寫,具有許多現代... ......

    uj5u.com 2023-05-28 08:42:44 more
  • Flutter熱更新技術探索

    APP發布到市場后,難免會遇到嚴重的BUG阻礙用戶使用,因此有在不發布新版本APP的情況下使用熱更新技術立即修復BUG需求。原生APP(例如:Android & IOS)的熱更新需求已經比較成熟,但Flutter技術堆疊目前還缺少類似的技術方案,因此Flutter研發團隊,也需要類似的熱更新技術。 ......

    uj5u.com 2023-05-26 15:05:50 more
  • 無人直播插件 直播替換安裝使用教程

    無人直播插件 直播替換安裝使用教程 一、適用機型及系統1、機型:所有iphone機型,可越獄即可2、系統:支持13系統及以上所有可越獄系統二、安裝手機端1、越獄(1)Windows越獄方法:電腦端安裝愛思助手,點擊工具 》一鍵越獄 》 選擇uncover 》開始越獄,如提示需要登錄,點擊商城,注冊并 ......

    uj5u.com 2023-05-25 10:54:39 more
  • Flutter三棵樹系列之詳解各種Key

    key是widget、element和semanticsNode的唯一標識,同一個parent下的所有element的key不能重復,但是在特定條件下可以在不同parent下使用相同的key,比如page1和page2都可以使用ValueKey ......

    uj5u.com 2023-05-25 10:54:03 more
  • 無人直播插件 直播替換安裝使用教程

    無人直播插件 直播替換安裝使用教程 一、適用機型及系統1、機型:所有iphone機型,可越獄即可2、系統:支持13系統及以上所有可越獄系統二、安裝手機端1、越獄(1)Windows越獄方法:電腦端安裝愛思助手,點擊工具 》一鍵越獄 》 選擇uncover 》開始越獄,如提示需要登錄,點擊商城,注冊并 ......

    uj5u.com 2023-05-25 10:52:59 more
  • 《蘇丹的復仇》攜手華為HMS生態,實作用戶、收入雙增長

    中國出海中東和北非地區的策略類手游《蘇丹的復仇》(Revenge of Sultans,ROS)和華為HMS生態深度合作,為本地用戶帶來創新游戲體驗,成為當地廣受歡迎的游戲之一,下載量居應用市場前列。2023年5月10日,在阿聯酋迪拜舉辦的HUAWEI P60系列及旗艦產品發布會中,ONEMT中東G ......

    uj5u.com 2023-05-24 09:35:30 more
  • 《蘇丹的復仇》攜手華為HMS生態,實作用戶、收入雙增長

    中國出海中東和北非地區的策略類手游《蘇丹的復仇》(Revenge of Sultans,ROS)和華為HMS生態深度合作,為本地用戶帶來創新游戲體驗,成為當地廣受歡迎的游戲之一,下載量居應用市場前列。2023年5月10日,在阿聯酋迪拜舉辦的HUAWEI P60系列及旗艦產品發布會中,ONEMT中東G ......

    uj5u.com 2023-05-24 09:35:09 more
  • 京東購物車如何提升30%性能

    本文主要介紹在業務復雜化背景下,京東零售購物車團隊努力踐行工匠精神,通過全異步化改造提升系統性能、提升用戶體驗。通過本文,讀者可以了解購物車中臺進行全異步化改造的總體方案,以及方案落地程序中遇到的問題及解決方法,讀者可重點關注文中提到的多分頁并行后,分頁精細控制及底層RPC例外資訊問題。 ......

    uj5u.com 2023-05-23 13:12:04 more
  • 關于微信小程式原生組件與uniApp混合開發程序遇到的問題與解決方

    前言: 在實際開發程序中,尤其是小程式的開發,我們常常會遇到一些在檔案中解決不了的問題,在這里,我就淺談一下我遇到的一些問題 1.小程式的構建框架是uni-app,卻突然被要求用原生的微信小程式代碼來開發,到最后要整合到uni-app里面 這個整合問題,uni-app官網就有解決方式,這里就不多說, ......

    uj5u.com 2023-05-23 10:18:21 more
  • 從熱愛到深耕,在開發路上的他們勇敢逐夢

    2022年的程式員節, #大齡程式員去哪兒了#成為了社交媒體上最火的話題之一,程式員的職場成長問題在社會上引起了廣泛關注。 有2位在技術領域摸爬滾打很多年的開發者,35歲后的他們,有70后,有80后,依然在編程開發,依然有離職創業的勇氣,努力實作自己的人生價值。走進他們的故事,你會發現,這個世上沒有 ......

    uj5u.com 2023-05-23 10:18:13 more