【问题标题】:Which implementation to use when creating a List from Iterable从 Iterable 创建 List 时使用哪种实现
【发布时间】:2016-12-20 02:31:34
【问题描述】:

我发现自己经常做以下事情:

Iterator<A> itr = iterableOfA.getIterator();
List<B> list = new ArrayList<>(); // how about LinkedList?
while (itr.hasNext()) {
    B obj = iter.next().getB();
    list.add(obj);
}
someMethod(list); // this method takes an Iterable

我不知道iterableOfA 中可能有多少元素——可能是 5,可能是 5000。在这种情况下,LinkedList 会是一个更好的实现吗(因为list.add(obj) 会那么是 O(1))?就目前而言,如果 iterableOfA 有 5000 个元素,这将导致 list 的支持数组的许多大小调整。

其他选项是:

Iterator<A> itr = iterableOfA.getIterator();
int size = Iterables.size(iterableOfA); // from Guava
List<B> list = new ArrayList<>(size);
// and the rest...

这意味着iterableOfA 的双重迭代。当迭代的大小未知并且变化很大时,哪个选项最好:

  1. 只需使用ArrayList
  2. 只需使用LinkedList
  3. 计算iterableOfA中的元素并分配一个ArrayList

编辑 1

澄清一些细节:

  1. 我主要针对性能进行优化,其次针对内存使用进行优化。
  2. list 是一个短暂的分配,因为在请求结束时,任何代码都不应持有对它的引用。

编辑 2

对于我的具体情况,我意识到 someMethod(list) 不能处理超过 200 个元素的可迭代对象,因此我决定使用 new ArrayList&lt;&gt;(200),这对我来说已经足够好了。

但是,在一般情况下,我更愿意实施已接受答案中概述的解决方案(包装在自定义迭代中,无需分配列表)。

所有其他答案都对ArrayListLinkedList 的比较提供了宝贵的见解,因此我代表广大的 SO 社区感谢大家!

【问题讨论】:

    标签: java performance list memory-management


    【解决方案1】:

    当迭代的大小未知并且变化很大时,哪个选项最好

    这取决于您要优化什么。

    • 如果您正在优化性能,那么使用ArrayList 可能会更快。尽管ArrayList 需要调整后备数组的大小,但它使用指数增长模式来做到这一点。但是,这取决于迭代的开销。

    • 如果您要针对长期内存使用进行优化,请考虑使用ArrayList,后跟trimToSize()

    • 如果要针对峰值内存使用进行优化,“计数优先”方法可能是最好的。 (这假设您可以迭代两次。如果迭代器实际上是惰性计算的包装器......这可能是不可能的。)

    • 如果您正在优化以减少 GC,那么“先计数”可能是最好的,具体取决于迭代的细节。

    在所有情况下,我们都会建议您:

    1. 在您花更多时间处理此问题之前对您的应用程序进行简介。在很多情况下,您会发现这根本不值得您进行优化。

    2. 使用应用程序中的类和典型数据结构对您正在考虑的两个替代方案进行基准测试。


    就目前而言,如果iterableOfA 有 5000 个元素,这将导致列表后备数组的许多大小调整。

    ArrayList 类调整为与当前大小成比例的新大小。这意味着调整大小的次数为O(logN)N 列表追加调用的总成本为O(N)

    【讨论】:

    • 好奇的补充:ArrayList 更快,因为它更紧凑的内存布局,其中一个额外的条目仅消耗 8 个字节(或 4 个压缩 oops),而 LinkedList 则每个新的entry 需要一个大约 32 字节的附加 Node 对象(或大约 16 字节的压缩 oops)。也就是说,即使数组过度分配了 2 倍(最坏情况),它仍然占用 LinkedList 一半的内存。
    【解决方案2】:

    我会完全跳过将元素复制到新集合。

    我们有实用程序代码可以轻松地将迭代器包装成迭代器和过滤器用于类型之间的转换,但它的要点是:

    final Iterable<A> iofA ... ;
    Iterable<B> iofB = new Iterable<B>() {
      public Iterator<B> iterator() {
        return new Iterator<B>() {
          private final Iterator<A> _iter = iofA.iterator();
          public boolean hasNext() { return _iter.hasNext(); }
          public B next() { return _iter.next().getB(); }
        };
      }
    };
    

    不需要额外的存储等。

    【讨论】:

    • 虽然这不是我所问问题的直接答案,但它是我一直在考虑的更聪明的解决方案,因此将其标记为已接受的答案。这不会减少其他人的答案。
    【解决方案3】:

    第三个选项还不错。为了获得大小,大多数集合只返回它们在内部维护的计数器......它不会遍历整个列表。这取决于实现,但所有 java.util.xxx 集合类都是这样做的。

    如果您知道“iterableOfA”的潜在类型是什么,您可以检查它们的大小。

    如果“iterableOfA”将是一些自定义实现并且您不确定大小是如何完成的,那么链表会更安全。那是因为你的大小变化和调整大小的潜力更高,因此你将无法获得可预测的性能。

    也不确定您在填充“B”的集合中执行什么操作,您的选择也取决于此。

    【讨论】:

    • 好的,你正在使用 - Iterables.size(iterableOfA); // 来自 Guava...它仍然会使用底层集合的 size() 方法,所以它取决于底层集合是什么。如果是这些 java 集合中的任何一个,应该没问题....但如果是自定义实现,则必须检查。
    【解决方案4】:

    LinkedList 是一个对缓存有敌意的内存,它的父亲 (Joshua Bloch) regrets

    我敢打赌,在你的情况下它不会更快,因为 ArrayList 调整大小已经过优化,并且每个元素也需要摊销 O(1)

    基本上,LinkedList 更快的唯一情况是以下循环:

    for (Iterator<E> it = list.iterator(); it.hasNext(); ) {
        E e = it.next();
        if (someCondition(e)) e.remove();
    }
    

    就目前而言,如果 iterableOfA 有 5000 个元素,这将导致列表支持数组的许多大小调整。

    许多类似于log(5000 / 10) / log(1.5),即15。但计数并不重要,因为最后一次调整大小占主导地位。您可能会复制每个对象引用两次,这很便宜。

    假设你会对列表做任何事情,它非常便宜。


    在某些情况下,仅仅为了找出元素的数量而进行迭代可能会有所帮助,但速度取决于输入 Iterable。所以除非你非常需要速度并且你知道输入永远不会很慢,否则我会避免这样的优化。

    【讨论】:

      猜你喜欢
      • 2011-04-19
      • 2011-04-26
      • 1970-01-01
      • 2015-11-25
      • 1970-01-01
      • 2020-01-24
      • 1970-01-01
      • 2018-03-04
      • 2011-08-28
      相关资源
      最近更新 更多