1. メソッド distinct: 重複を取り除く
実務では、データをフィルタリング・変換するだけでなく、コレクションからユニークな要素だけを選ぶ、結果のサイズを制限する、あるいは先頭のいくつかの要素をスキップする、といった要件がよくあります。例えば:
- ユーザー名のユニークな一覧を取得する。
- ページ表示のために先頭 10 件だけを取得する。
- 先頭 5 要素をスキップする(例: ページングで「2 ページ目から表示」)。
このような場面では、Stream API には専用のメソッド distinct、limit、skip があります。
distinct はどのように動く?
メソッド distinct() は、重複要素をすべて取り除いた新しいストリームを返します。重複かどうかは、対象クラスの equals と hashCode によって判定されます。
あなたがコイン収集家だとして、メインのコレクションに重複がないことが大事だとします。重複したコインは交換用の箱へ回すイメージです。まさに distinct() は、メインのコレクションから重複をふるい落とす役割を果たします。
例: ユーザー名のユニーク化
List<String> names = List.of(
"アリサ", "ボブ", "アリサ", "エヴァ", "ボブ", "デニス", "グレブ", "エヴァ"
);
ユニークな名前のリストを取得します:
List<String> uniqueNames = names.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(uniqueNames);
// 出力: [アリサ, ボブ, エヴァ, デニス, グレブ]
例: ユーザーのユニークな email
たとえば、次のような User クラスがあるとします:
public class User {
String name;
String email;
// コンストラクタ、ゲッター、toString() — 便宜上
public User(String name, String email) {
this.name = name;
this.email = email;
}
@Override
public String toString() {
return name + " <" + email + ">";
}
}
email が重複しているユーザーの一覧:
List<User> users = List.of(
new User("アリサ", "alice@mail.com"),
new User("ボブ", "bob@mail.com"),
new User("エヴァ", "eva@mail.com"),
new User("アリサ2", "alice@mail.com"), // email の重複!
new User("グレブ", "gleb@mail.com"),
new User("エヴァ2", "eva@mail.com") // email の重複!
);
単に users.stream().distinct() としても、重複は取り除かれません。というのも、User オブジェクトではデフォルトでは equals と hashCode がオーバーライドされていないからです。この場合、distinct は同一参照でのみ重複とみなします。
解決策: equals と hashCode をオーバーライドし、email をもとに一意性を判定するようにします。
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(email, user.email);
}
@Override
public int hashCode() {
return Objects.hash(email);
}
これで:
List<User> uniqueUsers = users.stream()
.distinct()
.collect(Collectors.toList());
uniqueUsers.forEach(System.out::println);
// アリサ <alice@mail.com>
// ボブ <bob@mail.com>
// エヴァ <eva@mail.com>
// グレブ <gleb@mail.com>
重要: 自作クラスで distinct を「ちゃんと」動作させたいなら、equals と hashCode を必ずオーバーライドしましょう!
2. メソッド limit: 要素数を制限する
メソッド limit(long maxSize) は、元のストリームの先頭から最大 maxSize 個の要素だけを含む新しいストリームを返します。
たとえ話: ケーキ屋に行って、100 個あるうち 3 個だけ味見したいとします。limit(3) — すると最初の 3 つだけが出てきて、残りは「今日はここまで」になります。
例: 先頭 3 つの名前
List<String> firstThree = names.stream()
.limit(3)
.collect(Collectors.toList());
System.out.println(firstThree);
// 出力: [アリサ, ボブ, アリサ]
アプリの例: 新規ユーザーのトップ3
List<User> firstUsers = users.stream()
.limit(3)
.collect(Collectors.toList());
firstUsers.forEach(System.out::println);
// アリサ <alice@mail.com>
// ボブ <bob@mail.com>
// エヴァ <eva@mail.com>
ソートと併用する例
ソートと組み合わせることもできます。例えば、もっとも短い email を持つユーザーのトップ2を取得する:
List<User> top2ShortEmail = users.stream()
.sorted(Comparator.comparingInt(u -> u.email.length()))
.limit(2)
.collect(Collectors.toList());
top2ShortEmail.forEach(System.out::println);
// ボブ <bob@mail.com>
// エヴァ <eva@mail.com>
3. メソッド skip: 先頭の要素をスキップする
メソッド skip(long n) は、元のストリームの先頭から n 個の要素をスキップした新しいストリームを返します。
例: 先頭 2 つの名前をスキップ
List<String> afterTwo = names.stream()
.skip(2)
.collect(Collectors.toList());
System.out.println(afterTwo);
// 出力: [アリサ, エヴァ, ボブ, デニス, グレブ, エヴァ]
例: ページング(pagination)
Web サイトで「1 ページあたり 3 ユーザーを表示」といった要件はよくあります。2 ページ目を表示するには、先頭の 3 件をスキップして次の 3 件を取得します:
int pageSize = 3;
int pageNumber = 2; // 2 ページ目
List<User> page = users.stream()
.skip(pageSize * (pageNumber - 1))
.limit(pageSize)
.collect(Collectors.toList());
page.forEach(System.out::println);
// アリサ2 <alice@mail.com>
// グレブ <gleb@mail.com>
// エヴァ2 <eva@mail.com>
4. distinct、limit、skip の組み合わせ
これらのメソッドは、目的に応じて組み合わせて使います。
例: 3 番目から始めてユニークな名前を 2 件取得する
List<String> result = names.stream()
.distinct() // 重複を除去: [アリサ, ボブ, エヴァ, デニス, グレブ]
.skip(2) // アリサとボブをスキップ: [エヴァ, デニス, グレブ]
.limit(2) // 2 件だけ取得: [エヴァ, デニス]
.collect(Collectors.toList());
System.out.println(result);
// 出力: [エヴァ, デニス]
フィルタと組み合わせる例
例えば、文字「a」を含むユニークな email を先頭から 2 件取得する:
List<String> emails = users.stream()
.map(user -> user.email)
.filter(email -> email.contains("a"))
.distinct()
.limit(2)
.collect(Collectors.toList());
System.out.println(emails);
// 出力: [alice@mail.com, eva@mail.com]
5. 実践: 応用課題
課題 1. 文字数が 3 より大きいユーザー名のユニークなリストを取得する
List<String> longUniqueNames = names.stream()
.filter(name -> name.length() > 3)
.distinct()
.collect(Collectors.toList());
System.out.println(longUniqueNames);
// 例: [アリサ, デニス]
課題 2. ユーザー一覧から 3 番目と 4 番目のユニークな email を取得する
List<String> thirdAndFourthEmail = users.stream()
.map(user -> user.email)
.distinct()
.skip(2)
.limit(2)
.collect(Collectors.toList());
System.out.println(thirdAndFourthEmail);
// 例: [eva@mail.com, gleb@mail.com]
課題 3. 10 より大きいユニークな数値を先頭から 5 件取得する
List<Integer> numbers = List.of(5, 12, 17, 5, 23, 17, 42, 19, 12, 8);
List<Integer> result = numbers.stream()
.filter(n -> n > 10)
.distinct()
.limit(5)
.collect(Collectors.toList());
System.out.println(result);
// 出力: [12, 17, 23, 42, 19]
6. ビジュアル図: 操作の適用順序
graph TD
A[元のリスト] --> B[filter]
B --> C[distinct]
C --> D[skip]
D --> E[limit]
E --> F[collect]
コメント:
通常はまず filter を適用し、次に distinct で重複を取り除き、その後に skip と limit を使い、最後に collect で集約します。ただし、要件によっては順序を入れ替えることもあります。
7. distinct、limit、skip のよくあるミス
誤り 1: distinct が「どんな」基準でも重複を消してくれると期待する。
実際には、distinct はオブジェクトの equals に基づいて動作します。あるフィールド(例えば email のみ)で一意性を判断したい場合は、equals/hashCode をオーバーライドするか、Collectors.toMap() や追加のフィルタリングといった工夫が必要です。
誤り 2: 操作の順序を誤る。
先に limit を適用してから distinct を行うと、重複が残る可能性があります。なぜなら、先頭の N 件に同じ要素が含まれているかもしれないからです。
誤り 3: skip で要素数より大きい数をスキップする。
ストリームに含まれる要素数より多くをスキップしようとした場合、結果は単に空リストになります。エラーにはなりませんが、意図しない結果になり得ます。
誤り 4: パフォーマンスを見落とす。
distinct、limit、skip は、非常に大きいストリームに対しては非効率になることがあります。特に非順序ストリームや要素が複雑な場合です。日常的な 99% のケースでは問題になりませんが、数百万件を扱うなら検討が必要です。
誤り 5: 自作クラスでの equals/hashCode のオーバーライドを忘れる。
ユーザー定義のオブジェクト(例えば User)を扱う場合、これらのメソッドをオーバーライドしていないと、distinct は論理的には同一でも別物として扱ってしまいます。
GO TO FULL VERSION