Java Listのフィルタリング完全解説!Stream APIとラムダ式でスマートに条件抽出
生徒
「JavaのListの中にあるたくさんのデータから、特定の条件に合うものだけを取り出したいんですけど、どうすればいいですか?」
先生
「Java 8から導入されたStream APIとラムダ式を使うのが一番スマートな方法ですよ。昔はfor文とif文を組み合わせて書いていましたが、今はもっと直感的に記述できるんです。」
生徒
「Stream APIとかラムダ式って難しそうですね…。具体的にどうやって書くんですか?」
先生
「最初は少し戸惑うかもしれませんが、型を覚えると驚くほどコードがスッキリします。基本の書き方から一緒に学んでいきましょう!」
1. Java Listのフィルタリングとは何か
Javaのプログラミングにおいて、コレクション(ListやSetなど)に格納された膨大なデータから、特定の条件を満たす要素だけを抽出する操作を「フィルタリング」と呼びます。例えば、顧客リストから20歳以上の人だけを抽出したり、商品リストから在庫があるものだけを選び出したりといった処理がこれに当たります。
従来のJavaでは、拡張for文の中でif文を使って条件判定を行い、新しいリストに要素を追加していくという手順が必要でした。しかし、この方法ではコードの行数が多くなり、何をしたいのかという意図が埋もれてしまいがちです。最新のJava開発では、Stream APIを活用することで、宣言的に、かつ簡潔にフィルタリングを行うのが一般的となっています。
2. Stream APIとラムダ式の基本構造
フィルタリングを理解する上で欠かせないのが「Stream API」と「ラムダ式」です。Stream APIはデータの集合を一連の流れ(ストリーム)として扱い、その中で「加工」や「抽出」などの処理を連結して記述できる仕組みです。そして、その処理内容を短く表現するための記法がラムダ式です。
基本的な構文は「リスト名.stream().filter(要素 -> 条件式).collect(Collectors.toList())」となります。この一連の流れを覚えるだけで、ほとんどのフィルタリング処理が完結します。まずは、リストをStreamに変換し、filterメソッドで絞り込み、最後に再びList形式に戻すという三段階のステップを意識しましょう。これにより、一時的な変数を減らし、読み取りやすいプログラムを書くことができます。
3. 文字列リストを特定の条件で抽出する
具体的なコード例を見てみましょう。ここでは、文字列のリストから特定の文字で始まる要素だけを抽出する最も基本的なパターンを紹介します。例えば、果物の名前が入ったリストから「あ」で始まるものだけを取り出す処理です。この方法は、検索機能の実装などに非常によく使われます。
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class StringFilterExample {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("apple");
fruits.add("banana");
fruits.add("apricot");
fruits.add("cherry");
// "a"で始まる要素だけをフィルタリング
List<String> filteredList = fruits.stream()
.filter(name -> name.startsWith("a"))
.collect(Collectors.toList());
System.out.println("元のリスト: " + fruits);
System.out.println("抽出後のリスト: " + filteredList);
}
}
元のリスト: [apple, banana, apricot, cherry]
抽出後のリスト: [apple, apricot]
このコードでは、filterメソッドの中でstartsWithという条件を使用しています。ラムダ式のnameはリスト内の各要素を指しており、その要素が条件を満たす場合のみ次の処理へ渡されます。
4. 数値リストで範囲指定フィルタリングを行う
次に、数値(Integer)のリストを扱う方法を見てみましょう。数値のフィルタリングでは、比較演算子を使って「〇〇以上」や「〇〇未満」といった範囲指定を頻繁に行います。Stream APIを使えば、複数の条件を組み合わせることも非常に簡単です。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class NumberFilterExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(10, 55, 23, 89, 4, 101, 60);
// 50以上の数字だけを抽出
List<Integer> highNumbers = numbers.stream()
.filter(n -> n >= 50)
.collect(Collectors.toList());
System.out.println("50以上の数値: " + highNumbers);
// 20以上かつ80以下の範囲で抽出
List<Integer> rangeNumbers = numbers.stream()
.filter(n -> n >= 20 && n <= 80)
.collect(Collectors.toList());
System.out.println("20以上80以下の数値: " + rangeNumbers);
}
}
50以上の数値: [55, 89, 101, 60]
20以上80以下の数値: [55, 23, 60]
このように、複数の条件を論理演算子(&&など)でつなげることで、複雑な条件抽出も一行で記述できるようになります。コードの可読性が格段に向上していることがわかります。
5. 自作クラス(オブジェクト)のリストを操作する
実際の開発現場では、StringやIntegerだけでなく、自分で定義したクラスのオブジェクトをリストにして管理することがほとんどです。例えば「Userクラス」や「Productクラス」のリストから、特定の属性を持つオブジェクトを抽出する場合です。この場合も基本的な書き方は同じですが、メソッド参照やフィールドへのアクセスが必要になります。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
public int getAge() { return age; }
public String getName() { return name; }
@Override
public String toString() { return name + "(" + age + ")"; }
}
public class ObjectFilterExample {
public static void main(String[] args) {
List<User> users = Arrays.asList(
new User("田中", 25),
new User("佐藤", 18),
new User("鈴木", 30),
new User("高橋", 15)
);
// 20歳以上のユーザーだけを抽出
List<User> adults = users.stream()
.filter(u -> u.getAge() >= 20)
.collect(Collectors.toList());
System.out.println("成人ユーザー: " + adults);
}
}
成人ユーザー: [田中(25), 鈴木(30)]
オブジェクトのリストをフィルタリングするときは、ラムダ式の引数(ここではu)からゲッターメソッドを呼び出して条件判定を行います。これにより、特定の属性に基づいた高度なデータ抽出が可能になります。
6. フィルタリング後の加工処理(mapメソッド)との連携
フィルタリング(抽出)した後に、そのデータの一部だけを取り出したり加工したりしたいケースも多いです。その場合はfilterの後にmapメソッドを繋げます。例えば、「20歳以上のユーザーを抽出して、その名前だけのリストを作る」という処理です。これこそがStream APIの真骨頂と言える機能です。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class FilterAndMapExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Yamada", "Tanaka", "Sato", "Suzuki");
// 文字数が5文字より長い名前を抽出して、すべて大文字に変換する
List<String> result = names.stream()
.filter(s -> s.length() > 5)
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println("変換結果: " + result);
}
}
変換結果: [YAMADA, TANAKA, SUZUKI]
このように処理をパイプラインのように連結していくことで、非常に複雑なデータ変換ロジックを簡潔に表現できます。map(String::toUpperCase)のような記述は「メソッド参照」と呼ばれ、ラムダ式をさらに短く書くための手法です。
7. 抽出した要素をList以外の形式で受け取る方法
これまでは最後にCollectors.toList()を使って再びリストに戻していましたが、他の形式で結果を受け取ることも可能です。例えば、条件に一致する最初の一個だけを取得したり、カンマ区切りの文字列として結合したり、あるいは単純に個数を数えたりすることができます。
個数を数える場合はcount()メソッドを使い、最初の一つを取得する場合はfindFirst()を使います。特にfindFirst()は、結果が存在しない可能性を考慮してOptional型という特別な箱に包まれて返されます。これにより、NullPointerException(ヌルポ)という有名なエラーを安全に回避することができるようになっています。状況に応じて適切な終端操作を選択しましょう。
8. フィルタリング時に注意すべきパフォーマンスのポイント
Stream APIは非常に便利ですが、使い所には注意が必要です。非常に大規模なデータを扱う場合、並列処理を行うparallelStream()という選択肢もありますが、通常の小さなリストであれば通常のstream()の方がオーバーヘッドが少なく高速です。また、ストリーム内での副作用(外部の変数を書き換えるなど)は避けるべきとされています。
さらに、フィルタリングの順番も重要です。計算コストの高い処理を後半に、データの件数を劇的に減らせる条件を前半に持ってくることで、全体の処理効率を高めることができます。初心者のうちはまずは正しく動くことを優先すべきですが、慣れてきたら処理の効率やストリームの「不変性」についても意識してみると、より上級者への道が開けます。
9. 複雑な条件分岐を美しく書くコツ
フィルタリング条件が非常に複雑になった場合、ラムダ式の中に長いコードを書くと可読性が低下します。そのようなときは、条件判定自体を別のメソッド(述語メソッド)として切り出すか、Predicateインターフェースを利用するのがおすすめです。コードが整理され、テストも書きやすくなります。
例えば、isPremiumUser()というメソッドを作っておけば、filter(User::isPremiumUser)と書くだけで意図が明確に伝わります。プログラミングにおいて「名前を付ける」ことは、他人がコードを読んだときの理解速度を劇的に変える重要なテクニックです。Stream APIを活用しながらも、常に読みやすさを追求する姿勢が、現場で重宝されるエンジニアへの近道となります。