Kotlin系列之函數的定義與調用

NO IMAGE

創建了一個 Kotlin 學習交流群有興趣的同學可以加群一起交流學習

Kotlin系列之函數的定義與調用

本章內容包括

  • 用於處理集合,字符串和正則表達式的函數
  • 使用命名參數,默認參數,以及中綴調用語法
  • 通過擴展函數和屬性來適配Java庫
  • 使用頂層函數,佈局函數和屬性架構代碼

Kotlin系列之函數的定義與調用

在 Kotlin 中創建集合

Kotlin 沒有自己的集合類庫而是完全使用標準的 Java 集合類庫。

val hashSet = hashSetOf(1, 2, 3, 4, 5)
println(hashSet.javaClass) // class java.util.HashSet
val linkedHashSet = linkedSetOf(1, 2, 3)
println(linkedHashSet.javaClass) // class java.util.LinkedHashSet
val arrayList = arrayListOf(1, 2, 3, 4, 5)
println(arrayList.javaClass) // class java.util.ArrayList
val list = listOf(1, 2, 3, 4, 5)
println(list.javaClass) // class java.util.Arrays$ArrayList
val hashMap = hashMapOf(1 to "a", 2 to "b", 3 to "c")
println(hashMap.javaClass) // class java.util.HashMap

通過上面這些函數就可以創建集合,通過自己 new 集合對象的方式也是可以的 , Kotlin 中在創建對象時省略了 new 關鍵字。雖然 Kotlin 採用的是 Java 集合類庫,但是 Kotlin 提供了一些額外的擴展。

val list = listOf("小明", "丹尼", "李華")
println("獲取第一個元素 : ${list.first()}") // 獲取第一個元素 : 小明
println("獲取最後一個元素: ${list.last()}") // 獲取最後一個元素: 李華
println("獲取指定下標的元素: ${list[1]}") // 獲取指定下標的元素: 丹尼
println("獲取當中最大的一個元素: ${list.max()}") // 獲取當中最大的一個元素: 李華
println("翻轉這個集合 :${list.asReversed()}") // 翻轉這個集合 :[李華, 丹尼, 小明]
println("根據條件在集合中查找滿足條件的元素 : ${list.find { it.startsWith("小") }}") 
// 根據條件在集合中查找滿足條件的元素 : 小明

在後面的部分會仔細探究他們的工作原來,以及這些在 Java 類中新增加的函數是從何而來。


讓函數更好調用

這一節我們從一個例子開始,需求是得到一個集合的字符串展示形式,可以指定元素之間的分隔符號,前綴和後綴。先寫一個最基本的函數。

fun <T> joinToString(collection: Collection<T> , separator: String ,
prefix: String , postfix: String): String {
val result = StringBuilder(prefix)
for ((index , element) in collection.withIndex()) {
if (index > 0) {
result.append(separator)
}
result.append(element)
}
result.append(postfix)
return result.toString()
}
val list = listOf("小明", "丹尼", "李華")
println(joinToString(list , "|" , "<" , ">"))
// <小明|丹尼|李華>

對 joinToString 函數的測試結果得到了我們的預期。接下來我們會用 Kotlin 支持的特性來改寫這個函數,力求讓它變得更簡潔和實用。


命名參數

命名參數 是 Kotlin 的特性之一 ,可以解決可讀性的問題 , 因為當你在調用這樣一個 API : joinToString(Collection , “” , “” , “”) 的時候。你很可能會搞不清楚每個位置的String類型的參數究竟意味著什麼,只要參數的順序傳錯了你就會得到一些奇怪的結果。為了避免這個問題你需要去看一下它的函數聲明,來確定每個位置上的需要的是什麼參數。

在 Kotlin 中可以通過命名參數來解決這個問題, 就是在調用一個函數傳入參數的時候,可以顯示的寫上參數的名稱,並且指定要傳入的值賦值給那個參數。但是如果在調用一個函數時,指明瞭一個參數的名稱時,為了避免混淆,這個參數之後的所有參數都需要標明名稱了。 例如我對 prefix 參數標明瞭名稱,那麼必須在對之後的 postfix 和 separator 參數都標明名稱。

