为啥要以初始容量启动 ArrayList?

Posted

技术标签:

【中文标题】为啥要以初始容量启动 ArrayList?【英文标题】:Why start an ArrayList with an initial capacity?为什么要以初始容量启动 ArrayList? 【发布时间】:2013-03-04 00:57:48 【问题描述】:

ArrayList的常用构造函数是:

ArrayList<?> list = new ArrayList<>();

但也有一个重载的构造函数,它的初始容量有一个参数:

ArrayList<?> list = new ArrayList<>(20);

当我们可以随意附加到 ArrayList 时,为什么创建具有初始容量的 ArrayList 很有用?

【问题讨论】:

你试过看ArrayList源码吗? @Joachim Sauer:有时我们在仔细阅读源代码时会发现。如果他已经阅读了源代码,我正在尝试。我理解你的一面。谢谢。 ArrayList 表现不佳时期,为什么要使用这样的结构 what's meant by parameter (int initial capacity) in an arraylist 的可能重复项 【参考方案1】:

Arraylist 的默认大小为 10

/**
 * Constructs an empty list with an initial capacity of ten.
 */
public ArrayList() 
    this(10);
   

因此,如果您要添加 100 条或更多记录,您可以看到内存重新分配的开销。

ArrayList<?> list = new ArrayList<>();    
// same as  new ArrayList<>(10);      

因此,如果您对将存储在 Arraylist 中的元素数量有任何想法,最好创建具有​​该大小的 Arraylist,而不是从 10 开始然后继续增加它。

【讨论】:

不保证将来JDK版本的默认容量总是10 - private static final int DEFAULT_CAPACITY = 10【参考方案2】:

因为ArrayList 是dynamically resizing array 数据结构,这意味着它被实现为具有初始(默认)固定大小的数组。当它被填满时,数组将被扩展为一个双倍大小的数组。此操作成本高昂,因此您希望尽可能少。

所以,如果您知道上限是 20 个项目,那么创建初始长度为 20 的数组比使用默认值(例如 15)然后将其调整为 15*2 = 30 并仅使用 20 而浪费扩展周期。

