为什么启动一个初始容量的ArrayList?

ArrayList的通常构造函数是:

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

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

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

为什么创build一个具有初始容量的ArrayList是非常有用的,我们可以随意添加它呢?

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

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

也就是说,即使没有预先分配,在ArrayList后面插入n元素也会保证总共需要O(n)时间。 换句话说,附加一个元素是一个摊销的恒定时间操作。 这是通过使每次重新分配按指数规模增加arrays的大小来实现的,一般是1.5 。 用这种方法,总的操作次数可以表示为O(n)

因为ArrayList是一个dynamicresize的数组结构,这意味着它被实现为一个初始(默认)固定大小的数组。 当这个被填满时,这个数组将会被扩展成一个双倍大小的数组。 这个操作是昂贵的,所以你希望尽可能less。

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

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

Arraylist的默认大小是10

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

所以,如果你要添加100个或更多的logging,你可以看到内存重新分配的开销。

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

因此,如果您对Arraylist中存储的元素数目有所了解,那么最好使用该大小创buildArraylist,而不是从10开始,然后继续增加。

实际上我在2个月前写了一篇关于这个话题的博客文章 。 这篇文章是针对C#的List<T>但Java的ArrayList有一个非常类似的实现。 由于ArrayList是使用dynamic数组实现的,因此需求量会增加。 所以容量构造器的原因是为了优化目的。

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

下面是一个如何增加ArrayList的例子:

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

因此,列表以10的容量开始,当第11个项目被添加时,增加50% + 116 。 在第17项上, ArrayList再次增加到25 ,依此类推。 现在考虑一下我们正在创build一个列表,其中所需容量已经被称为1000000 。 不带大小的构造函数创buildArrayList将调用ArrayList.add 1000000次,其中O(1)正常或O(n)resize。

1000000 + 16 + 25 + … + 670205 + 1005308 = 4015851操作

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

1000000 + 1000000 = 2000000次操作

Java与C#

Java如上所述,从10开始,每增加50% + 1大小。 C#从4开始,增加更多的积极性,在每次resize加倍。 1000000为C#添加上面的示例使用3097084操作。

参考

  • 我的博客文章在C#的List <T>上
  • Java的ArrayList源代码

设置ArrayList的初始大小,例如ArrayList<>(100) ,可以减less内存重新分配的次数。

例:

 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的大小通常是双倍的(尽pipe注意新的大小取决于你的实现)。 以下是Oracle引用的:

“每个ArrayList实例都有一个容量,容量是用于存储列表中元素的数组的大小,它总是至less与列表大小一样大,当元素被添加到一个ArrayList时,它的容量会自动增长。增长政策的具体细节并不限于增加一个要素具有不变的摊销时间成本。“

很显然,如果你不知道你将会拥有什么样的范围,设置这个尺寸可能不是一个好主意 – 但是,如果你确实有一个特定的范围,设置一个初始容量将会提高内存效率。

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

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

内部创buildnew Object[]
当你在数组列表中添加元素时,JVM需要努力创buildnew Object[] 。 如果你没有上面的代码(任何你认为的algorithm)来重新分配,那么每次你调用arraylist.add() ,都必须创buildnew Object[] ,这是毫无意义的,我们正在放弃增加1每一个对象都被添加。 所以最好用下面的公式来增加Object[]大小。
(JSL使用下面给出的预测公式来dynamic增长数组列表,而不是每次增长1,因为增长需要JVM的努力)

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

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

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

我会说这是一个优化。 没有初始容量的ArrayList将会有〜10个空行,并且当你进行一个添加时将会展开。

要准确地列出需要调用trimToSize()的项目数目

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

我已经testingArrayList和没有initialCapacity,我得到了惊喜的结果
当我将LOOP_NUMBER设置为100,000或更less时,结果是设置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)); } 

我已经testing了Windows8.1和jdk1.7.0_80