5.2 元素顺序

另外一个尚未提及的关于集合类的内容是流中的元素以何种顺序排列。读者可能知道,一些集合类型中的元素是按顺序排列的,比如List;而另一些则是无序的,比如HashSet。增加了流操作后,顺序问题变得更加复杂。

直观上看,流是有序的,因为流中的元素都是按顺序处理的。这种顺序称为出现顺序。出现顺序的定义依赖于数据源和对流的操作。

在一个有序集合中创建一个流时,流中的元素就按出现顺序排列,因此,例5-1中的代码总是可以通过。

例5-1 顺序测试永远通过

  1. List<Integer> numbers = asList(1, 2, 3, 4);
  2. List<Integer> sameOrder = numbers.stream()
  3. .collect(toList());
  4. assertEquals(numbers, sameOrder);

如果集合本身就是无序的,由此生成的流也是无序的。HashSet就是一种无序的集合,因此不能保证例5-2所示的程序每次都通过。

例5-2  顺序测试不能保证每次通过

  1. Set<Integer> numbers = new HashSet<>(asList(4, 3, 2, 1));
  2. List<Integer> sameOrder = numbers.stream()
  3. .collect(toList());
  4. // 该断言有时会失败
  5. assertEquals(asList(4, 3, 2, 1), sameOrder);

流的目的不仅是在集合类之间做转换,而且同时提供了一组处理数据的通用操作。有些集合本身是无序的,但这些操作有时会产生顺序,试看例5-3中的代码。

例5-3 生成出现顺序

  1. Set<Integer> numbers = new HashSet<>(asList(4, 3, 2, 1));
  2. List<Integer> sameOrder = numbers.stream()
  3. .sorted()
  4. .collect(toList());
  5. assertEquals(asList(1, 2, 3, 4), sameOrder);

一些中间操作会产生顺序,比如对值做映射时,映射后的值是有序的,这种顺序就会保留下来。如果进来的流是无序的,出去的流也是无序的。看一下例5-4所示代码,我们只能断言HashSet中含有某元素,但对其顺序不能作出任何假设,因为HashSet是无序的,使用了映射操作后,得到的集合仍然是无序的。

例5-4 本例中关于顺序的假设永远是正确的

  1. List<Integer> numbers = asList(1, 2, 3, 4);
  2. List<Integer> stillOrdered = numbers.stream()
  3. .map(x -> x + 1)
  4. .collect(toList());
  5. //顺序得到了保留
  6. assertEquals(asList(2, 3, 4, 5), stillOrdered);
  7. Set<Integer> unordered = new HashSet<>(numbers);
  8. List<Integer> stillUnordered = unordered.stream()
  9. .map(x -> x + 1)
  10. .collect(toList());
  11. // 顺序得不到保证
  12. assertThat(stillUnordered, hasItem(2));
  13. assertThat(stillUnordered, hasItem(3));
  14. assertThat(stillUnordered, hasItem(4));
  15. assertThat(stillUnordered, hasItem(5));

一些操作在有序的流上开销更大,调用unordered方法消除这种顺序就能解决该问题。大多数操作都是在有序流上效率更高,比如filtermapreduce等。

这会带来一些意想不到的结果,比如使用并行流时,forEach方法不能保证元素是按顺序处理的(第6章会详细讨论这些内容)。如果需要保证按顺序处理,应该使用forEachOrdered方法,它是你的朋友。