這個特性是沒法在調用 Java 函數時使用的。因為把參數名稱保存到.class 文件中是 Java 8及其更高版本的一個可選功能,Kotlin 需要保持對 Java 6 的兼容性。所以編譯器不能識別出調用函數的參數名稱。

val list = listOf("小明", "丹尼", "李華")
println(joinToString(list , prefix = "<" , separator = "|" , postfix = ">"))
// <小明|丹尼|李華>

默認參數值

Java 的另一個普遍存在的問題是一些類的重載函數太多。這些重載,原本是為了向後兼容,方便這些API的使用者,又或者是出於別的原因,但導致的最終結果是一樣的:重複。

在 Kotlin 中可以在聲明函數的時候指定參數的默認值,這樣可以避免創建重載函數。使用默認參數值對 joinToString 函數進行改寫。

fun <T> joinToString(collection: Collection<T> , separator: String = ", " ,
prefix: String = "[" , postfix: String = "]"): String {
val result = StringBuilder(prefix)
for ((index , element) in collection.withIndex()) {
if (index > 0) {
result.append(separator)
}
result.append(element)
}
result.append(postfix)
return result.toString()
}
val list = listOf("小明", "丹尼", "李華")
println(joinToString(list)) // [小明, 丹尼, 李華]

在對 joinToString 函數進行調用的時候我們只傳入了一個 list 參數值。其他參數都使用了我們在聲明函數時所指定的默認值。
注意!參數的默認值是被編碼到被調用的函數中,而不是調用的地方。如果你改變了參數的默認值並重新編譯這個函數,沒有給參數重新賦值的調用者,將會開始使用新的默認值

Java中是沒有默認值概念的,所以當從 Java 代碼中調用 Kotlin 函數的時候,調用者必須顯示的指定所有參數的值。同時 Kotlin 也給出了符合 Java 習慣的解決方法 ,在函數上加上 @JvmOverloads 註解,編譯器就會生成 Java 的重載函數,從最後一個開始省略每個參數,被省略的參數使用的是函數聲明時指定的默認值。

Kotlin系列之函數的定義與調用

@JvmOverloads
fun <T> joinToString(collection: Collection<T> , separator: String = ", " ,
prefix: String = "[" , postfix: String = "]"): String {
val result = StringBuilder(prefix)
for ((index , element) in collection.withIndex()) {
if (index > 0) {
result.append(separator)
}
result.append(element)
}
result.append(postfix)
return result.toString()
}
List<String> list = new ArrayList<>();
list.add("小明");
list.add("丹尼");
list.add("李華");
System.out.println(new KTDemo().joinToString(list)); // [小明, 丹尼, 李華]

消除靜態工具類:頂層函數和屬性

我相信絕大多數 Java 開發者都會在自己的,公司的,開源框架項目,或者是 JDK 中看到不少名稱為 XXXUtils 或者 XXXs 的類。這些類存在的意義就是工作在一些不需要對象的地方。這樣的類僅僅作為一堆靜態函數的容器存在。看吧事實就是這樣,並不是所有人都需要對象(object) (注意這裡的對象指的是編程世界中的對象,而不是中文口語的那個對象,事實上在現實世界中人人都需要對象,不然人該有多孤單啊)

在 Kotlin 中根本酒不需要去創建這些無意義的類。相反,可以把這些函數直接放在代碼文件的頂層 ,不用從屬於任何類。這些放在文件頂層的函數任然是包內的成員,如果你需要從包外訪問它,則需要 import 。

Kotlin系列之函數的定義與調用

這裡我們寫了一個 join.kt 文件,直接將 joinToString 函數放在了文件內。在 Java 代碼中調用這個函數 。

Kotlin系列之函數的定義與調用

仔細觀察可以發現 import static kt.demo.JoinKt.joinToString 這行代碼,這說明了 join.kt 文件被編譯成了一個類名為 JoinKt , joinToString 是其中的一個靜態函數。當然這裡你也可以這樣寫。

