初期容量でArrayListを開始するのはなぜですか?


149

の通常のコンストラクタArrayListは次のとおりです。

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

ただし、初期容量のパラメータを持つオーバーロードされたコンストラクタもあります。

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

必要にArrayList応じて追加できるのに、初期容量を持つを作成すると便利なのはなぜですか?


17
ArrayListソースコードを見てみましたか?
AmitG 2013年

@Joachim Sauer:ソースを注意深く読んだとき、時々私たちは認識を得ます。彼がソースを読んだなら、私は試してみました。私はあなたの側面を理解しました。ありがとう。
AmitG 2013年

ArrayListはパフォーマンスが悪い期間です。なぜこのような構造を使用するのですか
PositiveGuy

回答:


196

事前にサイズをご存知の場合 ArrayListは、初期容量を指定する方が効率的です。これを行わないと、リストが大きくなるにつれて内部配列を繰り返し再割り当てする必要があります。

最終的なリストが大きいほど、再割り当てを回避することにより、時間を節約できます。

つまり、事前割り当てがなくても、n要素の後ろに要素を挿入ArrayListすると、合計O(n)時間がかかることが保証されます。つまり、要素の追加は、償却済みの一定時間操作です。これは、各再割り当てで配列のサイズを指数的に、通常は倍に増やすことで実現され1.5ます。このアプローチを使用すると、操作の合計数をと表示できますO(n)


5
既知のサイズを事前に割り当てることは良い考えですが、行わないことは通常ひどいことではありません。最終的なサイズがnであるリストには、log(n)の再割り当てが必要ですが、多くはありません。
Joachim Sauer 2013年

2
@PeterOlson O(n log n)log n作業n時間を実行します。これはかなり大きく見積もられています(ただし、上限がOであるため、技術的には大きな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であるか、小さな定数をとる可能性があります)。この合計がせいぜいnよりも大きい一定の因子であることを確認するには、目を細めすぎます。したがって、O(n)であるO(k * n)コピーを取ります。

1
@delnan:それを主張することはできません!;)ところで、私はあなたの細かい議論が本当に好きでした。トリックのレパートリーに追加します。
NPE 2013年

6
議論を2倍にする方が簡単です。1つの要素から始めて、満杯になったときに2倍にするとします。8つの要素を挿入するとします。1つ挿入します(コスト:1)。2つ挿入-ダブル、1つの要素をコピーして2つ挿入(コスト:2)。3つ挿入-ダブル、2つの要素をコピー、3つ挿入(コスト:3)。4つ挿入します(コスト:1)。5つ挿入-ダブル、4つの要素をコピー、5つ挿入(コスト:5)。6、7、8を挿入します(コスト:3)。合計コスト:1 + 2 + 3 + 1 + 5 + 3 =16。これは挿入される要素の数の2倍です。このスケッチから、平均コストが一般的にインサートあたり2であることを証明できます。
Eric Lippert 2013年

9
それは時間のコストです。また、時間の経過に伴って無駄なスペースの量が変化し、時々0%で100%に近いこともわかります。係数を2から1.5、4、100などに変更すると、無駄なスペースの平均量とコピーに費やされた平均時間の量が変わりますが、時間の複雑さは、係数が何であっても、平均して線形のままです。
Eric Lippert 2013年

41

ためArrayListである動的にリサイズアレイが初期(デフォルト)固定サイズのアレイとして実現されるデータ構造は、。これがいっぱいになると、配列は倍のサイズに拡張されます。この操作はコストがかかるため、できる限り少なくする必要があります。

したがって、上限が20アイテムであることがわかっている場合は、初期の長さを20にして配列を作成する方が、デフォルトの15を使用するよりもサイズを変更して15*2 = 30、拡張のサイクルを無駄にしながら20だけを使用するよりも優れています。

PS-AmitGが言うように、拡張係数は実装固有です(この場合(oldCapacity * 3)/2 + 1)。


9
それは実際にint newCapacity = (oldCapacity * 3)/2 + 1;
AmitG 2013年

25

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に格納される要素の数について何か考えがある場合は、10から始めてそれを増やすのではなく、そのサイズのArraylistを作成することをお勧めします。


今後のJDKバージョンのデフォルト容量が常に10になる保証はありませんprivate static final int DEFAULT_CAPACITY = 10
vikingsteve

17

私は実際に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します。次に、必要な容量がすでにとして知られているリストを作成する例を考えてみましょう1000000ArrayListサイズコンストラクタなしでを作成すると、ArrayList.add 1000000通常はO(1)またはサイズ変更時にO(n)がかかる時間。

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

