过滤器 Java 没有外部库的就地列表

Filter Java List in place without external libraries

这个问题类似于 What is the best way to filter a Java Collection? "filter a java.util.Collection based on a predicate." 有额外的要求

我们可以假设 java.util.Collection 类型是实现 .remove(int)

java.util.List

可能的解决方案:

有没有更简单的解决方案?

是否为所有标准 Java List 实施 and/or Collection 实施 .remove(int)

FiltersPredicate 是 Java8 类型,所以如果你不想使用 Java8,你需要类似的东西。

您可以使用包装 Iterator 伪造过滤器并使其与对象一起使用(类似于 Prediates 的实现方式);但是,还有一些次要问题:

你说列表很大,解决方案的内存影响应该是O(1),但如果不知道正在操作的列表,就无法保证这样的事情。 remove(int) 运算符可以在实现中分配一个新的列表索引并复制到其中。

假设列表没有这样的事情,你能做的最好的事情就是实现你自己的迭代器,它接受类似测试的谓词,或者编写一个特定的循环来处理列表。

无论如何,这听起来像是一道面试题。这是一个例子

public interface MyPredicate<T> {
   public boolean isTrue(T value);
}

public void removeOnTrue(List<T> list, MyPredicate<T> predicate) {
   Iterator<T> iterator = list.iterator();
   while (iterator.hasNext()) {
      T next = iterator.next();
      if (predicate.isTrue(next)) {
         iterator.remove();
      }
   }
}

使用跨索引的 for 循环执行此操作大致相同,只是您随后必须跟踪索引(并使用索引删除)。

要使用上面的例子:

...
List<String> names = ...;
removeOnTrue(names, new MyPredicate<String>() {
  public boolean isTrue(String value) {
    return value.startsWith("A");
  }
});
...

将产生一个 names 并删除所有以 "A" 开头的字符串。

没有适合所有 List 的最佳解决方案,这就是您永远无法达到 Java8 效率的地方,因为作为 interface 方法,Java 8 的 default 方法可以被任何 List 实现覆盖,提供为特定 class.

量身定制的实现

当你想在Java 8之前的版本中合理实现类似功能时,你必须关注常见情况。几乎没有 JRE 提供的 remove(int) 适用但 Iterator.remove 不适用 1 的列表。但考虑到 ArrayList 是最常用的可变 List 实现,对于该实现,基于迭代器的解决方案对于大型列表和大量已删除项目的性能不佳。这是因为无论您使用 remove(int) 还是 Iterator.remove,每个删除操作都会在您继续之前将所有后续项目移动一个位置,并且可能会再次删除一个项目。在最坏的情况下,有一个谓词匹配所有项目,这将带来二次复杂性。因此,为这种情况提供更复杂的解决方案很重要:

interface Predicate<T> {
    boolean test(T object);
}
public static <T> boolean removeIf(List<T> list, Predicate<? super T> p) {
    if(list instanceof RandomAccess) {
        int num=list.size();
        BitSet bs=new BitSet(num);
        for(int index=0; index<num; index++) {
            if(p.test(list.get(index))) bs.set(index);
        }
        if(bs.isEmpty()) {
            return false;
        }
        for(int dst=bs.nextSetBit(0), src=dst;; dst++, src++) {
            src=bs.nextClearBit(src);
            if(src==num) {
              list.subList(dst, src).clear();
              break;
            }
            list.set(dst, list.get(src));
        }
        return true;
    }
    else {
        boolean changed=false;
        for(Iterator<T> it=list.iterator(); it.hasNext(); ) {
            if(p.test(it.next())) {
                it.remove();
                changed=true;
            }
        }
        return changed;
    }
}

在列表实现 RandomAccess 的情况下,它包括所有 arraylist 样式实现,该解决方案将模仿类似于 Java 8 的 ArrayList.removeIf 实现的东西,尽管我们没有直接的访问内部数组,我遗漏了所有快速失败并发修改检测的东西。现在,对于 ArrayList 类型的列表,它将具有线性复杂度,因此对于 LinkedList,它将具有线性复杂度,因为它没有实现 RandomAccess,因此将使用其 [=26] 进行处理=].

该方法还实现了 Java 8 的 removeIf 返回列表是否已被操作更改的方法的约定。


1 CopyOnWriteArrayList 是一个例外,但对于写时复制列表,就地 removeIf 的想法没有实际意义,除非提供通过列表本身,因为在通过其 remove(int)(或任何其他 public)操作实现它时,我们在每次更改时有效地复制了整个列表。所以在那种情况下,将整个列表复制到一个普通列表中,在该列表上执行 removeIf 并将其复制回来在大多数情况下会更有效率。