什么Java 8 Stream.collect等价物在标准Kotlin库中可用?

在Java 8中,有Stream.collect ,它允许在集合上进行聚合。 在Kotlin中,这不是以同样的方式存在,除了可能作为stdlib中扩展函数的集合。 但是不清楚不同用例的等价关系是什么。

例如, JavaDoc for Collectors的顶部是为Java 8编写的示例,将它们移植到Kolin时,不能在不同的JDK版本上使用Java 8类,因此可能应该使用不同的方式编写。

就在线资源展示Kotlin集合的例子而言,它们通常是微不足道的,并没有真正与相同的用例相比较。 什么是真正符合Java 8 Stream.collect记录的案例的好例子? 这里的列表是:

  • 将名字积累到列表中
  • 将名称累加到TreeSet中
  • 将元素转换为字符串并连接它们,用逗号分隔
  • 计算员工的工资总额
  • 按部门分组员工
  • 按部门计算工资总额
  • 将学生划分为通过和失败

上面链接了JavaDoc中的细节。

注意: 这个问题是由作者故意写的和回答的( 自我回答的问题 ),所以对于常见的Kotlin话题的习惯性的回答是在SO中。 此外,为了澄清一些真正的古老的答案写为科特林的阿尔法,是不是今天的Kotlin准确。

在Kotlin stdlib中有函数用于平均值,计数,清除,过滤,查找,分组,加入,映射,最小值,最大值,分割,切片,排序,求和,从数组到/从列表,到/从地图,联合,共同迭代,所有的功能范例,等等。 所以,你可以使用这些来创建一个1行,而不需要使用更复杂的Java 8语法。

我认为从内置的Java 8 Collectors类中唯一缺少的就是汇总(但是在这个问题的另一个答案是一个简单的解决方案)

有一件事从两者都缺少批处理计数,这是在另一个堆栈溢出答案中看到,也有一个简单的答案。 另外一个有趣的例子就是Stack Overflow中的这个例子: 使用Kotlin将序列溢出到三个列表中的习惯用法 。 如果您想为其他目的创建类似Stream.collect的内容,请参阅Kotlin中的自定义Stream.collect

编辑11.08.2017:加入kotlin 1.2 M2的分块/窗口收集操作,请参阅https://blog.jetbrains.com/kotlin/2017/08/kotlin-1-2-m2-is-out/


在创建可能已经存在的新函数之前,总体上探索kotlin.collections的API参考是一件好事。

以下是从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.ageInt因此过滤器表达式只允许与Int进行比较,并且people.name是一个String因此map步骤会生成一个List<String> (只读String List )。

现在,如果people可能为null ,那么在List<People>? 然后:

 val someList = people?.filter { it.age <= 30 }?.map { it.name } 

返回一个List<String>? 那么需要对它进行空值检查( 或者使用其他Kotlin运算符中的一个作为可为空的值,请参阅Kotlin惯用的处理可空值的 方法以及在Kotlin中处理可空或空列表的习惯方式

也可以看看:

  • Iterable扩展函数的 API参考
  • Array的扩展函数的 API参考
  • List的扩展函数的 API参考
  • 将扩展函数映射到 API的API参考

有关其他示例,以下是转换为Kotlin的Java 8 Stream Tutorial中的所有示例。 每个例子的标题来自源文章:

流如何工作

 // 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) 

或者,在String上创建一个名为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()函数

另请参阅: 扩展功能

另见: ?. 安全调用操作符 ,通常是可空的: 在Kotlin中,处理可空值的常用方法是什么,引用或转换它们

不同类型的流#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 Tutorial的这一部分与Kotlin和Java相同。

重用流

在Kotlin中,它取决于是否可以多次使用的收集类型。 一个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一次迭代,你可以使用Sequence constrainOnce()函数,如下所示:

 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中,我们可以创建简单的数据类并实例化测试数据,如下所示:

 // 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的一个更有趣的案例。 首先是错误的答案,以探索从集合/序列创建一个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!! 

我们只需要加入匹配的值来折叠列表,并提供一个转换器,将jointToStringPerson实例移动到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) 

但是最好是创建一个扩展函数,实际上是为了匹配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和适当的原始类型。

为了获得乐趣,请比较Java JDK代码和实现此汇总所需的Kotlin自定义代码 。

在某些情况下,很难避免调用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。