コレクションの初期容量指定(負荷低減) — パフォーマンスチューニング
「どれくらい要素が入るか」をあらかじめ見積もれるなら、初期容量を指定しておくと再割り当て(拡張)や再ハッシュを減らせます。結果としてメモリ確保やコピーの回数が減り、処理が軽くなります。初心者向けに、代表コレクションのプリサイズ方法とコツをまとめます。
なぜ初期容量が効くのか
- 割り当て回数の削減: 追加のたびに容量が足りなくなると、内部配列の拡張(コピー)が発生する。最初に十分な容量を確保すると、そのコストを避けられる。
- 再ハッシュの回避:
HashMapやHashSetは一定以上要素が増えると再ハッシュ(全エントリ再配置)。十分な容量で開始すれば不要な拡張を抑えられる。 - スループット向上: 大量追加やバッチ処理の場面で顕著に効く。
代表コレクションの初期容量指定
ArrayList(可変長配列)
// 例: 1万件入れる見込みなら
List<String> list = new ArrayList<>(10_000);
// 既存のリストに後から容量だけ確保
list.ensureCapacity(20_000);
Java- ポイント: 作成時の推定件数で十分。のちの拡張コピーを減らす。
HashMap / LinkedHashMap(ハッシュマップ)
// 例: 10,000件のキーを入れる見込み
// 「容量」は要素数に対して少し大きめにする(負荷係数 0.75 前提)
int expectedSize = 10_000;
int initialCapacity = (int) Math.ceil(expectedSize / 0.75);
Map<String, Integer> map = new HashMap<>(initialCapacity, 0.75f);
// 順序保持が必要なら LinkedHashMap でも同様
Map<String, Integer> lmap = new LinkedHashMap<>(initialCapacity, 0.75f);
Java- ポイント: 負荷係数(load factor)既定は 0.75。再ハッシュを避けるため「期待要素数 ÷ 0.75」程度で開始するとよい。
HashSet(ハッシュセット)
int expectedSize = 5_000;
int initialCapacity = (int) Math.ceil(expectedSize / 0.75);
Set<String> set = new HashSet<>(initialCapacity, 0.75f);
Java- ポイント: 実体は
HashMap。同じ考え方でプリサイズ。
ArrayDeque(両端キュー)
// 初期サイズ(内部配列容量)
Deque<String> dq = new ArrayDeque<>(1_024);
Java- ポイント: 大量追加前に「ざっくり大きめ」で開始。
ConcurrentHashMap(並行マップ)
int expectedSize = 20_000;
int initialCapacity = (int) Math.ceil(expectedSize / 0.75);
ConcurrentHashMap<String, Long> cmap = new ConcurrentHashMap<>(initialCapacity);
Java- ポイント: 期待件数が大きい場合はプリサイズで再構築コストを抑制。
具体例と練習
例題1: CSV から10万件読み込み(Listをプリサイズ)
int rows = 100_000; // 事前にわかっている/見積もれる
List<String> lines = new ArrayList<>(rows);
try (var br = new java.io.BufferedReader(new java.io.FileReader("data.csv"))) {
String line;
while ((line = br.readLine()) != null) {
lines.add(line);
}
}
// 大量でも拡張コピーの回数を抑えられる
Java例題2: ID→集計値(HashMapをプリサイズ)
int expectedIds = 50_000;
int cap = (int) Math.ceil(expectedIds / 0.75);
Map<String, Integer> counts = new HashMap<>(cap, 0.75f);
for (var record : records) {
counts.merge(record.id(), 1, Integer::sum);
}
Java例題3: ユニークタグ抽出(HashSetをプリサイズ)
int expectedTags = 10_000;
int cap = (int) Math.ceil(expectedTags / 0.75);
Set<String> tags = new HashSet<>(cap, 0.75f);
for (var post : posts) {
tags.addAll(post.tags());
}
Java実務のコツ(見積もりの仕方)
- ラフでOK: 正確でなくてよい。見積もりより少し大きめが安全。
- サイズが不明なら: 一旦追加後に、長く使うコレクションには
ensureCapacity(ArrayList)で増やす。 - 負荷係数の調整: 衝突が多いキー集合や厳密に低レイテンシが必要なら、
HashMapの負荷係数を下げて(例: 0.5)再ハッシュしづらくする。ただしメモリ使用量は増える。 - ストリーム収集時: 事前サイズがわかるなら、まずプリサイズのコレクションを作ってから
forEach(add)のほうが楽。collect(toList())は内部の初期容量は制御しづらい。
テンプレート集(そのまま使える形)
- ArrayList を期待サイズで作成
List<T> list = new ArrayList<>(expectedSize);
Java- HashMap を再ハッシュ回避の初期容量で作成
int cap = (int) Math.ceil(expectedSize / 0.75);
Map<K, V> map = new HashMap<>(cap, 0.75f);
Java- HashSet(HashMap と同様)
int cap = (int) Math.ceil(expectedSize / 0.75);
Set<E> set = new HashSet<>(cap, 0.75f);
Java- ArrayDeque を大きめに開始
Deque<E> dq = new ArrayDeque<>(initialCapacity);
Java- List の後追い拡張
list.ensureCapacity(minCapacity);
Javaよくある落とし穴と回避策
- 落とし穴: 初期容量=期待件数(HashMap系)
- 回避:
期待件数 ÷ 負荷係数(既定 0.75)で少し大きめに。キーが多いと結局拡張する。
- 回避:
- 落とし穴: 不要な巨大プリサイズ
- 回避: メモリ浪費やGC増加につながる。見積もりは「ほどほど」に。
- 落とし穴:
toList()に任せてしまう- 回避: 大量データでは自前コレクションに
ensureCapacityしてからforEach(add)。
- 回避: 大量データでは自前コレクションに
- 落とし穴: 連想配列の負荷係数を無自覚に変更
- 回避: 0.75 はバランス良好。要件(性能/メモリ)を見て慎重に調整。
まとめ
- 初期容量の指定は「大量追加時の配列コピーや再ハッシュ」を減らす即効テク。
ArrayListは見積もりサイズで作成、HashMap/HashSetは「期待件数 ÷ 負荷係数」を目安に。- ほどよく大きめに始め、必要なら
ensureCapacityを組み合わせる。大量・バッチ処理では特に効果が出る。
