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

建立了一個 Kotlin 學習交流羣有興趣的同窗能夠加羣一塊兒交流學習

本章內容包括

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

在 Kotlin 中建立集合

Kotlin 沒有本身的集合類庫而是徹底使用標準的 Java 集合類庫。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類型的參數究竟意味着什麼,只要參數的順序傳錯了你就會獲得一些奇怪的結果。爲了不這個問題你須要去看一下它的函數聲明,來肯定每一個位置上的須要的是什麼參數。bash

在 Kotlin 中能夠經過命名參數來解決這個問題, 就是在調用一個函數傳入參數的時候,能夠顯示的寫上參數的名稱,而且指定要傳入的值賦值給那個參數。可是若是在調用一個函數時,指明瞭一個參數的名稱時,爲了不混淆,這個參數以後的全部參數都須要標明名稱了。 例如我對 prefix 參數標明瞭名稱,那麼必須在對以後的 postfix 和 separator 參數都標明名稱。架構

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

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 的重載函數,從最後一個開始省略每一個參數,被省略的參數使用的是函數聲明時指定的默認值。

@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 。

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

仔細觀察能夠發現 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 的註解,將其放到這個文件的開頭,爲於包名的前面:

使用時就可使用 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 集合類庫的擴展是經過擴展函數來實現的。


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

使用函數來建立集合的時候能夠傳入任意個數的參數。

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) """
複製代碼

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

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

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

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


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

許多開發人員認爲,好代碼的重要標準之一是減小重複代碼,甚至還給這個原則起了個名字:不要重複你本身(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 沒有本身的集合類,而是在 Java集合類的基礎上提供了更豐富的 API
  • Kotlin 能夠給函數參數定義默認值,這樣大大下降了重載函數的必要性,並且命名參數讓多參數函數的調用更加易讀
  • Kotlin 容許更靈活的代碼結構:函數和屬性均可以直接在文件中聲明,而不只僅是在類中做爲成員
  • Kotlin 能夠調用擴展函數和屬性來擴展任何類的 API,包括在外部庫中定義的類,而不修改其源代碼,也沒有運行時開銷
  • 中綴調用提供了處理單個參數的,相似調用運算符方法的簡明語法
  • Kotlin 爲普通字符串和正則表達式都提供了大量的方便字符串處理的函數
  • 三重引號的字符串提供了一種簡潔的方式,解決了本來在 Java 中須要進行大量囉嗦的轉義和字符串鏈接的問題
  • 局部函數幫助你保持代碼的整潔同時,避免重複

內容參考自:


建立了一個 Kotlin 學習交流羣有興趣的同窗能夠加羣一塊兒交流學習

相關文章
相關標籤/搜索