标准 Kotlin 库中有哪些 Java 8 Stream.collect 等价物?
What Java 8 Stream.collect equivalents are available in the standard Kotlin library?
在 Java 8 中,有 Stream.collect
允许对集合进行聚合。在 Kotlin 中,这并不以相同的方式存在,除了可能作为 stdlib 中的扩展函数的集合。但尚不清楚不同用例的等价物是什么。
例如,在 top of the JavaDoc for Collectors
处是为 Java 8 编写的示例,当将它们移植到 Kolin 时,您不能使用 Java 8 类在不同的 JDK 版本上,它们可能应该以不同的方式编写。
就显示 Kotlin 集合示例的在线资源而言,它们通常微不足道,无法真正与相同的用例进行比较。什么是真正匹配 Java 8 Stream.collect
记录的案例的好例子?那里的名单是:
- 将名称累积到列表中
- 将名称累积到 TreeSet 中
- 将元素转换为字符串并连接它们,以逗号分隔
- 计算员工的工资总和
- 按部门对员工进行分组
- 按部门计算工资总和
- 将学生分为合格和不合格
在上面链接的 Java 文档中有详细信息。
注: 此题为作者(Self-Answered Questions)特意写下并回答,以便常见的 Kotlin 主题的惯用答案出现在 SO 中。还要澄清一些为 Kotlin 的 alpha 编写的非常古老的答案,这些答案对于当今的 Kotlin 来说并不准确。
Kotlin stdlib 中有用于平均、计数、不同、过滤、查找、分组、连接、映射、最小值、最大值、分区、切片、排序、求和、to/from 数组的函数,to/from 列表、to/from 映射、联合、co-iteration、所有函数范式等等。因此,您可以使用它们来创建小的 1 行,而无需使用更复杂的语法 Java 8.
我认为 built-in Java 8 Collectors
class 中唯一缺少的是总结(但在 是一个简单的解决方案) .
两者都缺少一件事是按计数进行批处理,这在 and has a simple answer as well. Another interesting case is this one also from Stack Overflow: . And if you want to create something like Stream.collect
for another purpose, see
中可以看到
编辑 2017 年 8 月 11 日: Chunked/windowed 在 kotlin 1.2 M2 中添加了收集操作,请参阅 https://blog.jetbrains.com/kotlin/2017/08/kotlin-1-2-m2-is-out/
在创建可能已经存在的新功能之前,先探索整个 API Reference for kotlin.collections 总是好的。
以下是从 Java 8 Stream.collect
个示例到 Kotlin 中等效项的一些转换:
将名称累加到列表中
// Java:
List<String> list = people.stream().map(Person::getName).collect(Collectors.toList());
// Kotlin:
val list = people.map { it.name } // toList() not needed
将元素转换为字符串并连接它们,以逗号分隔
// Java:
String joined = things.stream()
.map(Object::toString)
.collect(Collectors.joining(", "));
// Kotlin:
val joined = things.joinToString(", ")
计算雇员的工资总和
// Java:
int total = employees.stream()
.collect(Collectors.summingInt(Employee::getSalary)));
// Kotlin:
val total = employees.sumBy { it.salary }
按部门对员工进行分组
// Java:
Map<Department, List<Employee>> byDept
= employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
// Kotlin:
val byDept = employees.groupBy { it.department }
按部门计算工资总和
// Java:
Map<Department, Integer> totalByDept
= employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment,
Collectors.summingInt(Employee::getSalary)));
// Kotlin:
val totalByDept = employees.groupBy { it.dept }.mapValues { it.value.sumBy { it.salary }}
将学生分为合格和不合格
// Java:
Map<Boolean, List<Student>> passingFailing =
students.stream()
.collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));
// Kotlin:
val passingFailing = students.partition { it.grade >= PASS_THRESHOLD }
男成员姓名
// Java:
List<String> namesOfMaleMembers = roster
.stream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.map(p -> p.getName())
.collect(Collectors.toList());
// Kotlin:
val namesOfMaleMembers = roster.filter { it.gender == Person.Sex.MALE }.map { it.name }
名册中成员的群组名称(按性别)
// Java:
Map<Person.Sex, List<String>> namesByGender =
roster.stream().collect(
Collectors.groupingBy(
Person::getGender,
Collectors.mapping(
Person::getName,
Collectors.toList())));
// Kotlin:
val namesByGender = roster.groupBy { it.gender }.mapValues { it.value.map { it.name } }
将一个列表过滤到另一个列表
// Java:
List<String> filtered = items.stream()
.filter( item -> item.startsWith("o") )
.collect(Collectors.toList());
// Kotlin:
val filtered = items.filter { it.startsWith('o') }
寻找最短的字符串列表
// Java:
String shortest = items.stream()
.min(Comparator.comparing(item -> item.length()))
.get();
// Kotlin:
val shortest = items.minBy { it.length }
应用过滤器后对列表中的项目进行计数
// Java:
long count = items.stream().filter( item -> item.startsWith("t")).count();
// Kotlin:
val count = items.filter { it.startsWith('t') }.size
// but better to not filter, but count with a predicate
val count = items.count { it.startsWith('t') }
然后继续......在所有情况下,不需要特殊的折叠、减少或其他功能来模仿 Stream.collect
。如果您有更多的用例,请在评论中添加它们,我们会看到!
关于懒惰
如果你想惰性处理链,你可以在链前使用 asSequence()
转换为 Sequence
。在函数链的末尾,您通常也会以 Sequence
结尾。然后你可以使用 toList()
、toSet()
、toMap()
或其他函数来具体化最后的 Sequence
。
// switch to and from lazy
val someList = items.asSequence().filter { ... }.take(10).map { ... }.toList()
// switch to lazy, but sorted() brings us out again at the end
val someList = items.asSequence().filter { ... }.take(10).map { ... }.sorted()
为什么没有类型?!?
您会注意到 Kotlin 示例没有指定类型。这是因为 Kotlin 具有完整的类型推断并且在编译时是完全类型安全的。比 Java 更重要,因为它还具有可为空的类型,并且可以帮助防止可怕的 NPE。所以在 Kotlin 中是这样的:
val someList = people.filter { it.age <= 30 }.map { it.name }
等同于:
val someList: List<String> = people.filter { it.age <= 30 }.map { it.name }
因为 Kotlin 知道 people
是什么,而 people.age
是 Int
,因此过滤器表达式只允许与 Int
进行比较,而 people.name
是一个 String
,因此 map
步骤产生一个 List<String>
(String
的只读 List
)。
现在,如果 people
可能是 null
,as-in 一个 List<People>?
那么:
val someList = people?.filter { it.age <= 30 }?.map { it.name }
Returns 需要进行空值检查的 List<String>?
( 或对可空值使用其他 Kotlin 运算符之一,请参阅此 and also Idiomatic way of handling nullable or empty list in Kotlin )
另请参阅:
- API 参考 extension functions for Iterable
- API 参考 extension functions for Array
- API 参考 extension functions for List
- API 参考 extension functions to Map
有关其他示例,这里是 Java 8 Stream Tutorial 中转换为 Kotlin 的所有示例。每个例子的标题,来源于文章来源:
流的工作原理
// Java:
List<String> myList = Arrays.asList("a1", "a2", "b1", "c2", "c1");
myList.stream()
.filter(s -> s.startsWith("c"))
.map(String::toUpperCase)
.sorted()
.forEach(System.out::println);
// C1
// C2
// Kotlin:
val list = listOf("a1", "a2", "b1", "c2", "c1")
list.filter { it.startsWith('c') }.map (String::toUpperCase).sorted()
.forEach (::println)
不同类型的流#1
// Java:
Arrays.asList("a1", "a2", "a3")
.stream()
.findFirst()
.ifPresent(System.out::println);
// Kotlin:
listOf("a1", "a2", "a3").firstOrNull()?.apply(::println)
或者,在名为 ifPresent 的字符串上创建一个扩展函数:
// Kotlin:
inline fun String?.ifPresent(thenDo: (String)->Unit) = this?.apply { thenDo(this) }
// now use the new extension function:
listOf("a1", "a2", "a3").firstOrNull().ifPresent(::println)
另请参阅:apply()
function
另请参阅:Extension Functions
另请参阅:?.
Safe Call operator, and in general nullability: In Kotlin, what is the idiomatic way to deal with nullable values, referencing or converting them
不同类型的流 #2
// Java:
Stream.of("a1", "a2", "a3")
.findFirst()
.ifPresent(System.out::println);
// Kotlin:
sequenceOf("a1", "a2", "a3").firstOrNull()?.apply(::println)
不同类型的流 #3
// Java:
IntStream.range(1, 4).forEach(System.out::println);
// Kotlin: (inclusive range)
(1..3).forEach(::println)
不同类型的流 #4
// Java:
Arrays.stream(new int[] {1, 2, 3})
.map(n -> 2 * n + 1)
.average()
.ifPresent(System.out::println); // 5.0
// Kotlin:
arrayOf(1,2,3).map { 2 * it + 1}.average().apply(::println)
不同类型的流#5
// Java:
Stream.of("a1", "a2", "a3")
.map(s -> s.substring(1))
.mapToInt(Integer::parseInt)
.max()
.ifPresent(System.out::println); // 3
// Kotlin:
sequenceOf("a1", "a2", "a3")
.map { it.substring(1) }
.map(String::toInt)
.max().apply(::println)
不同类型的流#6
// Java:
IntStream.range(1, 4)
.mapToObj(i -> "a" + i)
.forEach(System.out::println);
// a1
// a2
// a3
// Kotlin: (inclusive range)
(1..3).map { "a$it" }.forEach(::println)
不同类型的流 #7
// Java:
Stream.of(1.0, 2.0, 3.0)
.mapToInt(Double::intValue)
.mapToObj(i -> "a" + i)
.forEach(System.out::println);
// a1
// a2
// a3
// Kotlin:
sequenceOf(1.0, 2.0, 3.0).map(Double::toInt).map { "a$it" }.forEach(::println)
为什么顺序很重要
Java 8 Stream 教程的这一部分对于 Kotlin 和 Java 是相同的。
重用流
在Kotlin中,是否可以多次消费取决于collection的类型。 Sequence
每次都会生成一个新的迭代器,除非它断言“只使用一次”,否则它可以在每次执行时重置为开始。因此,虽然以下在 Java 8 流中失败,但在 Kotlin 中有效:
// Java:
Stream<String> stream =
Stream.of("d2", "a2", "b1", "b3", "c").filter(s -> s.startsWith("b"));
stream.anyMatch(s -> true); // ok
stream.noneMatch(s -> true); // exception
// Kotlin:
val stream = listOf("d2", "a2", "b1", "b3", "c").asSequence().filter { it.startsWith('b' ) }
stream.forEach(::println) // b1, b2
println("Any B ${stream.any { it.startsWith('b') }}") // Any B true
println("Any C ${stream.any { it.startsWith('c') }}") // Any C false
stream.forEach(::println) // b1, b2
并在 Java 中获得相同的行为:
// Java:
Supplier<Stream<String>> streamSupplier =
() -> Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> s.startsWith("a"));
streamSupplier.get().anyMatch(s -> true); // ok
streamSupplier.get().noneMatch(s -> true); // ok
因此,在 Kotlin 中,数据的提供者决定它是否可以重置并提供新的迭代器。但是,如果你想有意将 Sequence
限制为一次迭代,你可以使用 constrainOnce()
函数来实现 Sequence
,如下所示:
val stream = listOf("d2", "a2", "b1", "b3", "c").asSequence().filter { it.startsWith('b' ) }
.constrainOnce()
stream.forEach(::println) // b1, b2
stream.forEach(::println) // Error:java.lang.IllegalStateException: This sequence can be consumed only once.
高级操作
收集示例#5(是的,我已经跳过了其他答案中的那些)
// Java:
String phrase = persons
.stream()
.filter(p -> p.age >= 18)
.map(p -> p.name)
.collect(Collectors.joining(" and ", "In Germany ", " are of legal age."));
System.out.println(phrase);
// In Germany Max and Peter and Pamela are of legal age.
// Kotlin:
val phrase = persons.filter { it.age >= 18 }.map { it.name }
.joinToString(" and ", "In Germany ", " are of legal age.")
println(phrase)
// In Germany Max and Peter and Pamela are of legal age.
附带说明一下,在 Kotlin 中我们可以创建简单的 data classes 并实例化测试数据,如下所示:
// Kotlin:
// data class has equals, hashcode, toString, and copy methods automagically
data class Person(val name: String, val age: Int)
val persons = listOf(Person("Tod", 5), Person("Max", 33),
Person("Frank", 13), Person("Peter", 80),
Person("Pamela", 18))
收集示例#6
// Java:
Map<Integer, String> map = persons
.stream()
.collect(Collectors.toMap(
p -> p.age,
p -> p.name,
(name1, name2) -> name1 + ";" + name2));
System.out.println(map);
// {18=Max, 23=Peter;Pamela, 12=David}
好的,这里有一个更有趣的 Kotlin 案例。首先探索从 collection/sequence:
创建 Map
的变体的错误答案
// Kotlin:
val map1 = persons.map { it.age to it.name }.toMap()
println(map1)
// output: {18=Max, 23=Pamela, 12=David}
// Result: duplicates overridden, no exception similar to Java 8
val map2 = persons.toMap({ it.age }, { it.name })
println(map2)
// output: {18=Max, 23=Pamela, 12=David}
// Result: same as above, more verbose, duplicates overridden
val map3 = persons.toMapBy { it.age }
println(map3)
// output: {18=Person(name=Max, age=18), 23=Person(name=Pamela, age=23), 12=Person(name=David, age=12)}
// Result: duplicates overridden again
val map4 = persons.groupBy { it.age }
println(map4)
// output: {18=[Person(name=Max, age=18)], 23=[Person(name=Peter, age=23), Person(name=Pamela, age=23)], 12=[Person(name=David, age=12)]}
// Result: closer, but now have a Map<Int, List<Person>> instead of Map<Int, String>
val map5 = persons.groupBy { it.age }.mapValues { it.value.map { it.name } }
println(map5)
// output: {18=[Max], 23=[Peter, Pamela], 12=[David]}
// Result: closer, but now have a Map<Int, List<String>> instead of Map<Int, String>
现在为正确答案:
// Kotlin:
val map6 = persons.groupBy { it.age }.mapValues { it.value.joinToString(";") { it.name } }
println(map6)
// output: {18=Max, 23=Peter;Pamela, 12=David}
// Result: YAY!!
我们只需要加入匹配值来折叠列表并为 jointToString
提供转换器以从 Person
实例移动到 Person.name
。
收集示例#7
好的,无需自定义 Collector
即可轻松完成此操作,因此让我们以 Kotlin 的方式解决它,然后设计一个新示例来展示如何为 Collector.summarizingInt
执行类似的过程Kotlin 本身不存在。
// Java:
Collector<Person, StringJoiner, String> personNameCollector =
Collector.of(
() -> new StringJoiner(" | "), // supplier
(j, p) -> j.add(p.name.toUpperCase()), // accumulator
(j1, j2) -> j1.merge(j2), // combiner
StringJoiner::toString); // finisher
String names = persons
.stream()
.collect(personNameCollector);
System.out.println(names); // MAX | PETER | PAMELA | DAVID
// Kotlin:
val names = persons.map { it.name.toUpperCase() }.joinToString(" | ")
他们选择了一个微不足道的例子不是我的错!!!好的,这是 Kotlin 的新 summarizingInt
方法和匹配示例:
SummarizingInt 示例
// Java:
IntSummaryStatistics ageSummary =
persons.stream()
.collect(Collectors.summarizingInt(p -> p.age));
System.out.println(ageSummary);
// IntSummaryStatistics{count=4, sum=76, min=12, average=19.000000, max=23}
// Kotlin:
// something to hold the stats...
data class SummaryStatisticsInt(var count: Int = 0,
var sum: Int = 0,
var min: Int = Int.MAX_VALUE,
var max: Int = Int.MIN_VALUE,
var avg: Double = 0.0) {
fun accumulate(newInt: Int): SummaryStatisticsInt {
count++
sum += newInt
min = min.coerceAtMost(newInt)
max = max.coerceAtLeast(newInt)
avg = sum.toDouble() / count
return this
}
}
// Now manually doing a fold, since Stream.collect is really just a fold
val stats = persons.fold(SummaryStatisticsInt()) { stats, person -> stats.accumulate(person.age) }
println(stats)
// output: SummaryStatisticsInt(count=4, sum=76, min=12, max=23, avg=19.0)
但最好创建一个扩展函数,2 实际上是为了匹配 Kotlin stdlib 中的样式:
// Kotlin:
inline fun Collection<Int>.summarizingInt(): SummaryStatisticsInt
= this.fold(SummaryStatisticsInt()) { stats, num -> stats.accumulate(num) }
inline fun <T: Any> Collection<T>.summarizingInt(transform: (T)->Int): SummaryStatisticsInt =
this.fold(SummaryStatisticsInt()) { stats, item -> stats.accumulate(transform(item)) }
现在您可以通过两种方式使用新的 summarizingInt
功能:
val stats2 = persons.map { it.age }.summarizingInt()
// or
val stats3 = persons.summarizingInt { it.age }
所有这些都会产生相同的结果。我们还可以创建这个扩展来处理 Sequence
和适当的原始类型。
为了好玩,compare the Java JDK code vs. Kotlin custom code 需要实现此摘要。
有些情况下很难避免调用 collect(Collectors.toList())
或类似的方法。在这些情况下,您可以使用扩展函数更快速地更改为 Kotlin 等价物,例如:
fun <T: Any> Stream<T>.toList(): List<T> = this.collect(Collectors.toList<T>())
fun <T: Any> Stream<T>.asSequence(): Sequence<T> = this.iterator().asSequence()
然后您可以简单地 stream.toList()
或 stream.asSequence()
返回 Kotlin API。 Files.list(path)
之类的情况会在您不想要时强制您进入 Stream
,这些扩展可以帮助您转回标准集合和 Kotlin API.
更多关于懒惰
让我们以 Jayson 给出的 "Compute sum of salaries by department" 的示例解决方案为例:
val totalByDept = employees.groupBy { it.dept }.mapValues { it.value.sumBy { it.salary }}
为了使其变得惰性(即避免在 groupBy
步骤中创建中间映射),无法使用 asSequence()
。相反,我们必须使用 groupingBy
和 fold
操作:
val totalByDept = employees.groupingBy { it.dept }.fold(0) { acc, e -> acc + e.salary }
对于某些人来说,这甚至可能更具可读性,因为您不是在处理地图条目:解决方案中的 it.value
部分起初也让我感到困惑。
因为这是一个常见的情况,我们不想每次都写出 fold
,最好只在 Grouping
上提供一个通用的 sumBy
函数:
public inline fun <T, K> Grouping<T, K>.sumBy(
selector: (T) -> Int
): Map<K, Int> =
fold(0) { acc, element -> acc + selector(element) }
这样我们就可以简单地写成:
val totalByDept = employees.groupingBy { it.dept }.sumBy { it.salary }
在 Java 8 中,有 Stream.collect
允许对集合进行聚合。在 Kotlin 中,这并不以相同的方式存在,除了可能作为 stdlib 中的扩展函数的集合。但尚不清楚不同用例的等价物是什么。
例如,在 top of the JavaDoc for Collectors
处是为 Java 8 编写的示例,当将它们移植到 Kolin 时,您不能使用 Java 8 类在不同的 JDK 版本上,它们可能应该以不同的方式编写。
就显示 Kotlin 集合示例的在线资源而言,它们通常微不足道,无法真正与相同的用例进行比较。什么是真正匹配 Java 8 Stream.collect
记录的案例的好例子?那里的名单是:
- 将名称累积到列表中
- 将名称累积到 TreeSet 中
- 将元素转换为字符串并连接它们,以逗号分隔
- 计算员工的工资总和
- 按部门对员工进行分组
- 按部门计算工资总和
- 将学生分为合格和不合格
在上面链接的 Java 文档中有详细信息。
注: 此题为作者(Self-Answered Questions)特意写下并回答,以便常见的 Kotlin 主题的惯用答案出现在 SO 中。还要澄清一些为 Kotlin 的 alpha 编写的非常古老的答案,这些答案对于当今的 Kotlin 来说并不准确。
Kotlin stdlib 中有用于平均、计数、不同、过滤、查找、分组、连接、映射、最小值、最大值、分区、切片、排序、求和、to/from 数组的函数,to/from 列表、to/from 映射、联合、co-iteration、所有函数范式等等。因此,您可以使用它们来创建小的 1 行,而无需使用更复杂的语法 Java 8.
我认为 built-in Java 8 Collectors
class 中唯一缺少的是总结(但在
两者都缺少一件事是按计数进行批处理,这在 Stream.collect
for another purpose, see
编辑 2017 年 8 月 11 日: Chunked/windowed 在 kotlin 1.2 M2 中添加了收集操作,请参阅 https://blog.jetbrains.com/kotlin/2017/08/kotlin-1-2-m2-is-out/
在创建可能已经存在的新功能之前,先探索整个 API Reference for kotlin.collections 总是好的。
以下是从 Java 8 Stream.collect
个示例到 Kotlin 中等效项的一些转换:
将名称累加到列表中
// Java:
List<String> list = people.stream().map(Person::getName).collect(Collectors.toList());
// Kotlin:
val list = people.map { it.name } // toList() not needed
将元素转换为字符串并连接它们,以逗号分隔
// Java:
String joined = things.stream()
.map(Object::toString)
.collect(Collectors.joining(", "));
// Kotlin:
val joined = things.joinToString(", ")
计算雇员的工资总和
// Java:
int total = employees.stream()
.collect(Collectors.summingInt(Employee::getSalary)));
// Kotlin:
val total = employees.sumBy { it.salary }
按部门对员工进行分组
// Java:
Map<Department, List<Employee>> byDept
= employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
// Kotlin:
val byDept = employees.groupBy { it.department }
按部门计算工资总和
// Java:
Map<Department, Integer> totalByDept
= employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment,
Collectors.summingInt(Employee::getSalary)));
// Kotlin:
val totalByDept = employees.groupBy { it.dept }.mapValues { it.value.sumBy { it.salary }}
将学生分为合格和不合格
// Java:
Map<Boolean, List<Student>> passingFailing =
students.stream()
.collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));
// Kotlin:
val passingFailing = students.partition { it.grade >= PASS_THRESHOLD }
男成员姓名
// Java:
List<String> namesOfMaleMembers = roster
.stream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.map(p -> p.getName())
.collect(Collectors.toList());
// Kotlin:
val namesOfMaleMembers = roster.filter { it.gender == Person.Sex.MALE }.map { it.name }
名册中成员的群组名称(按性别)
// Java:
Map<Person.Sex, List<String>> namesByGender =
roster.stream().collect(
Collectors.groupingBy(
Person::getGender,
Collectors.mapping(
Person::getName,
Collectors.toList())));
// Kotlin:
val namesByGender = roster.groupBy { it.gender }.mapValues { it.value.map { it.name } }
将一个列表过滤到另一个列表
// Java:
List<String> filtered = items.stream()
.filter( item -> item.startsWith("o") )
.collect(Collectors.toList());
// Kotlin:
val filtered = items.filter { it.startsWith('o') }
寻找最短的字符串列表
// Java:
String shortest = items.stream()
.min(Comparator.comparing(item -> item.length()))
.get();
// Kotlin:
val shortest = items.minBy { it.length }
应用过滤器后对列表中的项目进行计数
// Java:
long count = items.stream().filter( item -> item.startsWith("t")).count();
// Kotlin:
val count = items.filter { it.startsWith('t') }.size
// but better to not filter, but count with a predicate
val count = items.count { it.startsWith('t') }
然后继续......在所有情况下,不需要特殊的折叠、减少或其他功能来模仿 Stream.collect
。如果您有更多的用例,请在评论中添加它们,我们会看到!
关于懒惰
如果你想惰性处理链,你可以在链前使用 asSequence()
转换为 Sequence
。在函数链的末尾,您通常也会以 Sequence
结尾。然后你可以使用 toList()
、toSet()
、toMap()
或其他函数来具体化最后的 Sequence
。
// switch to and from lazy
val someList = items.asSequence().filter { ... }.take(10).map { ... }.toList()
// switch to lazy, but sorted() brings us out again at the end
val someList = items.asSequence().filter { ... }.take(10).map { ... }.sorted()
为什么没有类型?!?
您会注意到 Kotlin 示例没有指定类型。这是因为 Kotlin 具有完整的类型推断并且在编译时是完全类型安全的。比 Java 更重要,因为它还具有可为空的类型,并且可以帮助防止可怕的 NPE。所以在 Kotlin 中是这样的:
val someList = people.filter { it.age <= 30 }.map { it.name }
等同于:
val someList: List<String> = people.filter { it.age <= 30 }.map { it.name }
因为 Kotlin 知道 people
是什么,而 people.age
是 Int
,因此过滤器表达式只允许与 Int
进行比较,而 people.name
是一个 String
,因此 map
步骤产生一个 List<String>
(String
的只读 List
)。
现在,如果 people
可能是 null
,as-in 一个 List<People>?
那么:
val someList = people?.filter { it.age <= 30 }?.map { it.name }
Returns 需要进行空值检查的 List<String>?
( 或对可空值使用其他 Kotlin 运算符之一,请参阅此
另请参阅:
- API 参考 extension functions for Iterable
- API 参考 extension functions for Array
- API 参考 extension functions for List
- API 参考 extension functions to Map
有关其他示例,这里是 Java 8 Stream Tutorial 中转换为 Kotlin 的所有示例。每个例子的标题,来源于文章来源:
流的工作原理
// Java:
List<String> myList = Arrays.asList("a1", "a2", "b1", "c2", "c1");
myList.stream()
.filter(s -> s.startsWith("c"))
.map(String::toUpperCase)
.sorted()
.forEach(System.out::println);
// C1
// C2
// Kotlin:
val list = listOf("a1", "a2", "b1", "c2", "c1")
list.filter { it.startsWith('c') }.map (String::toUpperCase).sorted()
.forEach (::println)
不同类型的流#1
// Java:
Arrays.asList("a1", "a2", "a3")
.stream()
.findFirst()
.ifPresent(System.out::println);
// Kotlin:
listOf("a1", "a2", "a3").firstOrNull()?.apply(::println)
或者,在名为 ifPresent 的字符串上创建一个扩展函数:
// Kotlin:
inline fun String?.ifPresent(thenDo: (String)->Unit) = this?.apply { thenDo(this) }
// now use the new extension function:
listOf("a1", "a2", "a3").firstOrNull().ifPresent(::println)
另请参阅:apply()
function
另请参阅:Extension Functions
另请参阅:?.
Safe Call operator, and in general nullability: In Kotlin, what is the idiomatic way to deal with nullable values, referencing or converting them
不同类型的流 #2
// Java:
Stream.of("a1", "a2", "a3")
.findFirst()
.ifPresent(System.out::println);
// Kotlin:
sequenceOf("a1", "a2", "a3").firstOrNull()?.apply(::println)
不同类型的流 #3
// Java:
IntStream.range(1, 4).forEach(System.out::println);
// Kotlin: (inclusive range)
(1..3).forEach(::println)
不同类型的流 #4
// Java:
Arrays.stream(new int[] {1, 2, 3})
.map(n -> 2 * n + 1)
.average()
.ifPresent(System.out::println); // 5.0
// Kotlin:
arrayOf(1,2,3).map { 2 * it + 1}.average().apply(::println)
不同类型的流#5
// Java:
Stream.of("a1", "a2", "a3")
.map(s -> s.substring(1))
.mapToInt(Integer::parseInt)
.max()
.ifPresent(System.out::println); // 3
// Kotlin:
sequenceOf("a1", "a2", "a3")
.map { it.substring(1) }
.map(String::toInt)
.max().apply(::println)
不同类型的流#6
// Java:
IntStream.range(1, 4)
.mapToObj(i -> "a" + i)
.forEach(System.out::println);
// a1
// a2
// a3
// Kotlin: (inclusive range)
(1..3).map { "a$it" }.forEach(::println)
不同类型的流 #7
// Java:
Stream.of(1.0, 2.0, 3.0)
.mapToInt(Double::intValue)
.mapToObj(i -> "a" + i)
.forEach(System.out::println);
// a1
// a2
// a3
// Kotlin:
sequenceOf(1.0, 2.0, 3.0).map(Double::toInt).map { "a$it" }.forEach(::println)
为什么顺序很重要
Java 8 Stream 教程的这一部分对于 Kotlin 和 Java 是相同的。
重用流
在Kotlin中,是否可以多次消费取决于collection的类型。 Sequence
每次都会生成一个新的迭代器,除非它断言“只使用一次”,否则它可以在每次执行时重置为开始。因此,虽然以下在 Java 8 流中失败,但在 Kotlin 中有效:
// Java:
Stream<String> stream =
Stream.of("d2", "a2", "b1", "b3", "c").filter(s -> s.startsWith("b"));
stream.anyMatch(s -> true); // ok
stream.noneMatch(s -> true); // exception
// Kotlin:
val stream = listOf("d2", "a2", "b1", "b3", "c").asSequence().filter { it.startsWith('b' ) }
stream.forEach(::println) // b1, b2
println("Any B ${stream.any { it.startsWith('b') }}") // Any B true
println("Any C ${stream.any { it.startsWith('c') }}") // Any C false
stream.forEach(::println) // b1, b2
并在 Java 中获得相同的行为:
// Java:
Supplier<Stream<String>> streamSupplier =
() -> Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> s.startsWith("a"));
streamSupplier.get().anyMatch(s -> true); // ok
streamSupplier.get().noneMatch(s -> true); // ok
因此,在 Kotlin 中,数据的提供者决定它是否可以重置并提供新的迭代器。但是,如果你想有意将 Sequence
限制为一次迭代,你可以使用 constrainOnce()
函数来实现 Sequence
,如下所示:
val stream = listOf("d2", "a2", "b1", "b3", "c").asSequence().filter { it.startsWith('b' ) }
.constrainOnce()
stream.forEach(::println) // b1, b2
stream.forEach(::println) // Error:java.lang.IllegalStateException: This sequence can be consumed only once.
高级操作
收集示例#5(是的,我已经跳过了其他答案中的那些)
// Java:
String phrase = persons
.stream()
.filter(p -> p.age >= 18)
.map(p -> p.name)
.collect(Collectors.joining(" and ", "In Germany ", " are of legal age."));
System.out.println(phrase);
// In Germany Max and Peter and Pamela are of legal age.
// Kotlin:
val phrase = persons.filter { it.age >= 18 }.map { it.name }
.joinToString(" and ", "In Germany ", " are of legal age.")
println(phrase)
// In Germany Max and Peter and Pamela are of legal age.
附带说明一下,在 Kotlin 中我们可以创建简单的 data classes 并实例化测试数据,如下所示:
// Kotlin:
// data class has equals, hashcode, toString, and copy methods automagically
data class Person(val name: String, val age: Int)
val persons = listOf(Person("Tod", 5), Person("Max", 33),
Person("Frank", 13), Person("Peter", 80),
Person("Pamela", 18))
收集示例#6
// Java:
Map<Integer, String> map = persons
.stream()
.collect(Collectors.toMap(
p -> p.age,
p -> p.name,
(name1, name2) -> name1 + ";" + name2));
System.out.println(map);
// {18=Max, 23=Peter;Pamela, 12=David}
好的,这里有一个更有趣的 Kotlin 案例。首先探索从 collection/sequence:
创建Map
的变体的错误答案
// Kotlin:
val map1 = persons.map { it.age to it.name }.toMap()
println(map1)
// output: {18=Max, 23=Pamela, 12=David}
// Result: duplicates overridden, no exception similar to Java 8
val map2 = persons.toMap({ it.age }, { it.name })
println(map2)
// output: {18=Max, 23=Pamela, 12=David}
// Result: same as above, more verbose, duplicates overridden
val map3 = persons.toMapBy { it.age }
println(map3)
// output: {18=Person(name=Max, age=18), 23=Person(name=Pamela, age=23), 12=Person(name=David, age=12)}
// Result: duplicates overridden again
val map4 = persons.groupBy { it.age }
println(map4)
// output: {18=[Person(name=Max, age=18)], 23=[Person(name=Peter, age=23), Person(name=Pamela, age=23)], 12=[Person(name=David, age=12)]}
// Result: closer, but now have a Map<Int, List<Person>> instead of Map<Int, String>
val map5 = persons.groupBy { it.age }.mapValues { it.value.map { it.name } }
println(map5)
// output: {18=[Max], 23=[Peter, Pamela], 12=[David]}
// Result: closer, but now have a Map<Int, List<String>> instead of Map<Int, String>
现在为正确答案:
// Kotlin:
val map6 = persons.groupBy { it.age }.mapValues { it.value.joinToString(";") { it.name } }
println(map6)
// output: {18=Max, 23=Peter;Pamela, 12=David}
// Result: YAY!!
我们只需要加入匹配值来折叠列表并为 jointToString
提供转换器以从 Person
实例移动到 Person.name
。
收集示例#7
好的,无需自定义 Collector
即可轻松完成此操作,因此让我们以 Kotlin 的方式解决它,然后设计一个新示例来展示如何为 Collector.summarizingInt
执行类似的过程Kotlin 本身不存在。
// Java:
Collector<Person, StringJoiner, String> personNameCollector =
Collector.of(
() -> new StringJoiner(" | "), // supplier
(j, p) -> j.add(p.name.toUpperCase()), // accumulator
(j1, j2) -> j1.merge(j2), // combiner
StringJoiner::toString); // finisher
String names = persons
.stream()
.collect(personNameCollector);
System.out.println(names); // MAX | PETER | PAMELA | DAVID
// Kotlin:
val names = persons.map { it.name.toUpperCase() }.joinToString(" | ")
他们选择了一个微不足道的例子不是我的错!!!好的,这是 Kotlin 的新 summarizingInt
方法和匹配示例:
SummarizingInt 示例
// Java:
IntSummaryStatistics ageSummary =
persons.stream()
.collect(Collectors.summarizingInt(p -> p.age));
System.out.println(ageSummary);
// IntSummaryStatistics{count=4, sum=76, min=12, average=19.000000, max=23}
// Kotlin:
// something to hold the stats...
data class SummaryStatisticsInt(var count: Int = 0,
var sum: Int = 0,
var min: Int = Int.MAX_VALUE,
var max: Int = Int.MIN_VALUE,
var avg: Double = 0.0) {
fun accumulate(newInt: Int): SummaryStatisticsInt {
count++
sum += newInt
min = min.coerceAtMost(newInt)
max = max.coerceAtLeast(newInt)
avg = sum.toDouble() / count
return this
}
}
// Now manually doing a fold, since Stream.collect is really just a fold
val stats = persons.fold(SummaryStatisticsInt()) { stats, person -> stats.accumulate(person.age) }
println(stats)
// output: SummaryStatisticsInt(count=4, sum=76, min=12, max=23, avg=19.0)
但最好创建一个扩展函数,2 实际上是为了匹配 Kotlin stdlib 中的样式:
// Kotlin:
inline fun Collection<Int>.summarizingInt(): SummaryStatisticsInt
= this.fold(SummaryStatisticsInt()) { stats, num -> stats.accumulate(num) }
inline fun <T: Any> Collection<T>.summarizingInt(transform: (T)->Int): SummaryStatisticsInt =
this.fold(SummaryStatisticsInt()) { stats, item -> stats.accumulate(transform(item)) }
现在您可以通过两种方式使用新的 summarizingInt
功能:
val stats2 = persons.map { it.age }.summarizingInt()
// or
val stats3 = persons.summarizingInt { it.age }
所有这些都会产生相同的结果。我们还可以创建这个扩展来处理 Sequence
和适当的原始类型。
为了好玩,compare the Java JDK code vs. Kotlin custom code 需要实现此摘要。
有些情况下很难避免调用 collect(Collectors.toList())
或类似的方法。在这些情况下,您可以使用扩展函数更快速地更改为 Kotlin 等价物,例如:
fun <T: Any> Stream<T>.toList(): List<T> = this.collect(Collectors.toList<T>())
fun <T: Any> Stream<T>.asSequence(): Sequence<T> = this.iterator().asSequence()
然后您可以简单地 stream.toList()
或 stream.asSequence()
返回 Kotlin API。 Files.list(path)
之类的情况会在您不想要时强制您进入 Stream
,这些扩展可以帮助您转回标准集合和 Kotlin API.
更多关于懒惰
让我们以 Jayson 给出的 "Compute sum of salaries by department" 的示例解决方案为例:
val totalByDept = employees.groupBy { it.dept }.mapValues { it.value.sumBy { it.salary }}
为了使其变得惰性(即避免在 groupBy
步骤中创建中间映射),无法使用 asSequence()
。相反,我们必须使用 groupingBy
和 fold
操作:
val totalByDept = employees.groupingBy { it.dept }.fold(0) { acc, e -> acc + e.salary }
对于某些人来说,这甚至可能更具可读性,因为您不是在处理地图条目:解决方案中的 it.value
部分起初也让我感到困惑。
因为这是一个常见的情况,我们不想每次都写出 fold
,最好只在 Grouping
上提供一个通用的 sumBy
函数:
public inline fun <T, K> Grouping<T, K>.sumBy(
selector: (T) -> Int
): Map<K, Int> =
fold(0) { acc, element -> acc + selector(element) }
这样我们就可以简单地写成:
val totalByDept = employees.groupingBy { it.dept }.sumBy { it.salary }