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

新手上路,请多包涵

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

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

就显示 Kotlin 集合示例的在线资源而言,它们通常是微不足道的,并不能真正与相同的用例进行比较。什么是真正匹配 Java 8 Stream.collect 记录的案例的好例子?那里的清单是:

  • 将名称累积到列表中
  • 将名称累积到 TreeSet 中
  • 将元素转换为字符串并连接它们,以逗号分隔
  • 计算雇员的工资总和
  • 按部门分组员工
  • 按部门计算工资总和
  • 将学生分为合格和不合格

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

注意: 这个问题是作者有意编写和回答的( 答题),因此常见的 Kotlin 主题的惯用答案出现在 SO 中。还要澄清一些为 Kotlin alpha 编写的非常古老的答案,这些答案对于当今的 Kotlin 来说并不准确。

原文由 Jayson Minard 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 672
2 个回答

Kotlin stdlib 中有用于平均、计数、不同、过滤、查找、分组、连接、映射、最小值、最大值、分区、切片、排序、求和、到/从数组、到/从列表、到/从映射的函数、联合、协同迭代、所有功能范式等等。因此,您可以使用它们来创建小的 1-liner,而无需使用 Java 8 的更复杂语法。

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

两者都缺少一件事是按计数进行批处理,这在 另一个 Stack Overflow 答案 中可以看到,并且也有一个简单的答案。另一个有趣的案例也是来自 Stack Overflow 的案例: Idiomatic way to spilled sequence into three lists using Kotlin 。如果您想出于其他目的创建类似 Stream.collect 的内容,请参阅 Kotlin 中的 Custom Stream.collect

2017 年 8 月 11 日 编辑: 在 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 。 Then you can use toList() , toSet() , toMap() or some other function to materialize the Sequence at the end.

 // 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。所以在 Kotlin 中是这样的:

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

是相同的:

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

Because Kotlin knows what people is, and that people.age is Int therefore the filter expression only allows comparison to an Int , and that people.name is a String therefore the map step produces a List<String> (readonly List of String )。

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

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

返回一个 List<String>? 需要进行空检查( _或使用其他 Kotlin 运算符之一来获取可空值,请参阅此 Kotlin idiomatic way to deal with nullable values 以及 Idiomatic way of handling nullable or empty list in科特林_)

也可以看看:

原文由 Jayson Minard 发布,翻译遵循 CC BY-SA 4.0 许可协议

有关其他示例,这里是转换为 Kotlin 的 Java 8 Stream 教程 中的所有示例。每个示例的标题均来自源文章:

流是如何工作的

// 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() 函数

另见: 扩展函数

另见: ?. Safe Call operator 和一般的可空性: 在 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 教程的这一部分对于 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 中,数据提供者决定是否可以重置并提供新的迭代器。但是,如果您想有意将 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 中,我们可以创建简单的 数据类 并实例化测试数据,如下所示:

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

他们选择了一个微不足道的例子不是我的错!!! 好的,这是一个新的 summarizingInt Kotlin 方法和一个匹配示例:

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 自定义代码

原文由 Jayson Minard 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
logo
Stack Overflow 翻译
子站问答
访问
宣传栏