本文将介绍如何使用 Kotlin 的高阶函数,如sumBy
, reduce
, fold
, map
,filter
,forEach
等,来应对常见的集合数据处理场景。不了解高阶函数的同学可以先看下之前的文章。
场景:输入一个账户列表List
,求这些账户的财产总和sum
。
一般来说 Java 可以这样写:
public int getAccountsSum(List accounts) {
int sum = 0;
for (Account a: accounts) {
sum += a.value;
}
return sum;
}
Kotlin 可以使用高阶函数 sumBy
:
val sum = accounts.sumBy { it.value }
那么sumBy
做了什么呢?点击源码可以看到,其实它做的事情和上面 Java 实现的getAccountsSum
是一样的,只是增加的值是通过我们传入的 lambda 来计算,而不是写死的Account.value
。这种通过传入函数来完成函数功能的函数,被称为高阶函数,高阶函数也因此具有很高的通用性和复用效率。
不仅传入函数作为参数的函数被称为高阶函数,返回值为函数的函数也同样被称为高阶函数。
sumBy
有一点不好,他只能求和,而且只接受Int
和Double
两种类型的值(sumBy:不然我起这个名字干嘛?)。如果我们要得到一个更复杂的逻辑的结果呢?
场景:输入一个列表List
,返回它们全部相乘的结果。
Java:
public int getResult(List values) {
int result = 0;
for (Account a: accounts) {
result *= values;
}
return result;
}
Kotlin 我们可以使用reduce
:
val result = values.reduce { acc, v -> acc * v }
reduce
的逻辑是:将初始值acc
设置为集合的第一个值,然后从第二个值开始,依次执行acc = lambda(acc, v)
,遍历完后返回acc
。**reduce
不仅限做加法运算,它比sumBy
具有更广的通用性。
那如果reduce
可以代替sumBy
,为什么还需要sumBy
?——因为它写起来更简单呀!
如果集合为空,
reduce
会抛出 UnsupportedOperationException(“Empty collection can’t be reduced.”)
细心的同学已经发现了,sumBy
的场景和reduce
的场景用的是不同的数据结构。因为acc
会被初始化为集合的第一个元素,所以reduce
函数的输出也被限制为集合的范型类型。也就是说,sumBy
的场景无法用reduce
代替。
那 Kotlin 有没有能指定acc
类型的高阶函数?有的,它叫fold
。
我们再回到sumBy
的场景:输入一个账户列表List
,求这些账户的财产总和sum
:
val result = accounts.fold(0) { acc, v -> acc + v.value }
fold
比reduce
多了一个参数——初始值,用来赋值给acc
。得益于范型,我们可以通过这个办法来指定acc
的类型。这样一来,fold
可以完美替代sumBy
的场景。而相比fold
,sumBy
更专用,表意更清晰,写起来也更简洁。
fold
还有另一点好:因为acc
由传入参数初始化,所以没有集合不能为空的限制。所以绝大部分情况下,我都建议使用fold
来代替reduce
。
JavaScript 的 reduce 函数就是 Kotlin 的 fold 函数。u1s1,Kotlin 的 reduce 函数挺危险的,还有类型限制,不建议使用。
场景:输入一个账户列表List
,返回资产小于 100 的账户:
Java:
public List getPoorAccounts(List accounts) {
List qbAccounts = new ArrayList<>(); // 想起虾米音乐的 穷逼 VIP 了
for (Account a: accounts) {
if (a.value < 100) {
qbAccounts.add(a);
}
}
return qbAccounts;
}
Kotlin 可以使用filter
函数:
val qbAccounts = accounts.filter { it.value < 100 }
filter
的逻辑是,新建一个空的 ArrayList(),然后把 lambda 返回值为 true 的元素加入到这个列表里。
场景:输入一个账户列表List
,找到所有资产大于 10000 的账户,封装成 VIP 账户返回:
Java:
public List getVipAccounts(List accounts) {
List vipAccounts = new ArrayList<>();
for (Account a: accounts) {
if (a.value >= 10000) {
vipAccounts.add(new VipAccount(a));
}
}
return vipAccounts;
}
Kotlin 可以通过filter
函数加map
函数完成:
val vipAccounts = accounts
.filter { it.value >= 10000 }
.map { VipAccount(it) }
第一步我们用filter
函数筛选出资产大于 10000 的账户,然后用map
函数将过滤后的每一个账户转换为VipAccount
。map
的逻辑也很简单,它回返回一个和调用者大小相同的列表,具体的元素值为 lambda 的执行结果。
如果遇到了已知高阶函数都不适合的场景,不妨试试用forEach
代替传统的 for 循环。为什么?因为写起来稍微简单一点。。
accounts.forEach {
println("account: $it")
}
什么?你还想要 index下标?
accounts.forEachIndexed { index, account ->
println("index: $index")
println("account: $account")
}
不仅forEach
有下标版本forEachIndexed
,几乎所有高阶函数都有对应的 Indexed 版本。
Kotlin 官方提供了数十个高阶函数,但其实掌握了以上几个高阶函数,基本可以 cover 所有场景了。其他的只是写的简洁还是写的复杂一点的区别。而且你还有另一条路可以走:自己写一个特定的高阶函数。
大家可能会担心,如此频繁的声明 lambda,会不会使得类的数量大量膨胀?其实官方提供的高阶函数,都是用inline
关键字修饰的。这意味着不仅高阶函数的调用最终会被函数的实际代码代替,而且声明的 lambda 也会被解析成具体的代码,而不是方法调用。所以Kotlin 高阶函数用 inline 关键字修饰,所以 lambda 不会生成新的 jvm class。而我们在声明自己的高阶函数时,也应该用inline
关键字修饰,防止类数量膨胀。
大家可能担心的另一点,像map
,filter
这样返回列表的高阶函数,每一次操作都会生成一个列表,这会不会增加垃圾回收的压力?答案是会的。但如果数据量不是万级别的,操作频率不是毫秒级别的,对性能的影响实在小之又小,特别是在移动端的场景更是难以遇到。但我们还是要了解高阶函数对性能开销,在对性能要求高的位置避免对象申请(如UI绘制的回调)。
Java 也类似高阶函数的能力,如 Collections.sort 这种允许自定义排序的方法,和 Java 8 的 steam API。但因为 Java 没有 inline 无法有效的优化 lambda,且 Java 的 lambda 没有完整的闭包特性,无法修改外部变量。还有一些语法的原因,Java 的高阶函数使用起来相对没有那么舒服。