import kt.demo.JoinKt 
public class JavaClassDemo {
@Test
public void test1() {
List<String> list = new ArrayList<>();
list.add("小明");
list.add("丹尼");
list.add("李華");
System.out.println(JoinKt.joinToString(list));
}
}

修改文件類名

想要改變包含 Kotlin 頂層函數的編譯生成的類名稱,需要給這個 Kotlin 文件添加 @JvmName 的註解,將其放到這個文件的開頭,為於包名的前面:

Kotlin系列之函數的定義與調用

使用時就可以使用 JoinFunctions 這個名稱。


頂層屬性

和函數一樣屬性也可以被放到文件頂層。放在頂層的屬性會被編譯成一個靜態字段。默認情況下頂層屬性和其他任意屬性是一樣的,是通過訪問器暴漏給使用者。為了方便使用,如果你想要把一個常量以 public static final 的屬性暴漏給 Java 可以使用 const 來修飾它。

const val UNIX_LINE_SEPARATOR = "\n"
public static final String UNIX_LINE_SEPARATOR = "\n";
// 這兩行代碼等同

給別人添加方法:擴展函數和屬性

理論上來說擴展函數非常簡單,就是一個類的成員函數,不過這個成員函數定義在了類的外面。如下圖我們就為 String 定義了一個擴展函數用來獲取字符串的最後一個字符。

fun String.lastChar(): Char = this.last()
  • 擴展函數中接收者類型是由擴展函數定義的,所謂的接收者就是要被擴展的那個類,在這個例子中是 String
  • 接收者對象是該類型的一個實例,在這個例子中接收者對象是一個 String 類型的實例,也就是這個例子中的 this

可以像調用類的普通成員去調用這個函數:

println("Kotlin".lastChar()) // n

在上面這個例子中 ,String 就是接收者類型 。 “Kotlin” 字符串就是接收者對象。現在我們不需要修改 String 類的源碼就為它增加了新的行為。不管 String 類是用 Java 、Kotlin,或者像 Groovy 的其他 JVM 語言編寫的,只要他會編譯為 Java 類,就可以為這個類添加自己的擴展。

  • 在擴展函數中可以直接訪問接收者類的其他方法和屬性
  • 擴展函數不允許打破接收者的封裝性,在擴展類中不能訪問接收者的私有或者受保護的成員

導入和擴展函數

一個擴展函數不會自動在整個項目範圍內生效。如果你需要使用它需要進行導入。如果導入後發現了命名衝突可以使用 as 關鍵字來另外定義一個名稱,這樣對導入的類或者函數都是有效的。

import javax.persistence.Entity
import org.hepeng.cornerstone.entity.Entity as E

從 Java 中調用擴展函數

  • 實際上擴展函數是一個靜態函數,它把接收者對象做為第一個參數傳遞給函數。擴展函數本質上是靜態函數的一個高效語法糖。
  • 因為擴展函數的本質是靜態函數所以也不存在重寫的問題
  • 如果一個類的成員函數和擴展函數有相同的簽名,成員函數會被優先使用

因為是靜態函數,這樣調用擴展函數就不會創建適配的對象或者任何運行時的額外開銷。知道了這一點如何從 Java 中調用擴展對象就很簡單了,無非就是調用這個靜態函數罷了。

import kt.demo.StringsKt;
public class JavaClassDemo {
@Test
public void test2() {
String s = "kotlin";
System.out.println(StringsKt.lastChar(s)); // n
}
}

作為擴展函數的工具函數

在學習了以上這些知識後我們可以進一步改寫 joinToString 函數了 :

@JvmOverloads
fun <T> Collection<T>.joinToString(collection: Collection<T> , separator: String = ", " ,
prefix: String = "[" , postfix: String = "]"): String {
val result = StringBuilder(prefix)
for ((index , element) in collection.withIndex()) {
if (index > 0) {
result.append(separator)
}
result.append(element)
}
result.append(postfix)
return result.toString()
}

在 Kotlin 中調用擴展函數 :

