1. はじめに
プログラミングでは、大量のデータから必要な要素だけを選び出す場面が常にあります。これがフィルタリングです。たとえば偶数だけを残したい、"Java" という語を含む文字列を見つけたい、18 歳超のユーザーだけを選びたい——これらはすべてフィルタリングの課題です。
Java ではこの種の操作は至るところで登場するため、各種アプローチをしっかり扱え、その特性を理解しておくことが重要です。
2. ループによるフィルタリング
まずは最も基本的な方法——普通の for ループから始めましょう。この方法は命令的アプローチと呼ばれ、何をどうするかを明示的に記述します。可読性が高く、理解しやすいのが特徴です。
例: 偶数だけを残す
import java.util.*;
public class FilterExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> evenNumbers = new ArrayList<>(); // 結果用の新しいリストを作成する
for (Integer n : numbers) {
if (n % 2 == 0) { // 条件を確認: 偶数
evenNumbers.add(n);
}
}
System.out.println("偶数: " + evenNumbers);
}
}
結果:
偶数: [2, 4, 6, 8, 10]
ここでのポイント: 元のコレクション(numbers)は変更されません。結果用の新しいリストを作成しています。
例: 部分文字列による文字列のフィルタリング
List<String> words = Arrays.asList("java", "python", "javascript", "kotlin", "c++");
List<String> javaWords = new ArrayList<>();
for (String word : words) {
if (word.contains("java")) {
javaWords.add(word);
}
}
System.out.println(javaWords); // [java, javascript]
同じ考え方です: 文字列リストを走査し、メソッド contains で部分文字列の有無を確認します。
3. コレクションからの要素削除: そう単純ではない理由
どこに落とし穴があるのか?
時には元のコレクションから不要な要素を削除したくなるでしょう。しかし for-each ループで走査中にそれを行おうとすると、ConcurrentModificationException が発生します。
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, -2, 3, -4, 5));
for (Integer n : numbers) {
if (n < 0) {
numbers.remove(n); // 危険! ConcurrentModificationException!
}
}
結果:
Exception in thread "main" java.util.ConcurrentModificationException
このように for-each による反復中にコレクションを変更すると、イテレーションが破綻します。
コレクションから要素を正しく削除するには?
方法 1: Iterator.remove() を使う
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, -2, 3, -4, 5));
Iterator<Integer> it = numbers.iterator();
while (it.hasNext()) {
Integer n = it.next();
if (n < 0) {
it.remove(); // 安全に削除!
}
}
System.out.println(numbers); // [1, 3, 5]
ここではメソッド iterator() でイテレータを作成し、hasNext() と next() で進め、不要な要素は it.remove() を呼んで削除します。
方法 2: 必要な要素だけで新しいリストを作る
List<Integer> numbers = Arrays.asList(1, -2, 3, -4, 5);
List<Integer> positive = new ArrayList<>();
for (Integer n : numbers) {
if (n >= 0) {
positive.add(n);
}
}
System.out.println(positive); // [1, 3, 5]
元のコレクションには触れず、新しいリストを構築します。安全で分かりやすく、条件が複雑になっても拡張しやすい方法です。
方法 3: removeIf を使う(Java 8+)
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, -2, 3, -4, 5));
numbers.removeIf(n -> n < 0);
System.out.println(numbers); // [1, 3, 5]
条件を 1 行で渡します。「0 未満のものをすべて削除」。安全な変更のための内部処理はコレクション側が担います。
まとめ: 要素の削除には for-each を使わないでください。Iterator.remove()、新しいリストを作る方法、あるいは簡潔な removeIf から選びましょう。
4. フィルタリングでどの方法をいつ使うべきか?
通常のループによる命令的アプローチは、要素を選別するだけでなく、その場で何か処理(出力や変換など)も行いたいときに便利です。シンプルで分かりやすい方法です。
元のコレクションから削除する話であれば、for-each 中の削除は避けましょう。逐次的に制御したいなら Iterator.remove()、最も短く表現力が高いのは removeIf() です。
例: 単語リストから 4 文字未満の単語をすべて削除する:
List<String> words = new ArrayList<>(Arrays.asList("Java", "is", "fun", "awesome", "code"));
words.removeIf(word -> word.length() < 4);
System.out.println(words); // [Java, awesome, code]
メソッド removeIf は述語(フィルタの規則)を受け取り、それに一致するものを削除します。
5. コレクションのフィルタリングでよくあるミス
エラー1: ループ for-each 中にコレクションから要素を削除しようとする。
このコードは ConcurrentModificationException を引き起こします:
for (Integer n : numbers) {
if (n < 0) {
numbers.remove(n); // ドカン! ConcurrentModificationException
}
}
正しい選択肢: Iterator.remove() か removeIf を使う。
エラー2: フィルタ条件の書き方が誤っている。
負の数だけを削除したい場合:
List<Integer> numbers = new ArrayList<>(Arrays.asList(-3, -1, 0, 2, 4));
numbers.removeIf(n -> n < 0);
System.out.println(numbers); // [0, 2, 4]
しかし誤って「非正」にして <= を使うと、0 まで消えてしまいます:
numbers.removeIf(n -> n <= 0); // 結果: [2, 4](0 が誤って削除された)
条件の正確さに注意しましょう。比較を 1 つ間違えるだけで結果がまったく変わります。
GO TO FULL VERSION