distinct で重複排除

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

distinct で重複排除 とは

Stream の distinct() で配列やリストから重複要素を取り除き、ユニークな要素数を集計する書き方を身につけよう。

重複排除はなぜ難しいのか

プログラムを書いていると「同じ値が複数あったら 1 つにまとめたい」「リストにユニークな要素が何種類あるか知りたい」という場面が頻繁にやってきます。たとえばアンケートの回答から重複する選択肢を消したい、ログから訪れたユーザー ID のユニーク数を出したい、商品タグから種類数を数えたい、といったケースです。

配列や List だけで重複排除を素朴に書こうとすると、二重ループ で 1 個ずつ比較したり、フラグ用の boolean[] を持ち回したりと、意外と面倒なコードになります。for の中で if を書いて、すでに見た値を別の ArrayList に貯めて、まだなければ追加して… という流れは、書いていて飽きるし、計算量も O(n^2) に近づきやすいので大きなデータには向きません。

「重複排除は Set を使えばいいのでは?」というのも正しい発想です。HashSet に全件突っ込めば重複は自然に消えます。実際に Stream の distinct() は内部で HashSet 系の仕組みを使っているので、Set を一から書くのと本質は同じです。違いは「短く、宣言的に書ける」点と、「他の Stream 操作とパイプラインで繋げられる」点にあります。

そこで Java 8 で導入された Stream APIdistinct() 中間操作の出番です。stream.distinct() と書くだけで、Stream の中を流れる要素から重複を取り除いた新しい Stream を返してくれます。filtermap と同じ「中間操作」なので、後ろに count()collect() を繋げてそのまま集計に持ち込めるのが強みです。

diagram (will load when visible)

このレッスンでは、distinct() の仕組みと使い方、equals / hashCode との関係、そして集約系操作との組み合わせを順番に押さえていきます。最終的には「整数配列を受け取って、重複を除いたユニーク要素数を返す」課題を解いてもらいます。

distinct() の基本動作

まずは一番シンプルな例から見ていきましょう。int[] から IntStream を作って distinct() を呼ぶと、重複した数値が落ちて、count() で個数が取れます。

Java

