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

Java 为什么要启动具有初始容量的ArrayList?,java,data-structures,arraylist,capacity,Java,Data Structures,Arraylist,Capacity,ArrayList的常用构造函数是: ArrayList<?> list = new ArrayList<>(); ArrayList list=new ArrayList(); 但也有一个重载构造函数,其初始容量有一个参数: ArrayList<?> list = new ArrayList<>(20); ArrayList list=新的ArrayList(20); 当我们可以随心所欲地附加到ArrayList时,为什么创建具有初始容量

ArrayList
的常用构造函数是:

ArrayList<?> list = new ArrayList<>();
ArrayList list=new ArrayList();
但也有一个重载构造函数,其初始容量有一个参数:

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

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

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

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


也就是说,即使没有预先分配,在
ArrayList
后面插入
n
元素也会保证占用总
O(n)
时间。换句话说,追加一个元素是一个摊销的固定时间操作。这是通过让每次重新分配都以指数方式增加数组的大小来实现的,通常是以
1.5
的系数增加。使用这种方法,操作总数。

将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(3);
示例。添加(1);//大小()==1
示例。添加(2);//大小()==2,
示例。添加(2);//size()==3,示例已“填充”
示例。添加(3);//size()==4,示例已“展开”,以便可以添加第四个元素。
正如您在上面的示例中所看到的,如果需要,可以扩展
ArrayList
。这并没有告诉您Arraylist的大小通常是原来的两倍(不过请注意,新的大小取决于您的实现)。以下引述自:

“每个ArrayList实例都有一个容量。容量是 用于存储列表中元素的数组。它始终位于 至少与列表大小相同。当元素添加到 ArrayList,其容量会自动增长。增长的详细信息 策略的指定不超过添加元素 固定摊销时间成本。”


显然,如果您不知道将保持什么样的范围,设置大小可能不是一个好主意-但是,如果您心中有一个特定的范围,设置初始容量将提高内存效率

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 list=new ArrayList();
//与新ArrayList(10)相同;

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

我认为每个ArrayList的初始容量值为“10”。所以不管怎样,如果创建ArrayList时未在构造函数中设置容量,则将使用默认值创建该ArrayList。

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

我认为这是一种优化。没有初始容量的ArrayList将有约10个空行,并在执行添加时展开


要获得一个包含您需要调用的项数的列表,请执行以下操作:

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

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


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

这是为了避免为每个对象重新分配资源

int newCapacity = (oldCapacity * 3)/2 + 1;
内部创建了
新对象[]

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

事实上,我在两个月前写了一篇关于这个话题的文章。这篇文章是为C#的
List
写的,但是Java的
ArrayList
有一个非常类似的实现。由于
ArrayList
是使用动态数组实现的,因此它会根据需要增加大小。因此,容量构造器用于优化目的

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

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

10
16
25
38
58
... 17 resizes ...
198578
297868
446803
670205
1005308
因此,列表的容量从
10
开始,当添加第11项时,它将增加
50%+1
16
。在第17项中,
ArrayList
再次增加到
25
a
10
16
25
38
58
... 17 resizes ...
198578
297868
446803
670205
1005308
list1Sttop-list1Start = 14
list2Sttop-list2Start = 10
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));
}