val list = listOf("小明", "丹尼", "李華")
println(list.joinToString(separator = " @ ")) // [小明 @ 丹尼 @ 李華]

擴展屬性

擴展屬性提供了一種方法,用來擴展類的 API ,可以用來訪問屬性,用的是屬性語法而不是函數語法。儘管他們被稱為屬性,但是他們可以沒有任何狀態,因為沒有合適的地方來存儲它,不可能給現有的 Java 對象實例添加額外的字段。但有時短語法仍然是便於使用的。

  • 聲明一個擴展屬性,這裡必須顯示的定義 getter 函數,因為沒有對應的字段所以也不會存在默認的 getter 實現。同理初始化也是不可以的,因為沒有地方存儲值。

  • 在 Java 中調用擴展屬性的時候,是顯示的調用它的 getter 函數。

val String.lastChar: Char
get() = this.last()

處理集合:可變參數,中綴調用和庫支持

這節內容會涉及到的語言特性:

  • 可變參數的關鍵字 vararg ,可以用來聲明一個函數將可能有任意數量的參數
  • 一箇中綴表示法,當你在調用一些只有一個參數的函數時,使用它會讓代碼更簡練
  • 解構聲明,用來把一個單獨的組合值展開到多個變量中

Kotlin 擴展 Java 集合的 API

  • Kotlin 對 Java 集合類庫的擴展是通過擴展函數來實現的。
Kotlin系列之函數的定義與調用

Kotlin系列之函數的定義與調用


可變參數:讓函數支持任意數量的參數

使用函數來創建集合的時候可以傳入任意個數的參數。

val list = listOf(1 , 2 , 3 , 4 , 5)

在 Java 中的可變參數是通過 … 聲明的, 可以把任意個數的參數值打包到數組中傳給函數。 Kotlin 的可變參數使用 vararg 聲明。Kotlin 和 Java 之間另一給區別是,當需要傳遞的參數已經包裝在數組中時,調用該函數的語法。在 Java 中可以按原樣傳遞數組 ,而 Kotlin 則要求你顯示的解包數組,以便每個數組元素在函數中能作為單獨的參數來調用。從技術角度來講這個功能被稱為展開運算符,而使用的時候,不過是在參數前面放一個 * 。

fun main(args: Array<String>) {
val list = listOf("args: " , *args)
println(list)
}

鍵值對的處理:中綴調用和解構聲明

在之前的內容中我寫過一些這樣的代碼來創建一個 map 集合。在這行代碼中 to 不是內置的結構,而是一種特殊的函數調用,被稱為中綴調用。

在中綴調用中沒有添加額外的分隔符,函數名稱是直接放在目標對象名稱和參數之間的。 第二行代碼和第一行代碼調用方式是等價的。

val hashMap = hashMapOf(1 to "a", 2 to "b", 3 to "c")
val hashMap = hashMapOf(1.to("a"), 2.to("b"), 3.to("c"))
  • 中綴調用可以與只有一個參數的函數一起使用,無論是普通的函數還是擴展函數。
  • 要允許使用中綴符號調用函數 ,需要使用 infix 修飾符來標記它
infix fun String.join(s: String) = this.plus(" $s")
println("hello" join  "world")  // hello world

解構聲明

解構聲明可以把一個對象解構成很多變量,這樣會帶來一些便利性。

val map = mapOf(1 to "One", 2 to "Two", 3 to "three")
for ((key , value) in map) {
println("key = $key , value = $value")
}

例如這裡 (key , value) in map 就是一個解構聲明

data class Cat(var name: String? , var color: String?) 
val cat = Cat(name = "小將" , color = "白色")
val (name , color) = cat

這裡對 cat 也是一個解構聲明


字符串和正則表達式的處理

Kotlin 字符串和 Java 字符串完全相同。Kotlin 提供了一些有用的擴展函數,使得字符串使用起來更加方便。
Kotlin 中使用與 Java 完全相同的正則表達式語法。

三重引號字符串

val text = """
>Tell me and I forget.
>Teach me and I remember.
>Involve me and I learn.
>(Benjamin Franklin)
"""

