Javaでコレクションを使う際、「Set」と「List」のどちらを選ぶべきか迷ったことはありませんか?
どちらも要素の集まりを管理するインタフェースですが、内部構造や特性が異なるため、同じ操作でも処理性能に大きな差が出ることがあります。
この記事では、Set(HashSet)とList(ArrayList)に対して、「追加(add)」「検索(contains)」「削除(remove)」という基本操作の実行速度を比較し、それぞれの特性を明らかにします。
大量データを扱う場面では、こうした違いがアプリケーション全体のパフォーマンスに大きく影響します。処理内容に応じた最適なコレクション選定の参考になれば幸いです。
計測環境
各操作の処理時間を、以下の環境で実測しました。
- Javaバージョン:AdoptOpenJDK 11
- 実行マシン:Intel Core i7 / 16GB RAM
計測結果
add(追加)
0から1000万未満の整数を順に追加する処理を比較しました。
// Setの計測
Set<Integer> set = new HashSet<>();
long start1 = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
set.add(i);
}
long end1 = System.currentTimeMillis();
System.out.println("Set: " + (end1 - start1) + "ms");
// Listの計測
List<Integer> list = new ArrayList<>();
long start2 = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
list.add(i);
}
long end2 = System.currentTimeMillis();
System.out.println("List: " + (end2 - start2) + "ms");
Set: 3878ms
List: 1388ms
結果:追加処理では、Listの方が圧倒的に高速でした。
これは、HashSet
が要素をハッシュ化して管理するため、追加時にハッシュ計算や重複チェックが発生するためと考えられます。
contains(検索)
0〜99999の値を格納した後、ランダムな整数が含まれているかを10万回チェックする処理を比較しました。
// Setの計測
Set<Integer> set = new HashSet<>();
for (int i = 0; i < 100000; i++) {
set.add(i);
}
long start1 = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
set.contains((int) (Math.random() * set.size()));
}
long end1 = System.currentTimeMillis();
System.out.println("Set: " + (end1 - start1) + "ms");
// Listの計測
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
list.add(i);
}
long start2 = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
list.contains((int) (Math.random() * list.size()));
}
long end2 = System.currentTimeMillis();
System.out.println("List: " + (end2 - start2) + "ms");
Set: 16ms
List: 3658ms
結果:Setが圧倒的に高速です。
HashSet
ではハッシュによる高速検索が可能なのに対し、ArrayList
では線形探索が行われるため、要素数が増えるほど処理時間が長くなります。
remove(削除)
0〜99999の整数を格納した後、ランダムに一致する値を10万回削除する処理を比較しました。
// Setの計測
Set<Integer> set = new HashSet<>();
for (int i = 0; i < 100000; i++) {
set.add(i);
}
long start1 = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
set.remove(Integer.valueOf((int) (Math.random() * 100000)));
}
long end1 = System.currentTimeMillis();
System.out.println("Set: " + (end1 - start1) + "ms");
// Listの計測
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
list.add(i);
}
long start2= System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
list.remove(Integer.valueOf((int) (Math.random() * 100000)));
}
long end2 = System.currentTimeMillis();
System.out.println("List: " + (end2 - start2) + "ms");
Set: 15ms
List: 3891ms
結果:削除処理もSetの方が非常に高速でした。
HashSet
では要素の位置に関係なく削除できますが、ArrayList
では削除後に後続要素のシフト処理が発生するため、コストが高くなります。
※本結果は特定の環境・タイミングでの計測に基づく参考値です。実際のパフォーマンスは、データ構造の初期容量や実行時のヒープ状況などにも左右されるため、目的や環境に応じた検証が推奨されます。
まとめ:用途に応じて使い分けよう!
本記事では、JavaにおけるSet(HashSet)とList(ArrayList)の基本操作に対する処理速度を比較しました。
その結果、「add」操作ではListが高速であり、「contains」や「remove」などの検索・削除系の処理ではSetが圧倒的に高速であることが確認できました。
実用的な選択指針
開発現場でコレクションを選択する際の判断基準として、以下を参考にしてください。
Setを選ぶべき場面
- 頻繁な検索処理(contains)が必要な場合
- 要素の削除を多用する場合
- データの重複を排除したい場合
- 要素の存在確認がメイン処理の場合
Listを選ぶべき場面
- 要素の順次追加がメイン処理の場合
- 要素の順序を保持する必要がある場合
- インデックスによるアクセスが必要な場合
- 重複要素を許可したい場合
パフォーマンス最適化のポイント
処理内容に応じて適切なコレクションを選択することは、アプリケーション全体のパフォーマンス最適化に直結します。特に、大量データを扱う場面では、その差が顕著に表れるでしょう。
ただし、Setは順序を保証せず重複を許容しない一方で、Listは順序を保持し重複も可能です。処理速度だけでなく、データの特性や要件に応じて選択することが重要です。
また、初期容量の設定やガベージコレクションの影響なども考慮し、実際の運用環境での検証を行うことをお勧めします。
今後の開発でコレクションを選ぶ際の判断材料として、この記事が参考になれば幸いです。