标准Kotlin库中有哪些Java8Stream.collect等价物可用?

bfrts1fy  于 2023-02-07  发布在  Java
关注(0)|答案(4)|浏览(138)

在Java 8中,有Stream.collect允许集合上的聚合。在Kotlin中,这不是以同样的方式存在的,除了可能作为stdlib中的扩展函数的集合。但是不清楚不同用例的等价物是什么。
例如,top of the JavaDoc for Collectors是为Java8编写的示例,当将它们移植到Kolin时,您无法在不同的JDK版本上使用Java8类,因此它们可能应该以不同的方式编写。
在线资源显示了Kotlin集合的例子,它们通常是微不足道的,不能真正与相同的用例进行比较。什么是真正符合Java 8 Stream.collect文档中的用例的好例子?

  • 将名称累积到列表中
  • 将名称累积到树集中
  • 将元素转换为字符串并将其串联,以逗号分隔
  • 计算员工的工资总额
  • 按部门对员工分组
  • 按部门计算工资总额
  • 把学生分为及格和不及格

详细信息请参见上面链接的JavaDoc。

***注:***此问题是作者(Self-Answered Questions)有意编写和回答的,以便SO中出现Kotlin常见主题的惯用答案。同时澄清一些为Kotlin α编写的非常古老的答案,这些答案对当前的Kotlin不准确。

eiee3dmh

eiee3dmh1#

在某些情况下,很难避免调用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()重新使用KotlinAPI,例如Files.list(path)会在您不需要时强制您使用Stream,这些扩展可以帮助您重新使用标准集合和Kotlin API。

ljsrvy3e

ljsrvy3e2#

更多关于懒惰

我们以Jayson给出的“按部门计算工资总额”为例:

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

为了使这个操作变得懒惰(即避免在groupBy步骤中创建中间Map),不可能使用asSequence(),而是必须使用groupingByfold操作:

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

对于某些人来说,这可能更容易理解,因为您不需要处理Map条目:解决方案中的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 }
lg40wkob

lg40wkob3#

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

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

在创建可能已经存在于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:
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 }
    • 男性成员姓名**

一个12b1x一个13b1x

    • 按性别分列的名册中成员的姓名**
// 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 } }
    • 将列表筛选为另一个列表**

一个16b1x一个17b1x

    • 在列表中查找最短字符串**

一个18块一个19块一个

    • 应用筛选后计算列表中的项目数**
// 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 idiomatic way to deal with nullable valuesIdiomatic way of handling nullable or empty list in Kotlin *)

另请参见:

4dbbbstv

4dbbbstv4#

下面是从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**

一个二个一个一个
或者,在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() function
另请参阅:Extension Functions
另请参见:?.安全调用运算符,以及一般可空性:在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**

一个一个一个一个

    • 不同类型的流#6**

一个一个三个一个一个

    • 不同类型的流#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 stream中失败,但在Kotlin中可以工作:
一个17块一个18块一个
在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**(是的,我跳过了其他答案中的示例)

一个21个1x一个22个1x
顺便说一句,在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更有趣的案例。首先是探索从集合/序列创建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执行类似的过程。
一个27块一个一个28块一个

  • 他们选择了一个微不足道的例子,这不是我的错!!! * 好了,这里是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和适当的原语类型。
为了好玩,compare the Java JDK code vs. Kotlin custom code需要实现这个总结。

相关问题