distinct を一言でいうと
Stream#distinct() は、
「ストリームの中から“重複している要素”を取り除き、“一意な要素だけ”を残す中間操作」 です。
イメージとしては、
「同じ値が何回も流れてきたら、最初の 1 回だけ通して、あとは全部スルーするフィルター」
だと思ってください。
List に対して new HashSet<>(list) をするのと似ていますが、
Stream のパイプラインの中で、他の操作と自然につなげて使えるのが distinct() です。
distinct の基本的な動き
単純な値(String / Integer)での例
まずは一番シンプルな例から。
import java.util.List;
public class DistinctBasic {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Alice", "Charlie", "Bob");
List<String> unique =
names.stream()
.distinct()
.toList();
System.out.println(unique); // [Alice, Bob, Charlie]
}
}
Javaここで起きていることは、とても単純です。
ストリームに流れてくる要素を、
「これまでに見たことがあるか?」で判定し、
初めて出てきた値だけを次に流します。
2 回目以降に出てきた同じ値は、そこで落とされます。
distinct が「何をもとに重複判定するか」
equals と hashCode がすべての鍵
distinct() が「同じ」とみなすかどうかは、
その要素の equals と hashCode の実装に完全に依存します。
String や Integer のような標準クラスは、
すでに「値が同じなら equals も true、hashCode も同じ」という実装になっているので、
そのまま distinct() を使えば期待通りに動きます。
一方、自分で定義したクラスの場合、equals / hashCode をちゃんとオーバーライドしていないと、
「同じ内容なのに別物扱いされる」ことになります。
自作クラスで distinct を使うときの注意点
equals / hashCode を実装していない場合
次のコードを見てください。
import java.util.List;
class User {
private final String name;
User(String name) {
this.name = name;
}
public String getName() { return name; }
}
public class DistinctUserExample {
public static void main(String[] args) {
List<User> users = List.of(
new User("Alice"),
new User("Bob"),
new User("Alice")
);
List<User> unique =
users.stream()
.distinct()
.toList();
System.out.println(unique.size()); // 3 になる
}
}
Java"Alice" という名前の User を 2 回作っていますが、distinct() を通しても 3 件のままです。
理由は、User クラスが equals / hashCode をオーバーライドしていないため、
「インスタンスの中身」ではなく「インスタンスの参照(アドレス)」で比較されているからです。
つまり、同じ名前でも「別のインスタンス」なので、distinct() から見ると「全部違うもの」に見えてしまいます。
equals / hashCode を実装した場合
User に「名前が同じなら同じユーザーとみなす」という意味の equals / hashCode を実装してみます。
import java.util.Objects;
class User {
private final String name;
User(String name) {
this.name = name;
}
public String getName() { return name; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User other = (User) o;
return Objects.equals(this.name, other.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
Javaこの状態で先ほどの distinct() を実行すると、
List<User> unique =
users.stream()
.distinct()
.toList();
System.out.println(unique.size()); // 2 になる
Java今度は "Alice" が 1 件にまとまり、"Alice", "Bob" の 2 件だけが残ります。
ここから分かる一番大事なポイントは、
「distinct の“重複判定”は equals / hashCode に完全に依存する」
ということです。
「特定のキーで重複排除したい」場合の設計
distinct は「要素全体」でしか判定できない
distinct() は、あくまで「要素そのもの」で重複判定します。
「User の name だけを見て重複排除したい」
「id だけをキーにしたい」
といった、「特定のプロパティだけで重複を判定したい」場合、distinct() 単体では対応できません。
map でキーに変換してから distinct する
一番シンプルな方法は、
「先に map でキーだけに変換してから distinct() する」やり方です。
List<String> uniqueNames =
users.stream()
.map(User::getName) // User -> String(名前)
.distinct() // 名前で重複排除
.toList();
Javaこれで「名前の重複排除」はできます。
ただし、この方法だと「User 自体」を重複排除することはできません。
Map や Set を使って「キーで一意な User」を作る
「name をキーにして、一意な User を取りたい」場合は、Collectors.toMap や Collectors.toCollection を使うパターンがよく使われます。
例えば、最後に出てきた User を残すなら:
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
Map<String, User> byName =
users.stream()
.collect(Collectors.toMap(
User::getName, // キー:name
Function.identity(), // 値:User 自身
(oldV, newV) -> newV // 同じキーが来たら新しい方を採用
));
List<User> uniqueUsers = List.copyOf(byName.values());
Javaここまで来ると少し応用寄りですが、
「distinct は“要素全体”でしか判定できない」
「特定のキーで一意にしたいなら、別の設計が必要」
という感覚だけ持っておけば十分です。
distinct の位置づけとパイプライン設計
filter / map / distinct の役割分担
Stream の中での distinct() の役割は、
「すでに決まっている“等価性(equals)”に従って、重複を取り除く」
ことです。
なので、パイプラインの中では、
条件で絞り込むのは filter
形を変えるのは map
重複を消すのは distinct
というふうに、責務を分けて考えると設計がきれいになります。
例えば、「20 歳以上のユーザーの“重複しない名前リスト”」なら:
List<String> names =
users.stream()
.filter(u -> u.getAge() >= 20) // 絞り込み
.map(User::getName) // 変換
.distinct() // 重複排除
.toList();
Javaこの順番にすることで、
「何をしているか」が左から右に自然に読めます。
distinct のコストと意識しておきたいこと
内部では「これまでに見た要素」を覚えている
distinct() は、内部的には「これまでに見た要素」を Set のような構造に溜め込みながら処理します。
つまり、要素数が増えるほど、
「覚えておくためのメモリ」も増えていきます。
通常のサイズのリストなら気にしなくて構いませんが、
「とんでもなく大きなストリーム」や「無限ストリーム」に対して distinct() を使うと、
メモリを食い続けることになる、ということは頭の片隅に置いておくとよいです。
まとめ:distinct を自分の言葉で整理する
distinct() をあなたの言葉でまとめるなら、
「ストリームの中から、equals / hashCode に基づいて重複を取り除き、一意な要素だけを残す中間操作」
です。
特に意識しておきたいのは、
要素が標準クラス(String / Integer など)なら、そのまま期待通りに動くこと
自作クラスで使うときは、equals / hashCode を正しく実装しておく必要があること
「特定のプロパティだけで重複排除したい」場合は、map や Collectors を組み合わせる設計になること
filter / map / distinct の責務を分けて、パイプラインの意味が読みやすい順番で並べること
あたりです。
