为什么要以初始容量启动ArrayList?


149

通常的构造函数ArrayList是:

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

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

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

ArrayList当我们可以随意添加初始容量时,为什么创建初始容量有用呢?


17
您是否尝试查看ArrayList源代码?
阿米特G 2013年

@Joachim Sauer:有时候,当我们仔细阅读源代码时,我们就会有所了解。如果他已阅读源代码,我正在尝试。我了解您的方面。谢谢。
阿米特

ArrayList表现不佳,为什么要使用这样的结构
PositiveGuy

Answers:


196

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

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

也就是说,即使没有预先分配,也要保证n在元素的后面插入元素ArrayList会花费总O(n)时间。换句话说,附加元素是摊销的固定时间操作。这是通过使每个重新分配的数组大小通常成倍增加的方式来实现的1.5。使用这种方法,操作总数可以显示为O(n)


5
尽管预先分配已知大小是个好主意,但不这样做通常并不可怕:您将需要对最终大小为n的列表进行log(n)重新分配,这并不多。
约阿希姆·绍尔

2
@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 <n <s * 1.5 ^(m + 1))个元素。我的求和技巧不好,所以我无法为您提供精确的数学信息(调整因子2的大小为2n,因此给定或取一个小常数可能为1.5n),但事实并非如此。 t斜眼看到该总和最多是一个大于n的常数。因此,它需要O(k * n)个副本,这当然是O(n)。

1
@delnan:不能与之争论!;)顺便说一句,我真的很喜欢你的屈的论点;会将其添加到我的所有技巧中。
NPE 2013年

6
通过加倍进行参数比较容易。假设您从一个元素开始,在充满时加倍。假设您要插入8个元素。插入一个(成本:1)。插入两个-翻倍,复制一个元素,然后插入两个(成本:2)。插入三个-双重,复制两个元素,插入三个(成本:3)。插入四个(成本:1)。插入五个-重复,复制四个元素,插入五个(成本:5)。插入六,七和八(成本:3)。总成本:1 + 2 + 3 + 1 + 5 + 3 = 16,这是插入元素数量的两倍。从这个草图中,您可以证明每个刀片平均成本通常为2
埃里克·利珀特

9
那就是时间上的代价。您还可以看到,浪费的空间量随时间而变化,有时是0%,有时是100%。将因子从2更改为1.5或4或100,或者进行任何更改,都会平均浪费空间和平均复制时间,但是无论因子为何,时间复杂度平均都保持线性。
埃里克·利珀特

41

因为ArrayList动态调整大小的数组数据结构,所以它被实现为具有初始(默认)固定大小的数组。填充完毕后,该数组将扩展为两倍大小的数组。此操作成本高昂,因此您需要尽可能少的操作。

因此,如果您知道上限为20个项目,则创建初始长度为20的数组要比使用默认值(例如15)然后将其调整为大小15*2 = 30且仅使用20更好,同时浪费扩展周期。

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


9
实际上是int newCapacity = (oldCapacity * 3)/2 + 1;
AmitG

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


无法保证将来的JDK版本的默认容量将始终为private static final int DEFAULT_CAPACITY = 10
10-– vikingsteve

17

我实际上在2个月前就该主题写了一篇博客文章。本文适用于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不带size构造函数的会调用通常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#始于C#,4并且更加积极地增加,每次调整大小都会加倍。1000000以上是C#使用的添加示例3097084操作。

参考文献


9

将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时,其容量会自动增长。除了添加元素具有固定的摊销时间成本外,没有指定增长策略的细节。”

显然,如果您不知道要保持哪种范围,则设置大小可能不是一个好主意-但是,如果您确实有特定的范围,则设置初始容量将提高内存效率。


3

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


3

这是为了避免为每个对象进行重新分配而可能做出的努力。

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

内部new Object[]创建。当您在arraylist中添加元素时,
JVM需要努力创建 new Object[]。如果你没有上面的代码(任何算法中你认为)的重新分配,然后每次当你调用arraylist.add()然后new Object[]必须创建这是没有意义的,我们正在失去的时间由1对每一个要添加对象的规模日益扩大。因此最好Object[]用以下公式增加的大小。
(JSL使用下面给出的预测公式来动态增长数组列表,而不是每次都增长1。因为要增长,它需要JVM的努力)

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

ArrayList 不会为每一个单独执行重新分配add-它已经在内部使用了一些增长公式。因此,问题没有得到回答。
2013年

@AH我的答案是负面测试。请在两行之间阅读。我说:“如果您没有上述代码(您认为的任何一种算法)都可以重新分配,那么每次调用arraylist.add()时,都必须创建new Object [],这毫无意义,我们正在浪费时间。” 代码int newCapacity = (oldCapacity * 3)/2 + 1;存在于ArrayList类中的。您是否仍未回答?
AmitG

1
我仍然认为这没有得到解决:在任何情况下,以初始容量的任何值进行ArrayList摊销的重新分配。问题是:为什么要对初始容量完全使用非标准值?除此之外:“线间阅读”不是技术答案中所需要的。;-)
AH

@AH我的回答是,如果我们在ArrayList中没有重新分配过程,将会发生什么。答案也是如此。尝试阅读答案的精神:-)。我最好知道, 在ArrayList中,无论如何,初始容量的任何值都将进行摊销再分配。
AmitG

2

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


2

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

要获得具有确切数量的项目的列表,您需要调用trimToSize()


0

根据我的经验ArrayList,提供初始容量是避免重新分配成本的好方法。但它有一个警告。上面提到的所有建议都说,仅当知道元素数量的大概估计时,才应提供初始容量。但是,当我们尝试不加考虑地提供初始容量时,保留和未使用的内存量将是浪费,因为一旦列表填充到所需数量的元素,便可能永远不需要它。我的意思是,我们在开始分配容量时可以很务实,然后找到一种聪明的方式来知道运行时所需的最小容量。ArrayList提供了一种称为的方法ensureCapacity(int minCapacity)。但是后来,人们找到了一种聪明的方法...


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上进行了测试


1
嗨,不幸的是,currentTimeMillis的容差高达100毫秒(取决于),这意味着结果几乎不可靠。我建议使用一些自定义库来正确执行。
Bogdan
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.