【问题标题】:What's the most efficient equivalent of addAll when I need to adapt the source list?当我需要调整源列表时,最有效的 addAll 等效项是什么?
【发布时间】:2016-10-15 06:47:07
【问题描述】:

如果我想将一个列表添加到另一个列表中,我会调用target.adAll(source)

但是如果我需要先处理列表中的每个值呢?

我可以做类似的事情

for(String s: source) {
  target.add(s.toLowerCase());
}

或使用 java 8:

source.stream().map(x->x.toLowerCase()).forEachOrdered(target::add);

但无论哪种方式,我似乎都失去了addAll 的性能优势。执行此操作的最佳方式是什么?

【问题讨论】:

标签: java collections java-8


【解决方案1】:

那么,“addAll 的性能优势”是什么?最后,addAll 无论如何都必须将所有元素添加到目标Collection。如果目标是ArrayList,主要的好处是确保没有不必要的扩容操作。

但请注意,这是以创建临时数组为代价的,请参阅implementation of ArrayList.addAll。为了超过这笔费用,您必须添加大量元素。

如果我们要添加比目标当前容量更多的元素,增加操作是不可避免的。所以addAll 只提供了一个好处,如果您只使用add,目标必须增加容量不止一次。由于容量增加了 1.5 倍,并且容量等于或高于当前大小,我们必须添加至少比当前大小一半更多的元素,以防止不必要的容量增加操作。

如果你真的认为,这将是一个问题,很容易解决:

if(target instanceof ArrayList)
    ((ArrayList)target).ensureCapacity(target.size()+source.size());
source.stream().map(String::toLowerCase).forEachOrdered(target::add);

当然,在少数极端情况下,add 的成本要高得多,例如CopyOnWriteArrayList。对于此目标集合类型,首先通过collect(Collectors.toList()) 收集到List,然后是addAll 可能是有益的。或者你创建一个简单的懒惰Collection 作为中间步骤:

public static <T> Collection<T> lazyCollection(Supplier<? extends Stream<T>> s) {
    return new AbstractCollection<T>() {
        public Iterator<T> iterator() { return s.get().iterator(); }
        public int size() { return (int)s.get().count(); }
        public Object[] toArray() { return s.get().toArray(); }
    };
}

可以这样使用:

target.addAll(lazyCollection(() -> source.stream().map(String::toLowerCase)));

如果一个集合首先要求size(),然后再获取Iterator,那么这种方法将受到两次评估流的影响,但是afaik,没有标准的集合。他们要么使用迭代器而不依赖于预测的大小,要么求助于toArray(),比如ArrayList.addAllCopyOnWriteArrayList.addAll

【讨论】:

  • 谢谢,这是一个很好的解释。你能解释一下为什么 LinkedList.addAll 实现在源集合上调用 toArray 吗?我想知道是否还有其他一些我可能缺少的“性能魔法”(比如 L2 缓存),因为我看不出有什么好的理由。
  • 好吧,如果源是线程安全的集合,在一次调用中获取所有元素可能是原子的,具体取决于源的类型,从而使某些组合安全,但由于没有指定这样的行为,没有理由实施它。此外,如果您将列表添加到自身,这确保它可以工作,但另一方面,有一种廉价的方法可以测试这种特殊情况,因此没有必要以这种方式处理所有来源。一般来说,要求完整的数组表明对源的迭代器有些不信任,但我们不确定是什么推动了这个决定。
【解决方案2】:

您可以使用Eclipse Collections 中的collect() 模式,collect() 等效于 JDK 中的map()

MutableList<String> source = Lists.mutable.with("A", "B", "C");
MutableList<String> target = source.collect(String::toLowerCase);

如果你有一个现有的目标列表,你可以使用collect() 的变体,它接受一个目标集合:

MutableList<String> source = Lists.mutable.with("A", "B", "C");
source.collect(String::toLowerCase, target);

如果您无法从List 更改列表:

List<String> source = Arrays.asList("A", "B", "C");
List<String> target = ListAdapter.adapt(source).collect(String::toLowerCase);

你也可以在List上使用replaceAll()

List<String> source = Arrays.asList("A", "B", "C");
List<String> target = new ArrayList<>(source);
target.replaceAll(String::toLowerCase);

上述解决方案可能会或可能不会更高效,但是,它们都预先确定了目标列表的大小。

注意:我是 Eclipse Collections 的贡献者。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-11
    • 2012-12-27
    • 2018-11-12
    • 2012-10-16
    • 2010-10-01
    • 2016-03-20
    相关资源
    最近更新 更多