三重引號字符串中的內容不會被轉義,它可以包含任何字符,將會保持原樣。上面的字符串打印後會按照原樣輸出。

Kotlin系列之函數的定義與調用

如果為了更好的表示這樣的字符串,可以去掉縮進(左邊距)。為此可以向字符串內容添加前綴,標記邊距的結尾,然後調用 trimMargin 來刪除每行中的前綴和前面的空格。

val text = """
>Tell me and I forget.
>Teach me and I remember.
>Involve me and I learn.
>(Benjamin Franklin)
""".trimMargin(">")

Kotlin系列之函數的定義與調用

三重引號字符串中也是可以使用字符串模板的


讓你的代碼更整潔:局部函數和擴展

許多開發人員認為,好代碼的重要標準之一是減少重複代碼,甚至還給這個原則起了個名字:不要重複你自己(DRY)。但是當你寫 Java 代碼的時候,有時候做到這點就不那麼容易了。許多情況下可以抽取出多個方法,把長的函數分解成許多小的函數然後重用他們。但是這樣可能會讓代碼更費解,因為你以一個包含許多小方法的類告終,而且他們之間沒有明確的關係。可以更進一步將提取的函數組合成一個內部類,這樣就可以保持結構,但是這種函數需要用到大量的樣板代碼。

Kotlin 提供了一個更整潔的方案: 可以在函數中嵌套這些提取的函數。這樣既可以獲得所需要得結構,也無需額外得語法開銷。

data class User(var id:Int , var name: String , var address: String)
fun saveUser(user: User) {
if (user.name.isEmpty()) {
throw IllegalArgumentException("Can't save user ${user.id}: empty Name")
}
if (user.address.isEmpty()) {
throw IllegalArgumentException("Can't save user ${user.id}: empty Address")
}
// 保存到數據庫
}

提取局部函數來避免重複

data class User(var id:Int , var name: String , var address: String)
fun saveUser(user: User) {
fun validate(value: String , fieldName: String) {
if (value.isEmpty()) {
throw IllegalArgumentException("Can't save user ${user.id}: empty $fieldName")
}
}
validate(user.name , "Name")
validate(user.address , "Address")
// 保存到數據庫
}
  • 局部函數可以訪問所在函數中的所有參數和變量

提取邏輯到擴展函數中

data class User(var id:Int , var name: String , var address: String)
fun saveUser(user: User) {
user.validateBeforeSave()
// 保存到數據庫
}
fun User.validateBeforeSave() {
fun validate(value: String , fieldName: String) {
if (value.isEmpty()) {
throw IllegalArgumentException("Can't save user $id: empty $fieldName")
}
}
validate(name , "Name")
validate(address , "Address")
}

Kotlin系列之函數的定義與調用

小結

  • Kotlin 沒有自己的集合類,而是在 Java集合類的基礎上提供了更豐富的 API
  • Kotlin 可以給函數參數定義默認值,這樣大大降低了重載函數的必要性,而且命名參數讓多參數函數的調用更加易讀
  • Kotlin 允許更靈活的代碼結構:函數和屬性都可以直接在文件中聲明,而不僅僅是在類中作為成員
  • Kotlin 可以調用擴展函數和屬性來擴展任何類的 API,包括在外部庫中定義的類,而不修改其源代碼,也沒有運行時開銷
  • 中綴調用提供了處理單個參數的,類似調用運算符方法的簡明語法
  • Kotlin 為普通字符串和正則表達式都提供了大量的方便字符串處理的函數
  • 三重引號的字符串提供了一種簡潔的方式,解決了原本在 Java 中需要進行大量囉嗦的轉義和字符串連接的問題
  • 局部函數幫助你保持代碼的整潔同時,避免重複

內容參考自:


創建了一個 Kotlin 學習交流群有興趣的同學可以加群一起交流學習

Kotlin系列之函數的定義與調用

相關文章

架構師,怎樣才能搞定上下游客戶?

當Parallel遇上了DISpring並行數據聚合最佳實踐

異常記錄——使用Mybatis報BindingException

GoWeb編程之模板(一)