Java | Java 標準ライブラリ:コレクションの初期容量

Java Java
スポンサーリンク

「コレクションの初期容量」ってそもそも何?

まずことばの整理からいきます。

ArrayListHashMap などのコレクションは、内部で
「ある程度まとめてメモリを確保してから」
そこに要素を入れていきます。

この「最初に用意しておく入れ物の大きさ」が
初期容量(initial capacity) です。

たとえば new ArrayList<>() とすると、
「中で使う配列の最初の長さ」が決まります。
足りなくなってきたら、その都度「もっと大きい配列を作ってコピーし直す」
という処理が走ります。

ポイントはここです。

どれくらい入るか分からないからとりあえず小さめに作る
→ 後で足りなくなって何回も「拡張+コピー」が走る

どれくらい入るかあらかじめ分かっている
→ 最初から十分な容量で作っておけば、無駄な拡張が減る

初期容量は、この「拡張+コピー」(や HashMap なら「再ハッシュ」)の回数を減らして
無駄な処理を抑えるためのチューニングポイントです。


ArrayList の初期容量と「拡張コスト」

new ArrayList<>() は中でどう動いているか

ArrayList は内部的に配列を持っていて、
その配列の長さが「今使える容量」です。

次の 2 つの書き方を比べてみます。

List<String> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>(1000);
Java

list1 は「容量指定なし」
list2 は「1000 個ぶんの容量を最初から確保」

という意味です。

list1add を繰り返していくと、
あるタイミングで内部配列が足りなくなり、
「より大きな配列を作って、元の配列の中身を全部コピーする」
という処理が何度か走ります。

これは自分で for 文を書かなくても勝手にやってくれますが、
「重い処理であることは変わらない」というのがミソです。

大量に入ることが分かっているなら、最初から容量を指定する

たとえば 1 万件のレコードを読み込むと分かっているなら、
次のように書いたほうが、無駄が減ります。

int expectedSize = 10_000;
List<String> list = new ArrayList<>(expectedSize);

for (int i = 0; i < expectedSize; i++) {
    list.add("data-" + i);
}
Java

容量を 1 万にしておけば、
途中で「配列足りないからコピーし直し」が起こる回数がかなり減ります。

逆に、数件しか入らないのにやたら大きな容量で作ると、
メモリだけ余分に食うので、それもまた無駄です。

実務感覚としては、

おおよその件数が分かっているなら、そのくらいを初期容量に指定
全く読めないなら、デフォルトのままで気にしない

くらいで十分です。


HashMap / HashSet の初期容量と「再ハッシュ」の話

HashMap の内部で何が起きているか

HashMap は内部で「バケット」と呼ばれる配列のようなものを持っています。
このバケットの長さが、初期容量に対応します。

Map<String, Integer> map1 = new HashMap<>();
Map<String, Integer> map2 = new HashMap<>(1024);
Java

map1 は容量指定なし、map2 は「バケット数を 1024 からスタート」というイメージです。

要素をどんどん put していくと、
ある割合を超えたあたりで「再ハッシュ(resize + rehash)」が走ります。

再ハッシュとは、ざっくりいうと

もっと大きいバケット配列を新しく用意する
すでに入っているすべてのキーについて、
新しい配列のどこに入るべきかを計算し直して入れ直す

という処理です。

当然ながら、これはそれなりに重いです。

たくさん入るのが分かっているなら、最初から大きめにしておく

たとえば、「ユーザー 5 万人ぶんの ID → ユーザー情報」を Map に詰める処理があるとします。

Map<String, User> users = new HashMap<>();
// or
Map<String, User> users = new HashMap<>(50_000);
Java

前者は途中で何度か「再ハッシュ」しながらサイズアップしていきます。
後者は、最初からだいたい足りるサイズでスタートするので、
再ハッシュの回数を減らせます。

これもまた、

件数が読めるなら初期容量に反映
読めないならデフォルトでもいい

というバランスです。


「初期容量」と「サイズ」の違いを混同しないこと

ここで一つ、よく混ざるポイントを整理します。

ArrayList で考えると分かりやすいです。

List<String> list = new ArrayList<>(100);
Java

このとき、

今の「サイズ」(size) → 0
内部配列の「容量」 → 100

です。

size() は実際に入っている要素の数。
初期容量は「入る準備ができている最大数」であって、
入っている数とは別です。

サイズ 0 で容量 100 のリストに、1 個 add しただけでは
「99 個分の空き箱を持っているリスト」状態です。

Map でも同様で、

size() は「put 済みのキーの数」
初期容量は「バケットの数」であって、キー数とは一致しない

ここをごちゃ混ぜにしないことが大事です。


いつ初期容量を意識すべきか/しなくていいか

気にしなくていい場面

プログラミング初心者のうちは、
まずは「正しく動くこと」を優先するべきです。

要素数が数十〜数百程度
パフォーマンスが全く問題になっていない
実験・学習・小さめのツール

こういうケースでは、初期容量を気にする必要はほぼありません。

new ArrayList<>()
new HashMap<>()

と素直に書いておけばよく、
「動くようになってから、必要なら見直す」で十分です。

初期容量を意識すると「一段上に行ける」場面

逆に、こういうケースでは初期容量を考える価値が大きくなります。

外部システムや DB から、何件ぐらいデータを取得するかが分かっている
バッチ処理などで、何万件〜何十万件クラスのデータを扱う
性能をチューニングしていて、「オブジェクトの追加がボトルネックになっている」と分かった

このとき、

「どうせ 5 万件入るなら、最初から 5 万くらいの容量で用意しておこう」

と書き換えるだけで、無駄な拡張が激減して
処理時間が目に見えて変わることがあります。


初期容量を指定する書き方まとめと、考え方の軸

最後に、書き方と考え方をコンパクトに整理します。

ArrayList の初期容量指定:

List<String> list = new ArrayList<>(expectedSize);
Java

HashMap / HashSet の初期容量指定:

Map<String, String> map = new HashMap<>(expectedSize);
Set<String> set = new HashSet<>(expectedSize);
Java

「expectedSize」は、ざっくりの見積もりで構いません。

大事なのは、

  • サイズ(size)と容量(capacity)は別物
  • 容量を大きくしすぎるとメモリの無駄、小さすぎると拡張コストが増える
  • データ件数が読めるときだけ、初期容量をきちんと指定する価値が高い

という三つの感覚です。

タイトルとURLをコピーしました