[英]Why start an ArrayList with an initial capacity?
ArrayList
的常用構造函數是:
ArrayList<?> list = new ArrayList<>();
但是還有一個重載的構造函數,它的初始容量帶有一個參數:
ArrayList<?> list = new ArrayList<>(20);
當我們可以隨心所欲地附加到它時,為什么創建一個具有初始容量的ArrayList
很有用?
如果您事先知道
ArrayList<\/code>的大小,那么指定初始容量會更有效。
如果你不這樣做,隨着列表的增長,內部數組將不得不重復重新分配。
最終列表越大,通過避免重新分配節省的時間就越多。
也就是說,即使沒有預先分配,在
ArrayList<\/code>的后面插入
n<\/code>元素也可以保證總共花費
O(n)<\/code>時間。
換句話說,附加一個元素是一個攤銷的常數時間操作。
這是通過讓每個重新分配以指數方式增加數組的大小來實現的,通常是
1.5<\/code> 。
使用這種方法,操作總數
可以顯示為
O(n)<\/code><\/a> 。
因為ArrayList
是一個動態調整大小的數組數據結構,這意味着它被實現為一個具有初始(默認)固定大小的數組。 當它被填滿時,數組將被擴展為一個雙倍大小的數組。 此操作成本高昂,因此您希望盡可能少。
因此,如果您知道上限是 20 個項目,那么創建初始長度為 20 的數組比使用默認值(例如 15)更好,然后將其調整為15*2 = 30
並且僅使用 20 而浪費周期為擴展。
PS - 正如 AmitG 所說,擴展因子是特定於實現的(在這種情況下是(oldCapacity * 3)/2 + 1
)
Arraylist 的默認大小為10<\/strong> 。
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this(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
將調用ArrayList.add
1000000
次,這通常需要O(1)或O(n)調整大小。
1000000 + 16 + 25 + ... + 670205 + 1005308 = 4015851 次操作
使用構造函數進行比較,然后調用保證在O(1)中運行的ArrayList.add
。
1000000 + 1000000 = 2000000 次操作
Java 同上,從10
開始,每次調整大小增加50% + 1
。 C# 從4
開始,並且增加得更加積極,每次調整大小都會翻倍。 上面的1000000
為 C# 添加示例使用3097084
操作。
將 ArrayList 的初始大小設置為
ArrayList<>(100)<\/code>可以減少重新分配內部存儲器的次數。
例子:<\/strong>
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 在開始時分配更大的存儲空間,以免在嘗試為下一項分配更多空間時浪費 CPU 周期。 因此,在開始時分配一些空間更有效。
"這是為了避免為每個對象重新分配可能的努力。
int newCapacity = (oldCapacity * 3)/2 + 1;
在內部創建了new Object[]
。
當您在數組列表中添加元素時,JVM 需要努力創建new Object[]
。 如果您沒有上述代碼(您認為的任何算法)進行重新分配,那么每次調用arraylist.add()
時都必須創建new Object[]
,這是沒有意義的,我們正在浪費時間將大小增加 1每個要添加的對象。 所以最好用下面的公式來增加Object[]
的大小。
(JSL 使用下面給出的預測公式來動態增長 arraylist,而不是每次增長 1。因為增長需要 JVM 的努力)
int newCapacity = (oldCapacity * 3)/2 + 1;
我認為每個 ArrayList 的初始容量值為“10”。 所以無論如何,如果您創建一個 ArrayList 而不在構造函數中設置容量,它將使用默認值創建。
"我會說它是一種優化。 沒有初始容量的 ArrayList 將有大約 10 個空行,並且會在您進行添加時擴展。
根據我對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 上測試過
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.