为什么要启动具有初始容量的ArrayList?

时间:2013-03-15 10:41:06

标签: java data-structures arraylist capacity

ArrayList的通常构造函数是:

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

但是还有一个重载的构造函数,其初始容量有一个参数:

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

为什么在我们可以随意添加的情况下创建具有初始容量的ArrayList是否有用?

11 个答案:

答案 0 :(得分:190)

如果事先知道ArrayList的大小是多少,那么指定初始容量会更有效。如果不这样做,则必须在列表增长时重复分配内部数组。

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

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

答案 1 :(得分:41)

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

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

P.S。 - 正如AmitG所说,扩展因子是特定于实现的(在这种情况下为(oldCapacity * 3)/2 + 1

答案 2 :(得分:25)

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开头,然后继续增加它。

答案 3 :(得分:16)

我实际上在2个月前写了一篇关于这个话题的blog post。本文适用于C#的List<T>,但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操作

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

  

1000000 + 1000000 = 2000000次操作

Java vs C#

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

参考

答案 4 :(得分:8)

设置ArrayList的初始大小,例如到ArrayList<>(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,其容量自动增长。增长的细节   除了添加元素之外,没有指定策略   不变的摊余时间成本。“

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

答案 5 :(得分:3)

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

答案 6 :(得分:3)

这是为了避免为每个对象重新分配可能的工作。

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

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

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

答案 7 :(得分:2)

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

答案 8 :(得分:2)

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

要有一个列表,其中包含您需要拨打的项目数trimToSize()

答案 9 :(得分:0)

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

答案 10 :(得分:0)

我使用和不使用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

上测试过