コンストラクタを使用してこれを比較してから、 ArrayList.add O(1)で実行することが保証されているをします。

1000000 + 1000000 = 2000000オペレーション

JavaとC#

Javaは上記のとおりで、から始まり、10それぞれのサイズ変更を増やします50% + 1。C#は、4サイズを変更するたびに2倍になり、より積極的に増加します。の1000000上記のC#の使用例追加の例3097084操作をしています。

参考文献


9

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に追加されると、その容量は自動的に増加します。成長ポリシーの詳細は、要素を追加すると償却費が一定になるという事実を超えて規定されていません。」

明らかに、保持する範囲の種類がわからない場合は、サイズを設定することはおそらくお勧めできません。ただし、特定の範囲を念頭に置いている場合は、初期容量を設定するとメモリ効率が向上します。


3

ArrayListには多くの値を含めることができ、最初の大きな挿入を行う場合、次の項目にさらにスペースを割り当てようとするときにCPUサイクルを無駄にしないように、最初に大きなストレージを割り当てるようにArrayListに指示できます。したがって、最初にある程度のスペースを割り当てる方が効率的です。


3

これは、すべてのオブジェクトの再割り当てにかかる可能性のある作業を回避するためです。

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

内部的new Object[]に作成されます。arraylistに要素を追加
するnew Object[]とき、JVMは作成する労力を必要とします 。あなたが再割り当てのために(あなたが考えるすべてのALGO)のコードの上に持っていない場合は、あなたが呼び出すたびはarraylist.add()その後、new Object[]無意味である作成する必要があり、我々はそれぞれ追加されるすべてのオブジェクトに対して1によってサイズを大きくするための時間を失うされています。したがってObject[]、次の式でサイズを大きくすることをお勧めします。
(JSLは毎回1ずつ増加するのではなく、動的に成長する配列リストに以下のフォーキャスト式を使用しました。成長するにはJVMによる労力がかかるため)

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

ArrayListは、シングルごとに再割り当てを実行しませんadd -既に内部的にいくつかの増加式を使用しています。したがって、質問には答えられません。
AH

@AH私の答えは否定的なテストです。行の間で親切に読んでください。私は、「再割り当て用の上記のコード(考えているアルゴ)がない場合、arraylist.add()を呼び出すたびに、無意味な新しいObject []を作成する必要があり、時間が失われます。」そしてコードがあるint newCapacity = (oldCapacity * 3)/2 + 1;ArrayListクラスに存在します。それでも答えられないと思いますか?
AmitG 2013年

1
私はまだ答えられていないと思います:ArrayList償却後の再割り当てでは、初期容量の値がどのような場合でも発生します。そして問題は、初期容量に非標準の値を使用するのはなぜですか?これに加えて、「行の間を読む」ことは、技術的な回答では望ましいものではありません。;-)
AH

@AH私が答えているのは、ArrayListに再割り当てプロセスがないとどうなるかです。答えはそうです。答えの精神を読んでみてください:-)。私がよく知っている 償却再配分が初期容量のために任意の値を持つどのような場合に行われますArrayListのでは。
AmitG 2013年

2

各ArrayListは「10」の初期容量値で作成されていると思います。したがって、とにかく、コンストラクタ内で容量を設定せずにArrayListを作成すると、デフォルト値で作成されます。


2

それは最適化だと思います。初期容量のないArrayListには最大10行の空行があり、追加を実行すると展開されます。

正確な数のアイテムのリストを取得するには、trimToSize()を呼び出す必要があります


0

での私の経験によればArrayList、初期容量を指定することは、再割り当てコストを回避する良い方法です。ただし、注意が必要です。上記のすべての提案は、要素数の大まかな見積もりがわかっている場合にのみ初期容量を提供する必要があると述べています。しかし、何も考えずに初期容量を指定しようとすると、予約済みのメモリと未使用のメモリの量は無駄になります。リストが必要な数の要素でいっぱいになると、必要になることはないからです。私が言っていることは、容量を割り当てるときに最初から実用的であり、実行時に必要な最小容量を知るスマートな方法を見つけることができるということです。ArrayListはと呼ばれるメソッドを提供しますensureCapacity(int minCapacity)。しかし、その後、スマートな方法を見つけました...


0


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でテストしました


1
こんにちは、残念ながらcurrentTimeMillisの許容誤差は最大100ミリ秒(依存)であり、結果の信頼性はほとんどありません。それを正しく行うには、カスタムライブラリを使用することをお勧めします。
ボグダン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.