はじめに:「distinctって重複を消すだけじゃないの?」
distinct()を使えば重複が消える——それは知っている。でも試験本番で「あれ、これどっちが正しい?」と手が止まった経験はないでしょうか。
実はdistinct()は、何をもって”同じ”と判断するかという内部動作を理解していないと、ひっかけ問題でそのまま失点します。本記事では、distinct()の内部動作・equals()との関係・順序付きストリームと順序なしストリームでの挙動の違いまで、試験に出るポイントに絞って解説します。
distinct()の基本:何をしているのか
Stream.distinct()は、ストリームの要素から重複を取り除いた新しいストリームを返す中間操作です。
import java.util.List;
import java.util.stream.Collectors;
List<Integer> list = List.of(1, 2, 2, 3, 3, 3, 4);
List<Integer> result = list.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(result);
// [1, 2, 3, 4]ここまでは直感通りです。問題は「どうやって重複を判定しているか」です。
重複判定の仕組み:equals()とhashCode()が使われる
distinct()は内部的に**Object.equals()メソッドを使って重複を判定します。さらに、パフォーマンスのためにhashCode()も合わせて使用**します。つまり、HashSetに要素を追加するときと同じ仕組みです。
これが何を意味するかというと、equals()とhashCode()を正しくオーバーライドしていないクラスは、意図通りに重複除去されないということです。
import java.util.List;
import java.util.stream.Collectors;
class Point {
int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
// equals()もhashCode()もオーバーライドしていない
}
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
List<Point> result = List.of(p1, p2).stream()
.distinct()
.collect(Collectors.toList());
System.out.println(result.size());
// 2 ← 重複として扱われない!p1とp2は同じ座標を持っていますが、equals()をオーバーライドしていないため別オブジェクトとして判定され、両方残ります。
equals()とhashCode()を正しくオーバーライドすれば、期待通りの結果になります。
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
class Point {
int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point)) return false;
Point p = (Point) o;
return x == p.x && y == p.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
List<Point> result = List.of(p1, p2).stream()
.distinct()
.collect(Collectors.toList());
System.out.println(result.size());
// 1 ← 正しく重複除去される順序付きストリームと順序なしストリームでの違い
distinct()の挙動は、ストリームが順序付き(ordered)か順序なし(unordered)かによって変わります。
順序付きストリーム(List由来など)
Listから生成したストリームは順序付きです。この場合、distinct()は出現順序を保持します。つまり、最初に登場した要素が残ります。
import java.util.List;
import java.util.stream.Collectors;
List<String> list = List.of("b", "a", "b", "c", "a");
List<String> result = list.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(result);
// [b, a, c] ← 最初に出現した順序が保たれる順序なしストリーム(Set由来など)
Setから生成したストリームは順序なしです。この場合、どの要素が残るかは不定です。
import java.util.Set;
import java.util.stream.Collectors;
Set<String> set = Set.of("b", "a", "c");
// 順序は保証されない| ストリームの元 | 順序付き | distinct()後の順序 |
|---|---|---|
List | ○ | 出現順を保持 |
Arrays.asList() | ○ | 出現順を保持 |
Set | ✕ | 不定 |
Stream.of() | ○ | 出現順を保持 |
| 並列ストリーム(ordered) | △ | 保持されるが処理コストが高い |
並列ストリームでのdistinct()
並列ストリームでdistinct()を使うと、正確な結果は得られますが、パフォーマンスコストが高くなります。
順序付き並列ストリームでは、重複除去後も順序を保つために各スレッドの結果をマージする処理が必要になるためです。
import java.util.List;
import java.util.stream.Collectors;
List<Integer> list = List.of(1, 2, 2, 3, 3, 3);
List<Integer> result = list.parallelStream()
.distinct()
.collect(Collectors.toList());
System.out.println(result);
// [1, 2, 3] ← 結果は正しいが、内部処理はコストがかかるunordered()を組み合わせると、順序の保証を放棄する代わりに並列処理の効率が上がります。
List<Integer> result = list.parallelStream()
.unordered()
.distinct()
.collect(Collectors.toList());
// 結果に含まれる要素は同じだが、順序は不定試験で狙われるひっかけポイント
ひっかけ①:equals()だけオーバーライドしてhashCode()を忘れる
distinct()はHashSetと同じ仕組みで判定するため、equals()のみオーバーライドしてhashCode()を忘れると、重複除去が正しく機能しません。試験コードでは意図的に片方だけオーバーライドしたクラスが登場することがあります。
📌
equals()とhashCode()は必ずセットでオーバーライドする。これはJava全般の基本ルールであり、distinct()でも同様に適用される。
ひっかけ②:順序付きストリームで「どの要素が残るか」を問われる
List.of("a", "b", "a")に対してdistinct()を呼んだとき、残るのは**最初に出現した”a”**です。「後から出現した要素が残る」という選択肢は誤りです。
ひっかけ③:distinct()は中間操作である
distinct()は中間操作であり、戻り値はStream<T>です。終端操作を呼ぶまで実際の処理は行われません(遅延評価)。
Stream<Integer> s = List.of(1, 2, 2, 3).stream().distinct();
// この時点ではまだ何も処理されていない
List<Integer> result = s.collect(Collectors.toList());
// collect()が呼ばれて初めて処理される
// [1, 2, 3]ひっかけ④:null要素の扱い
distinct()はnullも要素として扱います。複数のnullがある場合、1つに集約されます。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
List<String> list = Arrays.asList("a", null, "b", null);
List<String> result = list.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(result);
// [a, null, b] ← nullは1つに集約されるただし、List.of()はnullを許容しないため、Arrays.asList()を使う点に注意してください。
まとめ:distinct()で押さえるべき4点
| ポイント | 内容 |
|---|---|
| 重複判定の基準 | equals()とhashCode()の両方を使用 |
| 順序付きストリーム | 出現順を保持して重複除去 |
| 順序なしストリーム | 残る要素の順序は不定 |
| 操作の種類 | 中間操作(遅延評価) |
distinct()はシンプルに見えて、試験では**equals()/hashCode()の実装有無とストリームの順序付き・順序なしの違い**が重点的に問われます。コードを読む際は「このクラスはequals()をオーバーライドしているか」「このストリームは順序付きか」を必ず確認する習慣をつけておきましょう。