ArrayList
の通常のコンストラクターは次のとおりです。
ArrayList<?> list = new ArrayList<>();
ただし、初期容量のパラメーターを持つオーバーロードされたコンストラクターもあります。
ArrayList<?> list = new ArrayList<>(20);
必要に応じて追加できるのに、ArrayList
を初期容量で作成すると便利なのはなぜですか?
ArrayList
のサイズが事前にわかっている場合は、初期容量を指定する方が効率的です。これを行わないと、リストが大きくなるにつれて内部配列を繰り返し再割り当てする必要があります。
最終リストが大きいほど、再割り当てを避けることで時間を節約できます。
つまり、事前割り当てがなくても、n
の後ろにArrayList
要素を挿入すると、合計O(n)
時間を要することが保証されます。言い換えれば、要素の追加は償却された一定時間の操作です。これは、通常、1.5
の係数で、再割り当てごとに配列のサイズを指数関数的に増加させることで実現されます。このアプローチでは、操作の合計数 O(n)
と表示できます。
ArrayList
は 動的にサイズ変更する配列 データ構造であるため、初期(デフォルト)固定サイズの配列として実装されます。これがいっぱいになると、配列は倍のサイズに拡張されます。この操作はコストがかかるため、できるだけ少なくする必要があります。
したがって、上限が20アイテムであることがわかっている場合、初期長さ20の配列を作成する方がデフォルトの15などを使用してから15*2 = 30
にサイズ変更し、展開のサイクルを浪費しながら20だけを使用するよりも優れています。
追伸-AmitGが言うように、拡張係数は実装固有です(この場合は(oldCapacity * 3)/2 + 1
)
Arraylistのデフォルトサイズは1です。
/**
* 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に格納される要素の数について何か考えがある場合は、10から始めてからそれを増やすのではなく、そのサイズのArraylistを作成する方が良いでしょう。
私は実際に2か月前のトピックについて ブログ投稿 を書きました。この記事はC#のList<T>
を対象としていますが、JavaのArrayList
の実装も非常に似ています。 ArrayList
は動的配列を使用して実装されるため、要求に応じてサイズが大きくなります。したがって、容量コンストラクターの理由は最適化のためです。
これらのサイズ変更操作のいずれかが発生すると、ArrayListは配列の内容を、古い配列の2倍の容量の新しい配列にコピーします。この操作は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)
に、内部メモリの再割り当てが発生する回数を減らします。
例:
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のサイズが通常2倍になることです(ただし、新しいサイズは実装に依存することに注意してください)。以下は Oracle から引用されています。
「各ArrayListインスタンスには容量があります。容量は、リスト内の要素を格納するために使用される配列のサイズです。常に少なくともリストサイズと同じです。要素がArrayListに追加されると、その容量は自動的に増加します。成長ポリシーの詳細は、要素を追加すると一定の償却時間コストがかかるという事実以外には指定されません。」
明らかに、保持する範囲の種類がわからない場合、サイズを設定することはおそらく良い考えではありません-ただし、特定の範囲を念頭に置いている場合、初期容量を設定するとメモリ効率が向上します。
これは、すべての単一オブジェクトの再割り当ての労力を回避するためです。
int newCapacity = (oldCapacity * 3)/2 + 1;
内部的にnew Object[]
が作成されます。
JVMは、arraylistに要素を追加するときにnew Object[]
を作成する必要があります。再割り当てのための上記のコード(あなたが考える任意のアルゴリズム)がない場合は、arraylist.add()
を呼び出すたびにnew Object[]
を作成する必要がありますが、これは無意味です。 。したがって、次の式を使用してObject[]
のサイズを増やすことをお勧めします。
(JSLは、毎回1ずつ増加するのではなく、配列リストを動的に成長させるために以下に示す予測式を使用しました。成長するにはJVMの手間がかかるためです)
int newCapacity = (oldCapacity * 3)/2 + 1;
ArrayListには多くの値を含めることができ、大きな初期挿入を行う場合、次の項目により多くのスペースを割り当てようとするときにCPUサイクルを無駄にしないように、より大きなストレージを割り当てるようArrayListに指示できます。したがって、最初にスペースを割り当てる方が効率的です。
各ArrayListは、初期容量値「10」で作成されると思います。とにかく、コンストラクター内で容量を設定せずに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でテストしました