标准Kotlin库中提供了哪些Java 8 Stream.collect等效项?


180

在Java 8中,Stream.collect它允许对集合进行聚合。在Kotlin中,除了以stdlib中的扩展功能的集合形式存在之外,这并不完全相同。但尚不清楚不同用例的等效性。

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

在网上显示Kotlin集合示例的资源方面,它们通常是微不足道的,不能真正与相同的用例进行比较。有什么真正与案例相匹配的好示例,例如针对Java 8记录的案例Stream.collect?列表是:

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

在上面链接的JavaDoc中有详细信息。

注意: 这个问题是作者故意写和回答的(“ 自我回答的问题”),因此SO中经常出现Kotlin主题的惯用答案。还要澄清一些针对Kotlin字母的非常古老的答案,这些答案对于当今的Kotlin而言并不准确。


如果您别无选择,只能使用collect(Collectors.toList())或类似的方法,您可能会遇到以下问题: stackoverflow.com/a/35722167/3679676(该问题,以及解决方法)
Jayson Minard,2016年

Answers:


256

Kotlin stdlib中有一些函数用于平均值,计数,不同,过滤,查找,分组,联接,映射,最小,最大值,分区,切片,排序,求和,与数组之间,列表与列表之间或地图之间,联合,共同迭代,所有功能范例等。因此,您可以使用它们来创建少量的1-liners,而无需使用Java 8的更复杂的语法。

我认为内置Java 8 Collectors类唯一缺少的是摘要(但是对此问题的另一个答案是一个简单的解决方案)

两者缺少的一件事是按计数进行批处理,这在另一个堆栈溢出答案中也可以看到,并且也有一个简单的答案。另一个有趣的情况是来自Stack Overflow的一种情况:使用Kotlin的惯用方式将序列溢出到三个列表中。如果要创建Stream.collect其他用途,请参见Kotlin中的Custom Stream.collect

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


在创建可能已经存在的新功能之前,最好先整体探讨kotlin.collectionsAPI参考

这是从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。如果您还有其他用例,请在注释中添加它们,我们可以看到!

关于懒惰

如果要延迟处理链,可以在链之前转换为Sequence使用asSequence()。在功能链的最后,您通常也以一个结尾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更是如此,因为它也具有可为null的类型,并且可以帮助防止可怕的NPE。所以在科特林:

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

是相同的:

val someList: List<String> = people.filter { it.age <= 30 }.map { it.name }

因为科特林知道什么people是,这people.ageInt因此,过滤器表达式只允许比较的Int,那people.name是一个String因此map步骤产生List<String>(只读ListString)。

现在,如果people是可能的null,因为,在一List<People>?则:

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

返回List<String>?需要进行空检查的a(或将其他Kotlin运算符之一用于可空值,请参阅此Kotlin惯用方式来处理可空值,以及用于处理Kotlin中可空或空列表的惯用方式

也可以看看:


Kotlin中有与Java8的parallelStream()等效的东西吗?
阿纳布'16

关于不可变集合和Kotlin的答案与@arnab相同,这里是并行的,存在其他库,请使用它们:stackoverflow.com/a/34476880/3679676
Jayson Minard

2
@arnab您可能想看看Kotlin对Java 7/8功能的支持(特别是kotlinx-support-jdk8),该功能已于今年早些时候提供:describe.kotlinlang.org/t/jdk7-8-features-in -kotlin-1-0 / 1625
漫游

在一条语句中使用3个不同的“ it”引用真的很习惯吗?
赫尔曼

2
这是一种偏爱,在上面的示例中,我将其简短,仅在必要时为参数提供本地名称。
Jayson Minard '18

47

对于其他示例,这是从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的String上创建扩展函数:

// 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流教程的这一部分与Kotlin和Java相同。

重用流

在Kotlin中,是否可以消费一次以上取决于集合的类型。A 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中,数据提供者决定是否可以重置并提供新的迭代器。但是,如果要有意将a约束Sequence为一次迭代,则可以按如下所示使用constrainOnce()function 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中,我们可以创建简单的数据类并实例化测试数据,如下所示:

// 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.summarizingIntKotlin本身不存在的类似过程。

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

他们选了一个小例子,这不是我的错! 好的,这是summarizingIntKotlin的一种新方法和一个匹配的示例:

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适当的原始类型上工作并适用于它们。

有趣的是,实现此摘要所需的Java JDK代码与Kotlin自定义代码进行比较


在流5中,使用两个映射而不是一个映射没有任何.map { it.substring(1).toInt() }好处:众所周知,推断类型是kotlin的力量之一。
米歇尔·德·阿米科

是的,但也没有缺点(出于可比较性,我将它们分开)
Jayson Minard

但是Java代码可以很容易地并行化,因此在许多情况下,最好从Kotlin调用Java流代码。
霍华德·洛瓦特

@HowardLovatt在许多情况下,并行是无法实现的,特别是在繁重的并发环境中,您已经在线程池中。我敢打赌,平均用例不是并行的,这是罕见的情况。但是,当然,您总是可以选择使用合适的Java类,而这并不是本问答的真正目的。
杰森·米纳德

3

在某些情况下,很难避免拨打电话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。


2

更多关于懒惰

让我们以Jayson给出的“按部门计算工资总额”的示例解决方案为例:

val totalByDept = employees.groupBy { it.dept }.mapValues { it.value.sumBy { it.salary }}

为了使这个懒惰(即,避免在该groupBy步骤中创建中间映射),无法使用asSequence()。相反,我们必须使用groupingByfold操作:

val totalByDept = employees.groupingBy { it.dept }.fold(0) { acc, e -> acc + e.salary }

对于某些人来说,这甚至可能更具可读性,因为您无需处理地图条目:it.value解决方案中的部分一开始也让我感到困惑。

由于这是常见的情况,我们不希望fold每次都写出来,因此最好在上提供通用sumBy函数Grouping

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 }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.