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