CodeGym /コース /JAVA 25 SELF /Stream API のサブセット: distinct, limit, skip

Stream API のサブセット: distinct, limit, skip

JAVA 25 SELF
レベル 30 , レッスン 2
使用可能

1. メソッド distinct: 重複を取り除く

実務では、データをフィルタリング・変換するだけでなく、コレクションからユニークな要素だけを選ぶ、結果のサイズを制限する、あるいは先頭のいくつかの要素をスキップする、といった要件がよくあります。例えば:

  • ユーザー名のユニークな一覧を取得する。
  • ページ表示のために先頭 10 件だけを取得する。
  • 先頭 5 要素をスキップする(例: ページングで「2 ページ目から表示」)。

このような場面では、Stream API には専用のメソッド distinctlimitskip があります。

distinct はどのように動く?

メソッド distinct() は、重複要素をすべて取り除いた新しいストリームを返します。重複かどうかは、対象クラスの equalshashCode によって判定されます。

あなたがコイン収集家だとして、メインのコレクションに重複がないことが大事だとします。重複したコインは交換用の箱へ回すイメージです。まさに 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 オブジェクトではデフォルトでは equalshashCode がオーバーライドされていないからです。この場合、distinct は同一参照でのみ重複とみなします。

解決策: equalshashCode をオーバーライドし、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 を「ちゃんと」動作させたいなら、equalshashCode を必ずオーバーライドしましょう!

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. distinctlimitskip の組み合わせ

これらのメソッドは、目的に応じて組み合わせて使います。

例: 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 で重複を取り除き、その後に skiplimit を使い、最後に collect で集約します。ただし、要件によっては順序を入れ替えることもあります。

7. distinctlimitskip のよくあるミス

誤り 1: distinct が「どんな」基準でも重複を消してくれると期待する。
実際には、distinct はオブジェクトの equals に基づいて動作します。あるフィールド(例えば email のみ)で一意性を判断したい場合は、equals/hashCode をオーバーライドするか、Collectors.toMap() や追加のフィルタリングといった工夫が必要です。

誤り 2: 操作の順序を誤る。
先に limit を適用してから distinct を行うと、重複が残る可能性があります。なぜなら、先頭の N 件に同じ要素が含まれているかもしれないからです。

誤り 3: skip で要素数より大きい数をスキップする。
ストリームに含まれる要素数より多くをスキップしようとした場合、結果は単に空リストになります。エラーにはなりませんが、意図しない結果になり得ます。

誤り 4: パフォーマンスを見落とす。
distinctlimitskip は、非常に大きいストリームに対しては非効率になることがあります。特に非順序ストリームや要素が複雑な場合です。日常的な 99% のケースでは問題になりませんが、数百万件を扱うなら検討が必要です。

誤り 5: 自作クラスでの equals/hashCode のオーバーライドを忘れる。
ユーザー定義のオブジェクト(例えば User)を扱う場合、これらのメソッドをオーバーライドしていないと、distinct は論理的には同一でも別物として扱ってしまいます。

1
タスク
JAVA 25 SELF, レベル 30, レッスン 2
ロック未解除
エクスクルーシブパーティーの入場列管理 🎉
エクスクルーシブパーティーの入場列管理 🎉
1
タスク
JAVA 25 SELF, レベル 30, レッスン 2
ロック未解除
売上のトップ4ユニーク指標の作成 💰
売上のトップ4ユニーク指標の作成 💰
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION