什么Java 8流。收集等价物可在标准Kotlin库?

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

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

就在线显示Kotlin集合示例的资源而言,它们通常是微不足道的,不能真正与相同的用例进行比较。有哪些好的例子能够真正匹配Java 8 Stream.collect所描述的情况?列表如下:

  • 将名字积累到一个列表中
  • 将名称累积到树集中
  • 将元素转换为字符串并连接它们,用逗号分隔
  • 计算员工的工资总额
  • 按部门分组员工
  • 按部门计算工资总额
  • 把学生分成及格和不及格两类

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

< em >注意:< / em > 这个问题是作者有意提出并回答的(自我回答的问题),这样常见的Kotlin主题的惯用答案就出现在so中。同时也要澄清一些为Kotlin alpha编写的旧答案,这些答案对于现在的Kotlin来说并不准确。

55059 次浏览

在Kotlin标准库中有用于平均、计数、区分、过滤、查找、分组、连接、映射、最小值、最大值、分区、切片、排序、求和、到/从数组、到/从列表、到/从映射、联合、共迭代、所有函数范式等等的函数。因此,您可以使用它们来创建小的一行程序,而不需要使用Java 8中更复杂的语法。

<年代> 我认为内置Java 8 Collectors类中唯一缺少的是摘要(但在中,这个问题的另一个答案是一个简单的解决方案)

两者都缺少的一件事是按计数批处理,这在另一个堆栈溢出的答案中可以看到,并且也有一个简单的答案。另一个有趣的例子也是来自Stack Overflow: 使用Kotlin将序列分成三个列表的惯用方法。如果你想为其他目的创建类似Stream.collect的对象,请参见自定义流。在Kotlin收集

编辑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.nameString,因此map步骤产生一个List<String> (String的只读List)。

现在,如果people可能是null,就像在List<People>?中一样,那么:

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

返回一个需要空检查的List<String>? (或使用其他Kotlin操作符来处理可空值,参见Kotlin处理可空值的惯用方法 Kotlin中处理可空或空列表的惯用方法)

参见:

对于其他示例,这里是所有从Java 8流教程转换到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)

不同类型的流

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

不同类型的流

// 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中,是否可以多次使用集合取决于集合的类型。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!!

我们只需要连接匹配的值来折叠列表,并为jointToString提供一个转换器,以从Person实例移动到Person.name实例。

收集示例#7

好吧,这个不需要自定义Collector就可以很容易地完成,所以让我们用Kotlin的方式来解决它,然后设计一个新例子,展示如何为Kotlin中不存在的Collector.summarizingInt执行类似的过程。

// 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和适当的基元类型。

为了好玩,比较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。

更多关于懒惰

让我们以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,因此在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 }