distinct で重複排除
同じ人を 3 回数えてしまう
アクセスログのユーザー ID を並べたリストから「何人が来たか」を出したいとします。行数をそのまま数えると、答えがずれます。
Java
List<String> ids = List.of("u1", "u2", "u1", "u3", "u1");
System.out.println(ids.size()); // 5来たのは 3 人ですが、u1 が 3 回訪れているので 5 と出ます。自力で数えようとすると、すでに見た ID を覚えておく Set を用意して、for の中で「入っていなければ足す」を書くことになります。ここまで来ると、やりたいことより準備の方が長くなります。
distinct() は、この「すでに見たかどうか」を代わりに覚えてくれる中間操作です。
Java
List<String> unique = ids.stream().distinct().toList();
// [u1, u2, u3]最初に出てきた 1 つを残し、後から来た同じものを捨てます。残った要素の順番は、元の並びのままです。
同じかどうかは equals と hashCode で決まる
String や Integer なら、distinct() は素直に効きます。困るのは自作クラスです。
Java
List<User> users = List.of(new User("u1"), new User("u1"));
System.out.println(users.stream().distinct().toList()); // 2 件とも残るid はどちらも u1 なのに、2 件とも残ります。distinct() は中身を見比べているのではなく、要素に equals と hashCode を尋ねているからです。何も書いていないクラスは、この 2 つを Object から受け継いだままで、その中身は「同じ場所にあるオブジェクトだけを同じとみなす」というものです。new を 2 回すれば別の場所にできるので、別物と判定されます。
自作クラスで中身の一致を見てほしいなら、両方を自分で書きます。
Java
class User {
String id;
User(String id) { this.id = id; }
@Override
public boolean equals(Object o) {
if (!(o instanceof User)) return false;
return id.equals(((User) o).id);
}
@Override
public int hashCode() {
return id.hashCode();
}
}hashCode を書き忘れると、equals だけでは重複が残ります。distinct() は速く探すために先に hashCode で振り分けてから equals を呼ぶので、hashCode がばらばらだと、equals を呼ぶところまで辿り着けないからです。この 2 つは必ずセットで書きます。
IDE には
equalsとhashCodeをまとめて生成する機能があります。片方だけ手書きするより、生成させて必要なフィールドだけ選ぶ方が確実です。
やってみよう
Solution.uniqueCount(int[] arr) を完成させて、配列に何種類の値が入っているかを返してください。
ファイルの先頭に import java.util.stream.IntStream; を書き、IntStream.of(arr) から Stream を作ります。distinct() を通してから、要素の個数を数えてください。今回扱うのは int なので、equals や hashCode を自分で書く必要はありません。
数えた結果は long で返ってくるので、(int) にキャストしてから return します。空配列を渡したときは、通る要素が 1 つも無いので 0 になります。
要件
- ファイル先頭で
import java.util.stream.IntStream;を書くこと IntStream.of(arr)からStreamを作り、distinct()を必ず使うことcount()の戻り値 (long) をintにキャストしてreturnすること
入出力例
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