在做 Collection 操作時,還有一個很常見的情境,就是要將 Collection 內所有元素做運算,最後回傳「一個值」,這種操作我們就統一稱做聚合(Aggregation)。在這個章節裡,我們就要來討論這一系列的操作。
這種聚合操作最常見的就是拿來計算,比方說計算 Collection 裡的數量、Collection 裡所有數值的總合、平均值…等。在 Kotlin 標準函式庫裡直接採用 count()
、sum()
及 average()
等在大多數程式語言裡相同的命名。
val numbers = listOf(6, 42, 10, 4)
numbers.count() // 4
numbers.sum() // 62
numbers.average() // 15.5
假如你想要指定計算總和的方式,sumBy()
可以用傳入一個 Lambda 來達成。不過 sumBy()
回傳的是 Int
,假如你希望回傳的是更精準的 Double
的話,用 sumByDouble()
。
val numbers = listOf(5, 42, 10, 4)
numbers.sumBy { it * 2 } // 122
numbers.sumByDouble { it.toDouble() / 2 } // 30.5
聚合操作裡也很常見的就是做數值比較,比方說在 Collection 裡找出最大值(maxOrNull()
)或是最小值(minOrNull()
)。
val numbers = listOf(6, 42, 10, 4)
numbers.maxOrNull() // 42
numbers.minOrNull() // 4
當然,一如往常的,這兩個 method 也有可以傳入 Lambda 自行定義行為的 minByOrNull()
及 maxByOrNull()
,假如找出最大/最小值的邏輯需要客製化的話,別忘了用這個版本。
val numbers = listOf(5, 42, 10, 4)
val min3Remainder = numbers.minByOrNull { it % 3 } // 42
val max3Remainder = numbers.maxByOrNull { it % 3 } // 5
另外,也有 maxWithOrNull()
及 minWithOrNull()
這兩個 method 可以用 Comparator
物件做為比較基準。
val strings = listOf("one", "two", "three", "four")
val longestString = strings.maxWithOrNull(compareBy { it.length }) // three
val shortestString = strings.minWithOrNull(compareBy { it.length }) // one
值得一提的是,這些 method 在許多舊版本的範例裡是都沒有 OrNull
的。不過這些 method 將在新版本裡被棄用,記得要改用比較安全的 OrNull
版本喔!
假如需要更客製化的操作,那就要使用 reduce()
和 fold()
了。這兩個 method 可以傳入一個 Lambda,在這個 Lambda 裡會接到之前所有的總合以及當前的元素兩個參數,你可以指定計算行為後回傳。而 reduce()
和 fold()
的差別,就在於 fold()
可以指定初始值。
val numbers = listOf(5, 2, 10, 4)
val sum = numbers.reduce { sum, element -> sum + element } // 21
val sumDoubled = numbers.fold(0) { sum, element -> sum + element * 2 } // 42
預設 reduce()
和 fold()
的計算方向是從左到右,但假如你是要從右算到左的話,可以用 reduceRight()
及 foldRight()
。假如你希望 Lambda 可以拿到 Collection 的 index 的話,可以用 reduceIndexed()
和 foldIndexed()
。當然假如要反向計算的話,也有對應的 reduceRightIndexed()
及 foldRightIndexed()
。
val numbers = listOf(5, 2, 10, 4)
val sumDoubledRight = numbers.foldRight(0) { element, sum -> sum + element * 2 } // 42
val sumEven = numbers.foldIndexed(0) { index, sum, element -> if (index % 2 == 0) sum + element else sum } // 15
val sumEvenRight = numbers.foldRightIndexed(0) { index, element, sum -> if (index % 2 == 0) sum + element else sum } // 15
另外要注意的是,所有的 reduce 操作遇到空 Collection 時會拋 Exception。假如希望直接回傳 null 的話,可以把所有 method 以 OrNull
結尾。
這個章節討論的操作都有一個共同點,就是最後都只會回傳一個值。數值計算和比較非常的實用,可以省下自己用迴圈計算的功;而 reduce()
和 fold()
則是在 Functional Programing 裡很常用到的動作。為了一覽這些 API 在不同 Collection 上的行為,以下用表格來整理本章所討論到的 method:
行為 | Array | List | Set | Map | |
---|---|---|---|---|---|
count() | 計算個數 | v | v | v | v |
sum() | 計算總和 | v | v | v | x |
sumBy{} | 依條件計算總和回傳 Int | v | v | v | x |
sumByDouble{} | 依條件計算總和回傳 Double | v | v | v | x |
average() | 求平均值 | v | v | v | x |
minOrNull() | 求最小值 | v | v | v | x |
maxOrNull() | 求最大值 | v | v | v | x |
minByOrNull{} | 依條件求最小值 | v | v | v | v |
maxByOrNull{} | 依條件求最大值 | v | v | v | v |
minWithOrNull{} | 依比較物件求最小值 | v | v | v | v |
maxWithOrNull{} | 依比較物件求最大值 | v | v | v | v |
reduce{} | 依 Lambda 聚合 | v | v | v | x |
reduceOrNull{} | 依 Lambda 聚合若空則回傳 null | v | v | v | x |
reduceRight{} | 由右向左依 Lambda 聚合 | v | v | x | x |
reduceRightOrNull{} | 由右向左依 Lambda 聚合若空則回傳 null | v | v | x | x |
reduceIndexed{} | 依 Lambda 聚合(有 index) | v | v | v | x |
reduceIndexedOrNull{} | 依 Lambda 聚合若空則回傳 null (有 index) | v | v | v | x |
reduceRightIndexed{} | 由右向左依 Lambda 聚合(有 index) | v | v | x | x |
reduceRightIndexedOrNull{} | 由右向左依 Lambda 聚合若空則回傳 null(有 index) | v | v | x | x |
fold(init) {} | 依 Lambda 以初始值聚合 | v | v | v | x |
foldRight(init) {} | 由右向左依 Lambda 以初始值聚合 | v | v | x | x |
foldIndexed(init) {} | 依 Lambda 以初始值聚合(有 index) | v | v | v | x |
foldRightIndexed(init) {} | 由右向左依 Lambda 以初始值聚合(有 index) | v | v | x | x |