附: - 正如 AmitG 所说,扩展因子是特定于实现的(在本例中为 (oldCapacity * 3)/2 + 1

【讨论】:

其实是int newCapacity = (oldCapacity * 3)/2 + 1;【参考方案3】:

我已经测试了带有和不带有 initialCapacity 的 ArrayList,我得到了令人惊讶的结果 当我将 LOOP_NUMBER 设置为 100,000 或更少时,结果是设置 initialCapacity 是有效的。

list1Sttop-list1Start = 14
list2Sttop-list2Start = 10

但是当我将 LOOP_NUMBER 设置为 1,000,000 时,结果变为:

list1Stop-list1Start = 40
list2Stop-list2Start = 66

最后,我无法弄清楚它是如何工作的?! 示例代码:

 public static final int LOOP_NUMBER = 100000;

public static void main(String[] args) 

    long list1Start = System.currentTimeMillis();
    List<Integer> list1 = new ArrayList();
    for (int i = 0; i < LOOP_NUMBER; i++) 
        list1.add(i);
    
    long list1Stop = System.currentTimeMillis();
    System.out.println("list1Stop-list1Start = " + String.valueOf(list1Stop - list1Start));

    long list2Start = System.currentTimeMillis();
    List<Integer> list2 = new ArrayList(LOOP_NUMBER);
    for (int i = 0; i < LOOP_NUMBER; i++) 
        list2.add(i);
    
    long list2Stop = System.currentTimeMillis();
    System.out.println("list2Stop-list2Start = " + String.valueOf(list2Stop - list2Start));

我在windows8.1和jdk1.7.0_80上测试过

【讨论】:

嗨,不幸的是 currentTimeMillis 的容差高达一百毫秒(取决于),这意味着结果几乎不可靠。我建议使用一些自定义库来做到这一点。【参考方案4】:

根据我对ArrayList 的经验,提供初始容量是避免重新分配成本的好方法。但它有一个警告。上面提到的所有建议都表明,只有在知道元素数量的粗略估计时才应该提供初始容量。但是当我们试图在不知道的情况下给出初始容量时,保留和未使用的内存量将是一种浪费,因为一旦列表填充到所需数量的元素,它可能永远不需要。我的意思是,我们可以在分配容量时一开始就务实,然后找到一种聪明的方法来了解运行时所需的最小容量。 ArrayList 提供了一个名为ensureCapacity(int minCapacity) 的方法。但后来,人们找到了一种聪明的方法......

【讨论】:

【参考方案5】:

这是为了避免为每个单独的对象重新分配可能的努力。

int newCapacity = (oldCapacity * 3)/2 + 1;

在内部创建new Object[]。 当您在arraylist 中添加元素时,JVM 需要努力创建new Object[]。如果您没有上述代码(您认为的任何算法)进行重新分配,那么每次调用 arraylist.add() 时都必须创建 new Object[],这是毫无意义的,我们正在浪费时间将每个大小增加 1要添加的对象。所以最好用下面的公式增加Object[]的大小。 (JSL使用下面给出的预测公式来动态增长arraylist,而不是每次增长1。因为增长需要JVM的努力)

int newCapacity = (oldCapacity * 3)/2 + 1;

【讨论】:

ArrayList 将不会为每个add 执行重新分配 - 它已经在内部使用了一些增长公式。因此这个问题没有得到回答。 @A.H.我的答案是否定测试。请在字里行间阅读。我说 “如果您没有上述代码(您认为的任何算法)进行重新分配,那么每次调用 arraylist.add() 时都必须创建新的 Object[],这是没有意义的,我们正在浪费时间." 并且 codeint newCapacity = (oldCapacity * 3)/2 + 1;,它存在于 ArrayList 类中。你仍然认为它没有答案吗? 我仍然认为它没有得到回答:在ArrayList 中,摊销重新分配发生在 any 情况下,初始容量为 any 值。问题是:为什么要对初始容量使用非标准值?除此之外:“字里行间阅读”不是技术答案所需要的。 ;-) @A.H.我的回答是,如果我们在 ArrayList 中没有重新分配过程会发生什么。答案也是如此。尝试阅读答案的精神:-)。我更清楚 在 ArrayList 中,摊销重新分配在任何情况下都会发生,初始容量具有任何值。【参考方案6】:

我实际上在 2 个月前就该主题写了blog post。这篇文章是针对 C# 的 List&lt;T&gt; 的,但 Java 的 ArrayList 有一个非常相似的实现。由于ArrayList 是使用动态数组实现的,因此它会按需增加大小。所以容量构造函数的原因是为了优化目的。

当这些调整大小操作之一发生时,ArrayList 将数组的内容复制到一个新数组中,该数组的容量是旧数组的两倍。此操作在 O(n) 时间内运行。

示例

以下是ArrayList 如何增加大小的示例:

10
16
25
38
58
... 17 resizes ...
198578
297868
446803
670205
1005308

所以列表以10 的容量开始,当添加第11 项时,它增加了50% + 116。在第 17 项中,ArrayList 再次增加到 25,依此类推。现在考虑我们正在创建一个列表的示例,其中所需容量已被称为1000000。在没有大小构造函数的情况下创建 ArrayList 将调用 ArrayList.add 1000000 次,这通常需要 O(1)O(n) 调整大小。

1000000 + 16 + 25 + ... + 670205 + 1005308 = 4015851 次操作

使用构造函数进行比较,然后调用ArrayList.add,保证在O(1)中运行。

1000000 + 1000000 = 2000000 次操作

Java 与 C#

Java 同上,从10 开始,在50% + 1 处增加每次调整大小。 C# 从4 开始,并且增长得更加积极,每次调整大小都会翻倍。 1000000 添加了上面的示例,用于 C# 使用 3097084 操作。

参考文献

My blog post on C#'s List<T> Java's ArrayList source code

【讨论】:

【参考方案7】:

设置 ArrayList 的初始大小,例如到ArrayList&lt;&gt;(100),减少了重新分配内部存储器的次数。

示例:

ArrayList example = new ArrayList<Integer>(3);
example.add(1); // size() == 1
example.add(2); // size() == 2, 
example.add(2); // size() == 3, example has been 'filled'
example.add(3); // size() == 4, example has been 'expanded' so that the fourth element can be added. 

正如您在上面的示例中看到的 - 如果需要,可以扩展 ArrayList。这并没有向您显示 Arraylist 的大小通常会翻倍(尽管请注意,新大小取决于您的实现)。以下引自Oracle:

"每个ArrayList实例都有一个容量,容量就是 用于存储列表中元素的数组。它总是在 至少与列表大小一样大。随着元素被添加到 ArrayList,它的容量会自动增长。成长的细节 除了添加元素具有 固定摊销时间成本。”

显然,如果您不知道您将持有什么样的范围,那么设置大小可能不是一个好主意 - 但是,如果您确实有一个特定的范围,那么设置初始容量将提高记忆效率。

【讨论】:

【参考方案8】:

如果您事先知道ArrayList 的大小,指定初始容量会更有效。如果不这样做,随着列表的增长,内部数组将不得不重复重新分配。

最终列表越大,通过避免重新分配节省的时间就越多。

也就是说,即使没有预先分配,在 ArrayList 的后面插入 n 元素也可以保证总共花费 O(n) 时间。换句话说,附加一个元素是一个摊销的常数时间操作。这是通过使每个重新分配以指数方式增加数组的大小来实现的,通常增加1.5 的因子。采用这种方法,操作总数can be shown to be O(n)

【讨论】:

虽然预先分配已知大小是个好主意,但不这样做通常并不可怕:您将需要大约 log(n) 重新分配具有n 的最终大小,不是很多。 那么为什么不是O(nlogn),因为它会以 1.5 的每个幂(或多或少)重新分配? @PeterOlson O(n log n) 会做log n 工作n 次。这是一个严重的高估(尽管技术上正确,因为它是一个上限,大 O)。它总共复制 s + s*1.5 + s*1.5^2 + ... + s*1.5^m (使得 s*1.5^m 加倍参数更容易。假设你在满时加倍,从一个元素开始。假设您要插入 8 个元素。插入一个(成本:1)。插入两个——加倍,复制一个元素并插入两个(成本:2)。插入三个——双倍,复制两个元素,插入三个(成本:3)。插入四个(成本:1)。插入五个——双倍,复制四个元素,插入五个(成本:5)。插入六、七和八(成本:3)。总成本:1 + 2 + 3 + 1 + 5 + 3 = 16,这是插入元素数量的 两倍。从这个草图中,您可以证明平均成本通常是每个插入两个 这是时间的成本。您还可以看到,浪费空间的数量随时间而变化,有时为 0%,有时接近 100%。将因子从 2 更改为 1.5 或 4 或 100 或任何更改平均浪费空间量和平均复制时间量,但无论因子是什么,时间复杂度平均保持线性。【参考方案9】:

我会说它是一种优化。没有初始容量的 ArrayList 将有大约 10 个空行,并且会在您进行添加时扩展。

要获得一份包含您需要的项目数量的清单,请致电trimToSize()

【讨论】:

【参考方案10】:

ArrayList 可以包含许多值,并且在进行大量初始插入时,您可以告诉 ArrayList 在开始时分配更大的存储空间,以免在尝试为下一项分配更多空间时浪费 CPU 周期。因此在开始时分配一些空间更有效。

【讨论】:

【参考方案11】:

我认为每个 ArrayList 的初始容量值为“10”。所以无论如何,如果你创建一个 ArrayList 而不在构造函数中设置容量,它将使用默认值创建。

【讨论】:

以上是关于为啥要以初始容量启动 ArrayList?的主要内容,如果未能解决你的问题,请参考以下文章

在 Java 8 中,为啥 ArrayList 的默认容量现在为零?

Java中ArrayList的初始容量和容量分配

ArrayList 初始容量和 IndexOutOfBoundsException [重复]

奇怪,为什么ArrayList初始化容量大小为10?

奇怪,为什么ArrayList初始化容量大小为10?

奇怪,为什么ArrayList初始化容量大小为10?