import java.util.Arrays; import java.util.stream.IntStream; public class Demo { public static void main(String[] args) { int[] arr = {1, 2, 2, 3, 3, 3}; long unique = IntStream.of(arr).distinct().count(); System.out.println(unique); // 3 } }

ここでは IntStream.of(arr)int 専用の Stream を作り、distinct() で重複を消し、count() で要素数を long として受け取っています。IntStream を使うと int のままで処理されるので、Integer への オートボクシング が発生せず、メモリにも CPU にも優しいのがポイントです。

String のリストでも書き方は同じです。

Java

import java.util.List; public class StringDemo { public static long uniqueCount(List<String> tags) { return tags.stream().distinct().count(); } }

tags.stream()Stream<String> を取り出し、distinct() を挟み、count() で件数を返しています。Stream<String>count() の戻り値も long なので、int で受けたいときは (int) のキャストか Math.toIntExact を使います。

count()long を返すのは、Stream が想定する最大サイズが int の範囲 (約 21 億) を超えうるからです。日常的な用途ではほぼ int に収まりますが、API の都合上 long で受け取る習慣を付けておくと安全です。

equalshashCode が判定の正体

distinct() は「同じ要素」を判定するときに、対象オブジェクトの equals メソッドと hashCode メソッドを使います。intString のように標準で equals / hashCode が正しく実装されている型を相手にしているときは、私たちは特に何も意識しなくて大丈夫です。問題は、自分で作ったクラスの Streamdistinct() を掛けるときです。

Java

class User { String name; int age; User(String name, int age) { this.name = name; this.age = age; } }

この User クラスをそのまま Stream<User> に流して distinct() してもうまく重複排除されません。User には equals / hashCode が書かれていないので、Object クラスのデフォルト実装、つまり 参照が同じかどうか だけで判定されるからです。つまり、new User("花子", 20) を 2 回作るとそれぞれ別のオブジェクトとみなされ、どちらも残ってしまいます。

diagram (will load when visible)

自作クラスで重複排除したいときは、equalshashCode をペアで実装するのが鉄則です。Java 16 以降なら record を使えば equals / hashCode / toString が自動で実装されるので、これらの判定を Stream 系の API に任せたいデータには record がぴったりです。

Java

record User(String name, int age) {} List<User> users = List.of( new User("花子", 20), new User("花子", 20), new User("太郎", 30) ); long count = users.stream().distinct().count(); // 2

record は「データを持ち運ぶだけのクラス」に最適です。フィールド宣言だけで自動的に値ベースの equals / hashCode が付き、Stream API との相性も抜群です。

集約系操作との組み合わせ

distinct() の真価は、他の Stream 操作と組み合わせたときに発揮されます。よく使うパイプラインの例を見てみましょう。

Java

import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; public class PipelineDemo { public static void main(String[] args) { List<String> tags = Arrays.asList( "java", "python", "java", "go", "python", "rust" ); long uniqueCount = tags.stream().distinct().count(); List<String> uniqueList = tags.stream().distinct().sorted().collect(Collectors.toList()); System.out.println(uniqueCount); // 4 System.out.println(uniqueList); // [go, java, python, rust] } }

1 つ目の式は count() で件数を取り、2 つ目の式は sorted()collect(Collectors.toList()) を繋いで「重複を消してアルファベット順に並べた List」を取り出しています。Stream のパイプラインは左から右に流れるイメージなので、distinct → sorted → collect の順に書くと頭の中で処理が追いやすくなります。

distinct() のあとに map を挟んで集計を変えることもできます。たとえば「ユニークな単語の文字数の合計」が欲しければ次のように書けます。

Java

int totalLength = tags.stream() .distinct() .mapToInt(String::length) .sum();

mapToIntIntStream に切り替え、sum() で合計を出しています。IntStream には sum / max / min / average といった便利な終端操作が揃っているので、数値集計はだいたい IntStream 経由で書くと短くなります。

Stream パイプラインは中間操作 (filter map distinct sorted 等) と終端操作 (count collect sum 等) のペアで完結します。終端操作を呼ばないと中間操作は実行されない、いわゆる遅延評価 (lazy) で動いている点にも注目してください。

よくある間違い

初学者がハマりやすいポイントを 3 つ挙げます。事前に頭に入れておくとデバッグが楽になります。

  • 自作クラスで equals / hashCode を実装し忘れる — 前述のとおり、自作クラスを distinct() に通しても参照比較になり、重複排除されません。record を使うか、IDE の自動生成で equalshashCode を必ずペアで書きましょう
  • Stream を 2 回使い回そうとするStream は一度終端操作を呼ぶと閉じられ、IllegalStateException が出ます。Stream<String> s = list.stream(); を 2 回使い回すのではなく、必要に応じて list.stream() を再度呼び直すのが正解です
  • 順序保証を勘違いするdistinct() は元の Stream の出現順を維持します。{3, 1, 2, 1, 3} を流すと {3, 1, 2} の順で残ります。並べ替えたいときは別途 sorted() を繋いでください。逆に parallelStream() では順序保証が崩れる場合があるので、順序が重要なら stream() を使うか forEachOrdered を選びます

もう 1 つよく混乱するのが、distinct() を呼んだあとの Stream の型です。Stream<T>distinct() を呼ぶと戻り値はやはり Stream<T> で、Set<T> にはなりません。Set が欲しければ最後に collect(Collectors.toSet()) を呼ぶ必要があります。

Collectors.toSet() の戻り値は HashSet 相当で順序保証がありません。順序を保ったまま Set にしたいときは collect(Collectors.toCollection(LinkedHashSet::new)) を使います。

やってみよう

それでは本題の課題に挑戦してみましょう。やることは次のとおりです。

  1. Solution.uniqueCount(int[] arr) メソッドを実装する
  2. IntStream.of(arr)IntStream を作る
  3. distinct() を繋いで重複を消す
  4. count() で件数を取り、int にキャストして返す

空配列が来たときは count()0L を返してくれるので、特別な分岐は要りません。素直に IntStream.of(arr).distinct().count()(int) でキャストして return するだけで OK です。

テストでは {1, 2, 2, 3, 3, 3}3{}0{5, 5, 5}1{1, 2, 3, 4, 5}5 のような入力が流れてきます。distinct() は順序を保つので、要素の並びが変わっても結果は変わりません。

慣れてきたら、int[] ではなく List<Integer> 版や List<String> 版も書いてみてください。stream() から始めて distinct().count() で締める基本パターンは共通で、対象が変わるだけです。Stream の世界では「データの流れ + 中間操作 + 終端操作」の組み合わせを覚えるのが何より大事なので、ここでパイプラインの感覚をしっかり掴んでおきましょう。

よくある質問

Q. set とリストはどう違いますか?

A. set は重複を持たない集合で、要素の追加と存在確認が O(1) と高速です。リストは順序付きで重複を許し、要素の前後関係が意味を持つ場面で使います。重複除去や「含まれているか」判定の高速化が必要なら set が圧倒的に有利です。

Q. set の順序は保証されますか?

A. set は順序が保証されません。順序を保ったまま重複を除去したい場合は dict.fromkeys(items) を使う(Python)か、Set + 配列の組み合わせで管理してください。順序付きで集合演算したい場面は意外と多いので覚えておくと便利です。

Q. set 同士の積集合・和集合はどう書きますか?

A. a & b で積集合、a | b で和集合、a - b で差集合になります。リスト同士の共通要素を求めたいときは set(a) & set(b) で O(n) に高速化できます。文字列もイテラブルなので set('abc') & set('bcd') → {'b', 'c'} のように使えます。

次のレッスン

次は limit と skip で、Stream の distinct() で配列やリストから重複要素を取り除き、ユニークな要素数を集計する書き方を身につけよう を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. distinct の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. distinct とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. ファイル先頭で import java.util.stream.IntStream; を書くこと
  2. IntStream.of(arr) から Stream を作り、distinct() を必ず使うこと
  3. count() の戻り値 (long) を int にキャストして return すること

入出力例

test-cases.txt

uniqueCount([1,2,2,3,3,3])3 uniqueCount([])0 uniqueCount([5,5,5])1 uniqueCount([1,2,3,4,5])5 uniqueCount([-1,0,0,-1,2])3

ヒント

main.java
main.java
学習モード

メモ

distinct で重複排除

⌘S で保存