【问题标题】:Why is the Sequence in Kotlin named 'Sequence' as like Java Stream?为什么 Kotlin 中的 Sequence 和 Java Stream 一样命名为“Sequence”?
【发布时间】:2022-11-19 20:09:14
【问题描述】:

我现在研究了 Kotlin,并且对 Kotlin 中的 Sequence 被命名为“Sequence”的原因有疑问。 我听说 Java Stream 类似于 Kotlin Sequence。 从名称上区分 Kotlin Sequence 和 Java Stream 是否存在一些关键差异?

如果它存在这样的描述或链接,我很感激你们在下面回复。

【问题讨论】:

  • 因为它代表了一系列事物?你明白英文单词“sequence”的意思吗?
  • 或者您只是想问 Java Streams 和 Kotlin Sequences 之间的区别是什么?
  • 是的。 Kotlin Sequence 和 Java Stream 通常都有filter()map()flatMap() 等方法,这些看起来功能差不多,那为什么 Kotlin Sequence 不像 Java Stream 那样命名为 'Stream' 呢?
  • @ejjang2030 欢迎来到 Stackoverflow,我提供了一个答案,如果它符合您的要求,请考虑批准它作为答案,否则请随时添加评论。
  • 感谢您欢迎@Lunatic!我还没有使用 Stackoverflow。在这个Stackoverflow上做题几乎是第一次。

标签: java kotlin collections stream sequence


【解决方案1】:

您问

Java Streams 与 Kotlin Sequences

4个主题的确定性观点。

  • 零安全

在使用非原始值时,在 Kotlin 代码中使用 Java 流会产生平台类型。例如,以下计算结果为 List<Person!> 而不是 List,因此它变得不那么强类型:

people.stream()
    .filter { it.age > 18 }
    .toList() // evaluates to List<Person!>

在处理可能不存在的值时,序列返回可为空的类型,而流将结果包装在一个 Optional 中:

// Sequence
val nameOfAdultWithLongName = people.asSequence()
    ...
    .find { it.name.length > 5 }
    ?.name
// Stream
val nameOfAdultWithLongName = people.stream()
    ...
    .filter { it.name.length > 5 }
    .findAny()
    .get() // unsafe unwrapping of Optional
    .name

尽管我们可以在上面的示例中使用 orElse(null),但编译器不会强制我们正确使用 Optional 包装器。即使我们确实使用了orElse(null),结果值也将是一个平台类型,因此编译器不会强制安全使用。这种模式已经困扰了我们好几次,因为如果找不到人,流版本将抛出运行时异常。然而,序列使用 Kotlin 可空类型,因此在编译时强制安全使用。因此,从空安全的角度来看,序列更安全。

  • 原语处理

尽管 Kotlin 没有在其类型系统中公开原始类型,但它会尽可能在幕后使用原始类型。例如,可为空的 Double(Double?)在幕后存储为 java.lang.Double,而不可为 null 的 Double 在可能的情况下存储为原始 double。

流具有避免自动装箱的原始变体,但序列没有:

// Sequence
people.asSequence()
    .map { it.weight } // Autobox non-nullable Double
    ...
// Stream
people.stream()
    .mapToDouble { it.weight } // DoubleStream from here onwards
    ...

但是,如果我们将它们捕获到一个集合中,那么它们将无论如何都会被自动装箱,因为通用集合存储引用。此外,如果您已经在处理装箱值,将它们拆箱并收集到另一个列表中比传递装箱引用更糟糕,因此原始流在过度使用时可能是有害的:

// Stream
val testScores = people.stream()
   .filter { it.testScore != null }
   .mapToDouble { it.testScore!! } // Very bad! Use map { ... }
   .toList() // Unnecessary autoboxing because we unboxed them

尽管序列没有原始变体,但它们通过包含实用程序来简化常见操作,从而避免了一些自动装箱。例如,我们可以使用 sumByDouble 而不是需要映射值,然后将其作为单独的步骤求和。这些减少了自动装箱并简化了代码。

当自动装箱作为序列的结果发生时,这会导致非常有效的堆使用模式。序列(和流)通过所有序列操作传递每个元素,直到到达终端操作,然后再移动到下一个元素。这导致在任何时间点只有一个可到达的自动装箱对象。垃圾收集器被设计为对短期对象有效,因为只有幸存的对象会四处移动,因此序列产生的自动装箱是最好/最便宜的堆使用类型。这些短暂的自动装箱对象的内存不会淹没幸存者空间,因此这将利用垃圾收集器的有效路径而不是导致完全收集。

在其他条件相同的情况下,最好避免自动装箱。因此,在单独的流操作中使用临时原始值时,流可以更有效。然而,这只适用于使用专门版本的情况,而且只要我们不过度使用原始变体,因为它们有时是有害的。

  • 可选值

当值可能不存在时(例如,使用 min、max、reduce、find 等),Streams 创建 Optional 包装器,而序列使用可为 null 的类型:

// Sequence
people.asSequence()
   ...
   .find { it.name.length > 5 } // returns nullable Person
// Stream
people.stream()
   ...
   .filter { it.name.length > 5 }
   .findAny() // returns Optional<Person> wrapper

因此,序列对于可选值更有效,因为它们避免创建 Optional 包装器对象。

  • Lambda 创建

序列支持在 1 步中映射和过滤非空值,从而减少 lambdas 实例的数量:

// Sequence
people.asSequence()
   .mapNotNull { it.testScore } // create lambda instance
...
// Stream
people.stream()
   .map { it.testScore } // create lambda instance
   .filter { it != null } // create another lambda instance
   ...

此外,序列上的大多数终端操作都是内联函数,可以避免创建最终的 lambda 实例:

people.asSequence()
    .filter { it.age >= 18 }
    .forEach { println(it.name) } // forEach inlined at compile time

因此,序列创建更少的 lambda 实例,从而由于更少的间接性而导致更高效的执行。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-01-24
  • 1970-01-01
  • 2016-12-17
  • 1970-01-01
  • 2022-12-17
  • 1970-01-01
  • 2022-11-24
相关资源
最近更新 更多