是否排序并立即对流进行处理?

时间:2018-03-14 23:15:53

标签: java java-stream

想象一下,我有一些看起来像这样的东西:

Stream<Integer> stream = Stream.of(2,1,3,5,6,7,9,11,10)
            .distinct()
            .sorted();

distinct()sorted()的javadoc表示它们是“有状态的中间操作”。这是否意味着内部流将执行诸如创建哈希集,添加所有流值,然后看到sorted()将这些值抛出到排序列表或排序集中?或者它比那更聪明?

换句话说,.distinct().sorted()会导致java遍历流两次还是java会延迟执行终端操作(例如.collect)?

2 个答案:

答案 0 :(得分:9)

你问了一个加载的问题,暗示必须在两个选择之间做出选择。

有状态中间操作必须存储数据,在某些情况下,直到能够向下游传递元素之前存储所有元素,但这并不会改变这项工作被推迟直到终端操作具有的事实已经开始了。

说它必须“遍历流两次”也是不正确的。有完全不同的遍历,例如在sorted()的情况下,首先,遍历内部缓冲区的源的遍历将被排序,其次是遍历缓冲区。在distinct()的情况下,顺序处理中不会发生第二次遍历,内部HashSet仅用于确定是否向下游传递元素。

所以当你运行

Stream<Integer> stream = Stream.of(2,1,3,5,3)
    .peek(i -> System.out.println("source: "+i))
    .distinct()
    .peek(i -> System.out.println("distinct: "+i))
    .sorted()
    .peek(i -> System.out.println("sorted: "+i));
System.out.println("commencing terminal operation");
stream.forEachOrdered(i -> System.out.println("terminal: "+i));

打印

commencing terminal operation
source: 2
distinct: 2
source: 1
distinct: 1
source: 3
distinct: 3
source: 5
distinct: 5
source: 3
sorted: 1
terminal: 1
sorted: 2
terminal: 2
sorted: 3
terminal: 3
sorted: 5
terminal: 5

表明在终端操作开始之前没有任何事情发生,并且来自源的元素立即通过distinct()操作(除非是重复),而所有元素都在sorted()操作中被缓冲通过下游。

可以进一步证明distinct()不需要遍历整个流:

Stream.of(2,1,1,3,5,6,7,9,2,1,3,5,11,10)
    .peek(i -> System.out.println("source: "+i))
    .distinct()
    .peek(i -> System.out.println("distinct: "+i))
    .filter(i -> i>2)
    .findFirst().ifPresent(i -> System.out.println("found: "+i));

打印

source: 2
distinct: 2
source: 1
distinct: 1
source: 1
source: 3
distinct: 3
found: 3

正如Jose Da Silva’s answer所解释和证明的那样,缓冲量可能会随着有序并行流而改变,因为部分结果必须先调整才能传递给下游操作。

由于这些操作在实际终端操作未知之前不会发生,因此OpenJDK中可能存在比当前更多可能的优化(但可能发生在不同的实现或未来的版本中)。例如。 sorted().toArray()可以使用并返回相同的数组,或sorted().findFirst()可能会变为min()等。

答案 1 :(得分:6)

根据javadoc,distinctsorted方法都是有状态的中间操作。

StreamOps说明了以下有关此操作的内容:

  

有状态操作可能需要在生成结果之前处理整个输入。例如,在查看流的所有元素之前,不能通过对流进行排序来产生任何结果。因此,在并行计算下,一些包含有状态中间操作的管道可能需要对数据进行多次传递,或者可能需要缓冲重要数据。

但是流的收集只发生在终端操作中(例如toArraycollectforEach),两个操作都在管道中处理,数据流经它。但是,需要注意的一件重要事情是执行此操作的顺序,distinct()方法的javadoc说:

  

对于有序流,不同元素的选择是稳定的(对于重复元素,保留在遇到顺序中首先出现的元素。)对于无序流,不进行稳定性保证。

对于顺序流,当对此流进行排序时,检查的唯一元素是前一个,当未排序时,内部使用HashSet,因此在{{1}之后执行distinct导致更好的表现。

(注意:正如Eugene评论的那样,在这个secuential流中性能增益可能很小,特别是当代码很热时,但仍然避免创建额外的时间sort

您可以在此处详细了解HashSetdistinct的顺序:

Java Streams: How to do an efficient "distinct and sort"?

另一方面,对于并行流,doc表示:

  

保持并行管道中的distinct()的稳定性相对昂贵(要求操作充当完全屏障,具有大量缓冲开销),并且通常不需要稳定性。如果您的情境的语义允许,使用无序流源(例如generate(Supplier))或使用BaseStream.unordered()删除排序约束可以显着提高并行管道中distinct()的执行效率。

full barrier operation表示:

  

必须在下游启动之前执行所有上游操作。 Stream API中只有两个完整的屏障操作:.sorted()(每次)和.distinct()(按顺序并行)。

出于这个原因,当使用并行流时,相反的顺序通常更好(只要当前流是无序的),即在sort之前使用distinct,因为已排序可以开始在处理不同的时候接收元素。

使用相反的顺序,首先排序(无序并行流)然后使用distinct,在两者中都设置一个屏障,首先必须处理所有元素(流程)sorted,然后全部用于{{1} }}

以下是一个例子:

sort

以下函数接收一个名称,并返回一个从0-2秒休眠的int使用者然后打印。

distinct

这将打印,混合B和D以及所有S(Function<String, IntConsumer> process = name -> idx -> { TimeUnit.SECONDS.sleep(ThreadLocalRandom .current().nextInt(3)); // handle exception or use // LockSupport.parkNanos(..) sugested by Holger System.out.println(name + idx); }; 中没有障碍)。

如果您更改IntStream.range(0, 8).parallel() // n > number of cores .unordered() // range generates ordered stream (not sorted) .peek(process.apply("B")) .distinct().peek(process.apply("D")) .sorted().peek(process.apply("S")) .toArray(); // terminal operation distinct的顺序:

sorted

这将打印所有B,然后是所有的D,然后是所有D(distinct中的障碍)。

如果您想再尝试 // ... rest .sorted().peek(process.apply("S")) .distinct().peek(process.apply("D")) // ... rest ,请再次添加distinct

unordered

这将打印所有B,然后是S和D的混合(再次在sorted中没有障碍)。

编辑:

更改了一些代码以更好地解释并使用 // ... rest .sorted().unordered().peek(process.apply("S")) .distinct().peek(process.apply("D")) // ... rest 作为sugested。