JavaのStreamメソッドdistinctをやさしく解説!初心者向け実例で重複削除を理解
生徒
「Javaで同じ値が何度も入っているリストから重複を消したいです。配列やリストに対して簡単に使える方法はありますか?」
先生
「java.util.streamのStreamにあるdistinctを使うと、重複を取り除いた流れを簡単に作れます。順番も保ちやすく、読みやすい書き方になりますよ。」
生徒
「どんな型でも使えますか?文字列や数値だけでなく、クラスのオブジェクトでも使えるのか気になります。」
先生
「使えます。ただしオブジェクトでは同一性の判定方法としてequalsとhashCodeの実装が関係します。まず基本から順に見ていきましょう。」
1. distinctとは何か
「1. distinctとは何か」の重要ポイントを、初心者の方にも分かりやすく簡潔に解説します。
distinctは、流れの中に同じ値が複数存在するときに、最初に現れた一つだけを残し、それ以外の重複を取り除いてくれる操作です。元の並び順を壊さずに重複削除ができるため、後続の処理がとても読みやすくなります。従来の手続き型の書き方では、補助の集合や条件分岐を用意して複雑になりがちでしたが、distinctを使うと意図がひと目で伝わる表現にまとまります。文字列や数値などの基本的な型はもちろん、独自のクラスでも活用でき、収集や表示、集計の前処理として幅広く使われます。
2. 文字列の重複を消す基本例
まずは文字列の重複削除です。同じ単語が何度も入っているリストから重複を取り除き、順番を保ったまま一覧にできます。短いコードで誤りにくい表現になるため、最初の練習におすすめです。
import java.util.List;
import java.util.stream.Collectors;
public class DistinctStringBasic {
public static void main(String[] args) {
List<String> words = List.of("りんご", "みかん", "りんご", "ぶどう", "みかん");
List<String> unique = words.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(unique);
}
}
[りんご, みかん, ぶどう]
並び順を保ったまま、最初に現れた要素だけが残ります。そのままsortedやlimitなどへつなげても読みやすさが崩れません。
3. 大文字小文字を無視して重複を消す
実務では大文字小文字の揺れをまとめたい場面があります。distinctは値の同一性で判定するため、そのままだと大小の違いは別扱いになります。そこで流れの前段で小文字へ正規化し、重複削除を行います。意図が明確で、あとから読み返しても迷いにくい形です。
import java.util.List;
import java.util.stream.Collectors;
public class DistinctIgnoreCase {
public static void main(String[] args) {
List<String> words = List.of("Java", "java", "JAVA", "Stream");
List<String> unique = words.stream()
.map(s -> s.toLowerCase())
.distinct()
.collect(Collectors.toList());
System.out.println(unique);
}
}
[java, stream]
このように前段の加工と組み合わせることで、要件に合わせた重複判定が実現できます。先に掃除をしてからdistinctという順序はよく使われます。
4. オブジェクトの重複削除とequalsやhashCodeの関係
「4. オブジェクトの重複削除とequalsやhashCodeの関係」の重要ポイントを、初心者の方にも分かりやすく簡潔に解説します。
独自クラスの重複削除では、要素が同じかどうかの判断基準をequalsとhashCodeで定義しておくのが基本です。同じ識別子を持つなら同一とみなす、といった設計を明確にしておくと、distinctが期待どおりに働きます。これらが未定義のままだと、インスタンスが別物として扱われ、重複が残ってしまうことがあります。識別子と同値の概念をそろえておくと、読み手が迷わないコードになります。
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
class Product {
final String code;
final String name;
Product(String code, String name){ this.code = code; this.name = name; }
@Override public boolean equals(Object o){
if(this == o) return true;
if(!(o instanceof Product)) return false;
Product p = (Product)o;
return Objects.equals(code, p.code);
}
@Override public int hashCode(){ return Objects.hash(code); }
@Override public String toString(){ return code + ":" + name; }
}
public class DistinctObjectById {
public static void main(String[] args) {
List<Product> list = List.of(
new Product("A001", "りんご"),
new Product("A001", "りんご特選"),
new Product("B010", "みかん")
);
List<Product> unique = list.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(unique);
}
}
[A001:りんご, B010:みかん]
識別子を基準に同一性を定めておくと、余計な条件分岐を避けられます。設計の意図がコードに現れるため、保守での混乱も防げます。
5. 特定のキーで重複を消す設計
複数のフィールドを持つオブジェクトで、equalsを変更したくない場合もあります。そのときは一時的にキーへ写像してdistinctを適用し、最後に元へ戻す構成が有効です。たとえばコードだけで重複を消し、最初に現れた明細を残したいといった要件で使えます。
import java.util.LinkedHashMap;
import java.util.List;
import java.util.stream.Collectors;
public class DistinctByKey {
public static void main(String[] args) {
record Item(String code, String label, int price){}
List<Item> items = List.of(
new Item("A", "一番", 100),
new Item("A", "二番", 120),
new Item("B", "三番", 200)
);
List<Item> unique = items.stream()
.collect(Collectors.toMap(
Item::code,
i -> i,
(first, dup) -> first,
LinkedHashMap::new))
.values().stream().collect(Collectors.toList());
System.out.println(unique);
}
}
[Item[code=A, label=一番, price=100], Item[code=B, label=三番, price=200]]
写像と収集を組み合わせると、元のequalsに手を付けず要件どおりの重複排除ができます。順序を守れる点も実務で扱いやすい利点です。
6. mapやfilterとの組み合わせ
distinctは前後の操作と相性が良く、正規化や条件の絞り込みと組み合わせると意図が明確になります。たとえば空の値を除外してから重複を消し、その後で並べ替えや集計につなげると、処理の段取りが読みやすい形に整理されます。段階を小さく区切るほど、変更にも強くなります。
import java.util.List;
import java.util.stream.Collectors;
public class DistinctWithFilter {
public static void main(String[] args) {
List<String> raw = List.of(" りんご", "", "みかん", "りんご ", " ");
List<String> cleaned = raw.stream()
.map(s -> s.trim())
.filter(s -> !s.isEmpty())
.distinct()
.collect(Collectors.toList());
System.out.println(cleaned);
}
}
[りんご, みかん]
前段で整えてからdistinctという順序が、最も素直で理解しやすい流れになります。保守や拡張の際にも迷いにくい構成です。
7. flatMapとdistinctで全体から一意な値を集める
「7. flatMapとdistinctで全体から一意な値を集める」の重要ポイントを、初心者の方にも分かりやすく簡潔に解説します。
入れ子の構造ではflatMapで平らにしてからdistinctを使います。全体の一覧から重複のない集合を得る手順が自然に表現でき、見通しの良いコードになります。分割や整形を先に済ませておくと、重複判定の基準がそろうため、漏れや二度手間を防げます。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class DistinctWithFlatMap {
public static void main(String[] args) {
List<String> lines = List.of("red, green", "blue, red", "yellow");
List<String> colors = lines.stream()
.flatMap(s -> Arrays.stream(s.split(",")))
.map(String::trim)
.distinct()
.collect(Collectors.toList());
System.out.println(colors);
}
}
[red, green, blue, yellow]
展開、整形、重複削除という段取りを順に並べるだけで、やりたい処理の全体像が伝わります。流れの書き方がそのまま設計の説明になっています。
8. 並列処理と性能の考え方
distinctは内部で既出の要素を記録して判定します。大量のデータでは記録のコストが関係するため、前段で不要な要素を早めに捨てると効率が上がります。並列処理では順序維持のコストも影響するので、順序が不要なら無秩序の収集を検討するなど、要件に合わせて調整すると良い結果が得られます。可視化のためにpeekを挟んで流れを確かめ、ボトルネックの位置を把握してから最適化へ進むやり方が安全です。
9. つまずきやすい失敗例と回避策
オブジェクトの重複が消えないのは、equalsやhashCodeが未定義か、基準が一致していない場合が多いです。識別子を決め、それに基づいて同一性を定義しておくと安定します。大小の揺れや空白の混入、記号の差異などは、前段の正規化や整形を徹底してからdistinctを適用するだけで解決できます。また、先にソートを行うと残す要素の位置が変わるため、保持したい方を先に確定してから操作の順序を決めると、意図しない結果を避けられます。
10. 学習と実務での使い分けのコツ
「10. 学習と実務での使い分けのコツ」の重要ポイントを、初心者の方にも分かりやすく簡潔に解説します。
学習ではまず基本の重複削除、次に正規化を伴う例、最後にオブジェクトとキーの写像という順で練習すると、distinctの考え方が身につきます。実務では、操作の順序を意識しながら前処理と重複削除を分けて書くと、仕様変更に強くなります。設計方針としては、判定基準を一か所へ集約する、責任を関数へ逃がさない、表示の整形は最後に回す、といった約束事を決めておくと、チームでの開発でも迷いを減らせます。distinctは短いメソッドですが、流れの設計を素直に表現できる頼もしい道具です。