8.2 使用ListSet

Java 8在ListSet的接口中新引入了以下方法。

  • removeIf移除集合中匹配指定谓词的元素。实现了ListSet的所有类都提供了该方法(事实上,这个方法继承自Collection接口)。
  • replaceAll用于 List接口中,它使用一个函数(UnaryOperator)替换元素。
  • sort也用于List接口中,对列表自身的元素进行排序。

以上所有方法都作用于调用对象本身。换句话说,它们改变的是集合自身,这一点跟流的操作有很大的不同,流的操作会生成一个新(复制)的结果。为什么要添加这些新方法呢?因为集合的修改烦琐而且容易出错。所以Java 8的开发团队添加了removeIfreplaceAll来解决这一问题。

8.2.1 removeIf方法

来看看下面这段代码,它试图从所有的交易记录中删除那些以数字打头的引用代码(reference code)的交易:

  1. for (Transaction transaction : transactions) {
  2. if(Character.isDigit(transaction.getReferenceCode().charAt(0))) {
  3. transactions.remove(transaction);
  4. }
  5. }

发现其中的问题了吗?非常不幸,这段代码可能导致ConcurrentModificationException。为什么会这样?因为在底层实现上,for-each循环使用了一个迭代器对象,所以代码的执行会像下面这样:

  1. for (Iterator<Transaction> iterator = transactions.iterator();
  2. iterator.hasNext(); ) {
  3. Transaction transaction = iterator.next();
  4. if(Character.isDigit(transaction.getReferenceCode().charAt(0))) {
  5. transactions.remove(transaction); ←---- 问题在这儿,我们使用了两个不同的对象来迭代和修改集合
  6. }
  7. }

注意,在这段代码中,集合由两个不同的对象管理着:

  • Iterator对象,它使用next()hasNext()方法查询源;
  • Collection对象,它通过调用remove()方法删除集合中的元素。

因此,迭代器对象的状态没有与集合对象的状态同步,反之亦然。为了解决这个问题,你只能显式地使用Iterator对象,并通过它调用remove()方法:

  1. for (Iterator<Transaction> iterator = transactions.iterator();
  2. iterator.hasNext(); ) {
  3. Transaction transaction = iterator.next();
  4. if(Character.isDigit(transaction.getReferenceCode().charAt(0))) {
  5. iterator.remove();
  6. }
  7. }

如此一来这段代码就变得非常烦琐。现在,你使用Java 8提供的removeIf方法可以取代这段代码中的逻辑,该方法不仅简单,还可以避免前述的缺陷。removeIf方法接受一个用于判断删除哪一个元素的谓词作为参数:

  1. transactions.removeIf(transaction ->
  2. Character.isDigit(transaction.getReferenceCode().charAt(0)));

不过,有些时候,你想要做的不是删除列表中的元素,而是替换它们。为了解决这个问题,Java 8新增了replaceAll方法。

8.2.2 replaceAll方法

List接口提供的replaceAll方法让你可以使用一个新的元素替换列表中满足要求的每个元素。你可以使用Stream API解决这一问题,如下所示:

  1. referenceCodes.stream() ←---- [a12, C14, b13]
  2. .map(code -> Character.toUpperCase(code.charAt(0)) +
  3. code.substring(1))
  4. .collect(Collectors.toList())
  5. .forEach(System.out::println); ←---- 输出A12, C14, B13

这段代码会生成一个新的字符串集合。然而,你想要的是更新现有集合的方法。你还可以使用ListIterator对象(该对象提供了set()方法,其可以替换集合中的元素):

  1. for (ListIterator<String> iterator = referenceCodes.listIterator();
  2. iterator.hasNext(); ) {
  3. String code = iterator.next();
  4. iterator.set(Character.toUpperCase(code.charAt(0)) + code.substring(1));
  5. }

如你所见,这段代码相当烦琐。此外,刚才介绍过,把Iterator对象和集合对象混在一起使用比较容易出错,特别是还需要修改集合对象的场景。在Java 8中,你可以通过下面这种简单的代码实现同样的逻辑:

  1. referenceCodes.replaceAll(code -> Character.toUpperCase(code.charAt(0)) +
  2. code.substring(1));

我们已经学习了ListSet的新特性,不过别忘了还有Map。下一节将介绍Map接口的新特性。