Java8中的流(Stream)在一组数据中保持顺序的一致性问题。

4
据我所知,在Java中,Set是一个无序的集合,迭代器会按照它自己选择的某种顺序处理项目(这里可能有误),但是确保处理所有Set中的元素。在Java8中,Collections中引入了stream() API,并且具有skip和limit功能。因此,我想知道从流中处理的项的顺序是否保持不变,无论我启动流多少次,还是每次都会随机?如果在流之间修改Set,顺序是否会改变?也许与问题无关,但我在这里提供了问题:现在来看问题,我有一个大小为2000或其他的Set,创建后不会被修改,我每次执行50个批量操作,每个批量操作涉及一个网络调用。我有一个起始参数,每个批量调用后都会增加50。如果我使用Stream覆盖我的Set,并将"start"作为每个批处理的跳过参数,那么对于每个批处理,都将是一个新的Stream,对吗?因此,流的顺序是否保证保持相同。显然,我不希望同一条目出现多次,更重要的是,我不想错过任何条目。最简单的方法就是使用ArrayList,但我想知道是否真的需要创建一个Set。

@holi-java 总之,我的问题是想了解当我多次执行 set.stream() 操作时,是否会保留元素的顺序。如果顺序经常变化,我就不能依赖于 stream API 中的 skip 和 limit 方法对所有元素进行批处理。 - Biscuit Coder
2
你不应该这样做!也许今天你运行一些测试并发现顺序被保留了,但是这并不是规范所保证的。如果明天JDK团队改变了实现方式,你的代码可能会开始出错。如果你想要保证顺序,你需要使用List,那就是它们的设计初衷。 - fps
1
@FedericoPeraltaSchaffner 同意。无论测试如何,这并不是一件可以保证的事情。因此使用它太冒险了。我将为此创建一个 ArrayList。 - Biscuit Coder
1
请查看后续问题我的回答 - Stuart Marks
2个回答

9

让我们从一个例子开始。首先是我认为显而易见的:

List<String> wordList = Arrays.asList("just", "a", "test");

    Set<String> wordSet = new HashSet<>(wordList);

    System.out.println(wordSet);

    for (int i = 0; i < 100; i++) {
        wordSet.add("" + i);
    }

    for (int i = 0; i < 100; i++) {
        wordSet.remove("" + i);
    }

    System.out.println(wordSet);

输出结果将显示不同的“顺序”-因为我们通过1-100的添加使容量更大,并且条目已移动。它们仍然是3个-但是以不同的顺序(如果可以称之为顺序)。

因此,是的,在流操作之间修改Set后,“顺序”可能会改变。

由于您说在创建后Set不会被修改-因此在当前实现下,顺序被保留在此刻(无论是什么)。更确切地说,一旦条目被放入Set中,它就不会在内部随机化。

但是绝对不要依赖这一点-任何时候都不要这样做。事情可能会毫无预兆地改变,因为合同允许这样做-文件不对任何顺序做出任何保证- Set主要用于唯一性。

例如,jdk-9不可变的SetMap 确实具有内部随机化,并且“顺序”将从运行到运行改变:

Set<String> set = Set.of("just", "a", "test");
System.out.println(set);

这是允许打印的:

 [a, test, just] or [a, just, test]

编辑

这是随机化模式的样子:

/**
 * A "salt" value used for randomizing iteration order. This is initialized once
 * and stays constant for the lifetime of the JVM. It need not be truly random, but
 * it needs to vary sufficiently from one run to the next so that iteration order
 * will vary between JVM runs.
 */
static final int SALT;
static {
    long nt = System.nanoTime();
    SALT = (int)((nt >>> 32) ^ nt);
}

这个函数的作用:

先取一个长整型数,将其前32位与后32位进行异或运算,然后从结果中取最后32位(通过强制类型转换为整型)。使用异或的原因是因为它具有50%的0和1分布,所以不会改变结果。

如何在代码中使用(例如对于一个包含两个元素的Set):

// based on SALT set the elements in a particular iteration "order"
if (SALT >= 0) {
   this.e0 = e0;
   this.e1 = e1;
} else {
   this.e0 = e1;
   this.e1 = e0;

根据这里的信息,我猜jdk9中内部随机排列的实现方式是这样的:

最终的安全特性是不可变Set元素和Map键的随机迭代顺序。HashSet和HashMap的迭代顺序一直未指定,但相当稳定,导致代码对该顺序有意外的依赖。当迭代顺序发生变化时,这会导致代码出现问题,这种情况偶尔会发生。新的Set / Map集合在运行时更改它们的迭代顺序,希望能够在测试或开发中尽早排除顺序依赖。

因此,这基本上是为了打破所有依赖于Set/Map顺序的代码。当人们从java-7升级到java-8并且依赖于HashMap的顺序(LinkedNode)时,也发生了同样的事情,由于TreeNode的引入,顺序变得不同了。如果保留这样的功能并且人们依赖它多年,那么很难删除它并执行一些优化,例如HashMap移动到TreeNode;因为现在你被迫保持那个顺序,即使你不想要。但这只是我的猜测,请将其视为猜测。


2
@FedericoPeraltaSchaffner,我编辑了代码以展示它的实现方式。 - Eugene
2
@FedericoPeraltaSchaffner,“为什么”并不容易,老实说我只能想到一种情况,那就是更好地适应规范(规范指出没有任何顺序得到保证)... - Eugene
2
@FedericoPeraltaSchaffner,这里还有一段非常有趣的代码,至少对我来说是这样。ImmutableCollections.SetN - 如何选择数组中的插槽... - Eugene
5
@Eugene,你对随机化背后的动机的猜测是正确的。我记得当时在 core-libs 开发者邮件列表中看到了这方面的讨论。即便是 OpenJDK 的测试代码也有很多这样的依赖关系,而消除这些依赖关系需要花费相当大的工作量。他们想要避免再次陷入同样的困境。 - Stefan Zobel
4
关于迭代顺序的回答很好,但是需要强调的是,一旦流是无序的,流上的 skiplimit 也不再受迭代顺序的限制。 - Holger
显示剩余6条评论

5
这里有两个方面。正如Eugene正确指出的,不能假设HashSet的迭代顺序保持不变--没有这样的保证。
但另一个方面是Stream实现,如果Spliterator没有报告ORDERED特征,则不需要维护迭代顺序。
换句话说,如果流是无序的,skip(1)不需要跳过第一个元素,因为没有“第一个”元素,而只需跳过一个元素。
虽然流不太可能实现随机化,但它们试图利用特征来最小化工作量。一个合理的情况是,Stream实现将像limit(size-n)一样对待无序但SIZED源的skip(n),因为这也会有效地跳过n个元素,且工作量更少。
这样的优化可能今天不会发生,但在下一个版本中可能发生,即使HashSet的迭代顺序没有改变,这也会破坏您的批处理场景。

2
很好的观点!我没有想到,但它非常有道理。 - Eugene

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接