主要內(nèi)容:
- 函數(shù)的定義
- 頂級函數(shù)
- 定義函數(shù)參數(shù)默認值
- 調(diào)用函數(shù)時給定參數(shù)名
- 單表達式函數(shù)
- 中綴調(diào)用
- 擴展函數(shù)
- 局部函數(shù)
- 匿名函數(shù)
- 函數(shù)可變參數(shù)及原理分析
- Java Arrays.asList 可變參數(shù)的坑和原理分析
- 展開操作
前面我們已經(jīng)介紹了函數(shù)的定義和組成,下面在繼續(xù)分析函數(shù)的其他方面
更方便的函數(shù)調(diào)用
調(diào)用函數(shù)時指定參數(shù)的名字
假設我們有如下的函數(shù):
fun <T> joinToString(collection: Collection<T>,
separator: String,
prefix: String,
postfix: String): String
然后調(diào)用該函數(shù)(為參數(shù)值指定參數(shù)名稱):
joinToString(collection, separator = " ", prefix = " ", postfix = ".")
為函數(shù)參數(shù)指定默認值
我們可以把 joinToString 定義改成如下形式:
fun <T> joinToString(collection: Collection<T>,
separator: String = ", ",
prefix: String = "",
postfix: String = "")
我們分別為函數(shù)的最后三個參數(shù)都設置了默認值却舀,我們可以這樣調(diào)用該函數(shù):
joinToString(list)
joinToString(list, prefix = "# ")
這樣也就間接的實現(xiàn)了Java中所謂的重載(overload)鸯屿,代碼也更簡潔虑稼,不用定義多個方法了
Parameter和Argument的區(qū)別
看過 《Kotlin In Action》 的英文原版細心的同學可能會發(fā)現(xiàn):書中的 3.2.1 章節(jié)是 Named Arguments
直譯過來是:為參數(shù)命名喉前。作者為什么沒有寫成 Named Parameters 呢哩掺?
下面我們就來看下 Parameter 和 Argument 的區(qū)別
簡而言之垄懂,就是在定義函數(shù)時候的參數(shù)稱之為 Parameter虚汛;調(diào)用函數(shù)傳入的參數(shù)稱之為 Argument
此外围来,除了 Parameter 和 Argument 跺涤,還有 Type Parameter 和 Type Argument
因為下面還要用到這兩個的概念,所以這里我們介紹下 Type Parameter 和 Type Argument
Type Parameter 和 Type Argument 的概念是在泛型類或者泛型函數(shù)的時候出現(xiàn)
頂級函數(shù)和屬性
在 Java 中我們需要把函數(shù)和屬性放在一個類中
在 Kotlin 中我們可以把某個函數(shù)或屬性直接放到某個 Kotlin 文件中
把這樣的函數(shù)或屬性稱之為 頂級(top level)函數(shù)或屬性
例如在 join.kt 文件中:
package strings
fun joinToString(...): String {
...
}
在 Java 代碼中如何調(diào)用該方法呢监透?因為 JVM 虛擬機只能執(zhí)行類中的代碼
所以 Kotlin 會生成一個名叫 JoinKt 的類桶错,并且頂級函數(shù)是靜態(tài)的
所以可以在 Java 中這樣調(diào)用頂級函數(shù):
JoinKt.joinToString(...)
在Kotlin中如何調(diào)用,如果在不同的包胀蛮,需要把這個頂級函數(shù)導入才能調(diào)用
//相當于 import strings.JoinKt.joinToString
import strings.joinToString
//相當于 import strings.JoinKt.*
import strings.*
所有的工具類都可以使用這樣的方式來定義
頂級屬性 同樣也是 static 靜態(tài)的
如果使用 var 來定義會生成對應的靜態(tài)setter院刁、getter函數(shù)
如果使用 val 來定義只會生成對應的靜態(tài)getter函數(shù)
我們知道頂級函數(shù)和屬性,最終還是會編譯放在一個類里面醇滥,這個類名就是頂級函數(shù)或屬性的 Kotlin文件名稱+Kt
如果所在的Kotlin文件名被修改黎比,編譯生成的類名也會被修改,可以通過注解的方式來固定編譯生成的類名:
@file:JvmName("StringFunctions")
package strings
fun joinToString(...): String {
...
}
調(diào)用的時候就可以這樣來調(diào)用:
import strings.StringFunctions;
StringFunctions.joinToString(list, ", ", "", "");
擴展函數(shù)
何謂 擴展函數(shù) 鸳玩? 擴展函數(shù)是在類的外部定義阅虫,但是可以像類成員一樣調(diào)用該函數(shù)
其中 receiver type 就是我們擴展的目標類,receiver object 就是目標類的對象(哪個對象調(diào)用該擴展函數(shù)不跟,這個this就是哪個對象)
lastChar 就是我們?yōu)?String 類擴展的函數(shù)
package strings
fun String.lastChar(): Char = this.get(this.length - 1)
然后我們這樣來調(diào)用該擴展函數(shù):
println("Kotlin".lastChar())
如果擴展函數(shù)所在的包名和使用地方的包名不一樣的話颓帝,需要導入擴展函數(shù)
import strings.*
//或者
import strings.lastChar
val c = "Kotlin".lastChar()
擴展函數(shù)原理分析
擴展函數(shù)本質上是靜態(tài)函數(shù),如上面的擴展函數(shù) lastChar 反編譯后對應的 Java 代碼:
public static final char lastChar(@NotNull String $receiver) {
Intrinsics.checkParameterIsNotNull($receiver, "receiver$0");
return $receiver.charAt($receiver.length() - 1);
}
編譯的時候,會在調(diào)用的該擴展函數(shù)的地方使用 StringUtilsKt.lastChar("") 代替
所以购城,如果要在 Java 中使用 Kotlin 定義的擴展函數(shù)吕座,也是直接調(diào)用該靜態(tài)方法即可
并且擴展函數(shù)是不能被覆寫(override) 的,因為它本質上是一個靜態(tài)函數(shù)
擴展屬性
擴展屬性和擴展函數(shù)的定義非常相似:
val String.lastChar: Char
get() = this.get(length - 1)
我們必須為這個擴展屬性定義 getter 函數(shù)瘪板,因為擴展屬性沒有 backing field
擴展屬性在定義的時候吴趴,也會生成靜態(tài)方法:
public static final char getLastChar(@NotNull String $receiver) {
Intrinsics.checkParameterIsNotNull($receiver, "receiver$0");
return $receiver.charAt($receiver.length() - 1);
}
如果擴展屬性的 receiver object 可以被修改,可以把擴展屬性定義成 var
var StringBuilder.lastChar: Char
get() = get(length - 1)
set(value: Char) {
this.setCharAt(length - 1, value)
}
函數(shù)的可變參數(shù)和展開操作符
可變參數(shù)
在 Java 中通過三個點(...)來聲明可變參數(shù)侮攀,如:
public static <T> List<T> listOf(T... items) {
System.out.println(items.getClass()); //數(shù)組類型
return Arrays.asList(items);
}
Kotlin 和 Java 不一樣锣枝,Kotlin 使用 vararg 關鍵來定義可變參數(shù):
fun <T> listOf(vararg items: T): List<T> {
println(items.javaClass) //數(shù)組類型
return Arrays.asList(*items) // * spread operator
}
對于可變參數(shù)的函數(shù),調(diào)用它的時候可以傳遞任意個參數(shù)
展開操作符
通過上面的兩段代碼比較我們發(fā)現(xiàn):Kotlin 需要顯示的將可變參數(shù)通過 * 展開兰英,然后傳遞給 asList 函數(shù)
這里的 * 就是 展開操作符(spread operator)撇叁,在 Java 中是沒有 展開操作符 的
下面我們再來看下,展開操作符的方便之處:
val intArr: Array<Int> = arrayOf(1, 2, 3, 4)
Arrays.asList(0, intArr).run {
println("size = $size")
}
//輸出結果:
size = 2
可以發(fā)現(xiàn)畦贸,不用展示操作符的話陨闹,集合里面只有兩個元素
那我們把它改成使用 展開操作符 的情況:
val intArr: Array<Int> = arrayOf(1, 2, 3, 4)
Arrays.asList(0, *intArr).run {
println("size = $size")
}
//輸出結果:
size = 5
Java中的Arrays.asList()的坑和原理分析
既然上面用到了 Java 中的 Arrays.asList() 函數(shù),下面來講下該函數(shù)的容易遇到的坑及原理分析:
public static void testArrays() {
int[] intArr = {1, 2, 3};
List list = Arrays.asList(intArr);
println(list.size()); //size = 1
}
public static void testArrays2() {
Integer[] intArr ={1, 2, 3};
List list = Arrays.asList(intArr);
println(list.size()); //size = 3
}
上面的 testArrays 和 testArrays2 函數(shù)非常相似薄坏,只不過是數(shù)組的類型不同趋厉,導致 Arrays.asList(arr) 返回的集合大小不一樣
只要是 原始類型數(shù)組 Arrays.asList 返回的集合大小為 1,如果是 復雜類型的數(shù)組胶坠,Arrays.asList 返回的集合大小為數(shù)組的大小
為什么會產(chǎn)生這種情況呢觅廓?下面來分析下:
首先看下 Arrays.asList 是怎么定義的:
public static <T> List<T> asList(T... a)
Java 中的可變參數(shù)相當于數(shù)組:
public static <T> List<T> asList(T[] a)
我們知道 Java 中的泛型必須是復雜類型,所以這里的泛型 T 也必須是 復雜類型
當我們傳遞 int[] 數(shù)組的時候涵但,就會出現(xiàn)問題杈绸,因為 int 是原始類型,T 是復雜類型
所以 int[] 賦值給 T[] 是非法的矮瘟,當 一維原始類型的數(shù)組 當做給可變參數(shù)的時候瞳脓,編譯器會把這個可變參數(shù)編譯成一個 二維數(shù)組
這就是為什么會出現(xiàn)上面情況的原因**
我們再來看下 Arrays.asList 完整源碼:
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
private static final long serialVersionUID = -2764017481108945198L;
private final E[] a;
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
//省略其他...
}
經(jīng)過上面的分析我們知道,如果是一維原始類型的數(shù)組傳遞給可變參數(shù)澈侠,這個可變參數(shù)就是 二維數(shù)組
然后把二維數(shù)組傳遞給內(nèi)部ArrayList的構造方法劫侧,通過 E[] 保存下來。這里的泛型 E 就相當于 int[]哨啃,E[] 相當于 int[][]
需要注意是 Java 不允許 將個二維數(shù)組 直接賦值 給一維的泛型數(shù)組:
int[][] intArray = {{1},{2}};
T[] t = intArray; //非法
但是 Java 允許 把二維數(shù)組傳遞給參數(shù)是一維的泛型數(shù)組的函數(shù)烧栋,如:
public static <T> void testGeneric(T[] data){
}
int[][] intArray = {{1},{2}};
testGeneric(intArray);
Kotlin 展開操作符的原理分析
講到這里你可能迫不及待的想知道,為什么我們上面的代碼使用了展開操作符 Arrays.asList(*intArr) 返回的集合大小就是 5 呢拳球?
val intArr: Array<Int> = arrayOf(1, 2, 3, 4)
Arrays.asList(0, *intArr).run {
println("size = $size")
}
//輸出結果:
size = 5
反編譯后對應的 Java 代碼如下:
Integer[] intArr2 = new Integer[]{1, 2, 3, 4};
SpreadBuilder var10000 = new SpreadBuilder(2);
var10000.add(0); //第1個元素
var10000.addSpread(intArr2); //數(shù)組里的4個元素
List var2 = Arrays.asList((Integer[])var10000.toArray(new Integer[var10000.size()]));
int var7 = false;
String var5 = "size = " + var2.size();
System.out.println(var5);
原來會通過 SpreadBuilder 來處理展開操作符审姓,SpreadBuilder 里面維護了一個ArrayList
所有的元素都會保存到這個 ArrayList 中,然后把這個集合轉成 元素為復雜類型數(shù)組祝峻,再傳給 Arrays.asList(arr) 函數(shù)
根據(jù)上面我們對 Arrays.asList(arr) 的分析魔吐,我們就知道返回的集合大小是 5 了
中綴調(diào)用
我們都知道什么是前綴(prefix)扎筒,后綴(suffix)。那什么是函數(shù)的中綴(infix)調(diào)用呢酬姆?
使用關鍵字 infix 修飾的函數(shù)都能夠 中綴調(diào)用
被關鍵字 infix 修飾的函數(shù)只能有一個參數(shù)
Kotlin 中的 to 就是一個中綴函數(shù):
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
下面我們來對比下 to 函數(shù)的常規(guī)調(diào)用和中綴調(diào)用:
1.to("one") //普通的函數(shù)調(diào)用
1 to "one" //函數(shù)的中綴調(diào)用
除了 to 函數(shù)嗜桌,還有我們介紹 循環(huán) 的時候講到的 until、downTo辞色、step 也是中綴函數(shù):
public infix fun Int.until(to: Int): IntRange {
if (to <= Int.MIN_VALUE) return IntRange.EMPTY
return this .. (to - 1).toInt()
}
public infix fun Int.downTo(to: Int): IntProgression {
return IntProgression.fromClosedRange(this, to, -1)
}
public infix fun IntProgression.step(step: Int): IntProgression {
checkStepIsPositive(step > 0, step)
return IntProgression.fromClosedRange(first, last, if (this.step > 0) step else -step)
}
//使用示例:
for(i in 0 until 100){
}
for (i in 100 downTo 0 step 2) {
}
局部函數(shù)
局部函數(shù)(local function) 是在函數(shù)里面定義函數(shù)骨宠,局部函數(shù)只能在函數(shù)內(nèi)部使用j局部函數(shù)說白了就是函數(shù)嵌套,那什么時候使用局部函數(shù)呢相满?當一個函數(shù)里的邏輯很多重復的邏輯诱篷,可以把這些邏輯抽取到一個局部函數(shù)
以《Kotlin In Action》的代碼為例:
fun saveUser(user: User) {
if (user.name.isEmpty()) {
throw IllegalArgumentException("Cannot save user ${user.id}: Name is empty")
}
if (user.address.isEmpty()) {
throw IllegalArgumentException("Cannot save user ${user.id}: Address is empty")
}
// Save user to the database
}
這個 saveUser 函數(shù)里面有些重復邏輯,如果 name 或 address 為空都會拋出異常
可以使用局部函數(shù)優(yōu)化下:
fun saveUser(user: User) {
fun validate(value: String, fieldName: String) {
if (value.isEmpty()) {
throw IllegalArgumentException("Can't save user ${user.id}: " + "$fieldName is empty")
}
}
validate(user.name, "Name")
validate(user.address, "Address")
// Save user to the database
}
局部函數(shù)避免了模板代碼的出現(xiàn)雳灵。如果不使用局部函數(shù),我們需要把 validate函數(shù) 定義到外面去闸盔,但是這個函數(shù)只會被 saveUser函數(shù) 使用到悯辙,從而污染了外面的全局作用域。通過局部函數(shù)使得代碼更加清晰迎吵,可讀性更高躲撰。
需要注意的是,雖然 Kotlin 允許在函數(shù)內(nèi)部定義函數(shù)击费,但是不要嵌套太深拢蛋,否則會導致可讀性太差
匿名函數(shù)
匿名函數(shù)顧名思義就是沒有名字的函數(shù):如:
fun(x: Int, y: Int): Int {
return x + y
}
匿名函數(shù)的返回類型的推導機制和普通函數(shù)一樣:
fun(x: Int, y: Int) = x + y
如果聲明了一個匿名函數(shù) ,如何調(diào)用呢蔫巩?
(fun(x: Int, y: Int): Int {
val result = x + y
println("sum:$result")
return result
})(1, 9)
//輸出結果:
//sum:10