通常的构造函数ArrayList
是:
ArrayList<?> list = new ArrayList<>();
但是,还有一个带有初始容量参数的重载构造函数:
ArrayList<?> list = new ArrayList<>(20);
ArrayList
当我们可以随意添加初始容量时,为什么创建初始容量有用呢?
通常的构造函数ArrayList
是:
ArrayList<?> list = new ArrayList<>();
但是,还有一个带有初始容量参数的重载构造函数:
ArrayList<?> list = new ArrayList<>(20);
ArrayList
当我们可以随意添加初始容量时,为什么创建初始容量有用呢?
Answers:
如果您事先知道 ArrayList
指定初始容量会更有效。如果不这样做,则随着列表的增加,内部数组将不得不反复重新分配。
最终列表越大,避免重新分配节省的时间越多。
也就是说,即使没有预先分配,也要保证n
在元素的后面插入元素ArrayList
会花费总O(n)
时间。换句话说,附加元素是摊销的固定时间操作。这是通过使每个重新分配的数组大小通常成倍增加的方式来实现的1.5
。使用这种方法,操作总数可以显示为O(n)
。
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)。
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开始,然后继续增加它。
private static final int DEFAULT_CAPACITY = 10
我实际上在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% + 1
为16
。在第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如上,从开始并在处10
增加每个调整大小50% + 1
。C#始于C#,4
并且更加积极地增加,每次调整大小都会加倍。1000000
以上是C#使用的添加示例3097084
操作。
将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时,其容量会自动增长。除了添加元素具有固定的摊销时间成本外,没有指定增长策略的细节。”
显然,如果您不知道要保持哪种范围,则设置大小可能不是一个好主意-但是,如果您确实有特定的范围,则设置初始容量将提高内存效率。
ArrayList可以包含许多值,并且在进行较大的初始插入时,可以告诉ArrayList开始分配更大的存储,以免在尝试为下一项分配更多空间时浪费CPU周期。因此,在开始时分配一些空间会更有效。
这是为了避免为每个对象进行重新分配而可能做出的努力。
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;
add
-它已经在内部使用了一些增长公式。因此,问题没有得到回答。
int newCapacity = (oldCapacity * 3)/2 + 1;
存在于ArrayList类中的。您是否仍未回答?
ArrayList
摊销的重新分配。问题是:为什么要对初始容量完全使用非标准值?除此之外:“线间阅读”不是技术答案中所需要的。;-)
我会说这是一种优化。没有初始容量的ArrayList将有约10个空行,并且在执行添加操作时会扩展。
要获得具有确切数量的项目的列表,您需要调用trimToSize()
根据我的经验ArrayList
,提供初始容量是避免重新分配成本的好方法。但它有一个警告。上面提到的所有建议都说,仅当知道元素数量的大概估计时,才应提供初始容量。但是,当我们尝试不加考虑地提供初始容量时,保留和未使用的内存量将是浪费,因为一旦列表填充到所需数量的元素,便可能永远不需要它。我的意思是,我们在开始分配容量时可以很务实,然后找到一种聪明的方式来知道运行时所需的最小容量。ArrayList提供了一种称为的方法ensureCapacity(int minCapacity)
。但是后来,人们找到了一种聪明的方法...
我测试了带有和不带有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上进行了测试