【Java】独自クラスのListで重複した要素を除外する

# はじめに

Javaにて自分で作成したクラスのListから重複した要素を除外する方法について、いつもやり方を探してしまうので個人的なメモとしてまとめてみます。

# 前提

こんな感じの`Person`クラスがあったとします。

```java:Person.java
public class Person {
    private String id;
    private String name;
    private int age;

    public Person(String id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public String getId() {
        return this.id;
    }

    public String getName() {
        return this.name;
    }

    public int getAge() {
        return this.age;
    }

    // 確認用
    @Override
    public String toString() {
        return "[" + this.id + ", " + this.name + ", " + this.age + "]";
    }
}
```

下記のような形で`Person`クラスのリストがあります。
そのリストの中から重複した要素を除外したいというケースになります。

```java:Sample.java

public class Sample {
    public static void main(String[] args) {
        List<Person> personList = Arrays.asList(
                new Person("00001", "テスト太郎", 32),
                new Person("00002", "テスト次郎", 27),
                new Person("00003", "テスト三郎", 25),
                new Person("00001", "テスト太郎", 32),
                new Person("00003", "テスト三郎", 25),
                new Person("00001", "テスト太郎", 36)
        );

        List<Person> result = deduplicate(personList);

        // 実行結果の確認
        System.out.println(result.toString());
    }

    /**
     * 重複した要素を除外して返却する。
     * @param personList
     * @return
     */
    public static List<Person> deduplicate(List<Person> personList) {
        // ここに処理を書く！
        // ほげほげ
        return personList;
    }
}

```

# 方法1：equals()とhashCode()をOverrideしてStream や HashMapを使う

対象のクラスにて`equals()`と`hashCode()`をOverrideします。

```java:Person.java
public class Person {

    // 省略

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (obj instanceof Person) {
            Person p = (Person) obj;
            return p.id.equals(this.id) && p.name.equals(this.name) && p.age == this.age;
        }
        return false;
    }

    @Override
    public int hashCode() {
        return (this.id + this.name + this.age).hashCode();
    }
}
```

上記をOverrideすることで、`Stream API`の`distinct()`で重複除外ができます。

```java:Sample.java
    public static List<Person> deduplicate(List<Person> personList) {
        return personList.stream().distinct().collect(Collectors.toList());
    }
```

```:実行結果
[[00001, テスト太郎, 32], [00002, テスト次郎, 27], [00003, テスト三郎, 25], [00001, テスト太郎, 36]]
```

または、一度`HashSet`とすることで、重複要素の除外ができます。（順序を変更したくない場合は`LinkedHashSet`を使用します。）

```java:Sample.java
    public static List<Person> deduplicate(List<Person> personList) {
        return new ArrayList<>(new LinkedHashSet<>(personList));
    }
```


# 方法2：適当なキーを指定してMapに入れる

上記の方法が使えないケースもあるかと思います。

* `equals`、`hashCode`のOverrideができない、したくない。（なんかプロジェクトの問題とかで）
* クラスの中の一部のプロパティだけをキーにしたい

そんなときは以下で対応できるかもしれません。


```java:Sample.java
    public static List<Person> deduplicate(List<Person> personList) {
        Map<String, Person> map = new HashMap<>();
        for (Person person : personList) {
            // IDと名前だけをキーにする
            String key = person.getId() +"_" + person.getName();

            // 上記のキーで要素を格納
            map.put(key, person);
        }
        // MapをListに変換
        return new ArrayList<>(map.values());
    }
```

上記はIDと名前のみをキーとした場合の例です。

IDと名前を結合した文字列をMapのキーとして使用します。
同キーであれば上書きされます。（リストの後にある要素が勝ちます。）
なので実行結果は下記の通りとなります。

```:実行結果
[[00001, テスト太郎, 36], [00002, テスト次郎, 27], [00003, テスト三郎, 25]]
```

36歳の方の`テスト太郎`が残りました。

反対に先に来た方を残したい場合は、同キーの要素を`continue`でスキップします。

```java:Sample.java
    public static List<Person> deduplicate(List<Person> personList) {
        Map<String, Person> map = new HashMap<>();
        for (Person person : personList) {
            // IDと名称だけをキーにする
            String key = person.getId() +"_" + person.getName();

            // 同じキーはスキップ
            if(map.containsKey(key)) {
                continue;
            }

            // 上記のキーで要素を格納
            map.put(key, person);
        }
        // MapをListに変換
        return new ArrayList<>(map.values());
    }
```

~~なんか自分が知らないだけでStream APIに使えそうなメソッドがありそうな気がしないでもない。~~

# [追記]方法3：Comparatorを定義してTreeSetに詰め込む

コメントにて@saka1029さんよりご教示いただいた方法です。
`Comparator`を定義し、`TreeSet`に詰め込む方法になります。
`Comparator`に定義したプロパティをキーとして重複した要素を除外します。
※重複した場合、リストの先頭に近い方の要素が残ります。

```java:Sample.java
    public static List<Person> deduplicate(List<Person> personList) {
        // Comparatorを定義
        Comparator<Person> personComparator = Comparator
                .comparing(Person::getId)
                .thenComparing(Person::getName);
        // TreeSetに詰め込む
        Collection<Person> personCollection = personList.stream()
                .collect(Collectors.toCollection(() -> new TreeSet<>(personComparator)));

        // [[00001, テスト太郎, 32], [00002, テスト次郎, 27], [00003, テスト三郎, 25]]
        return new ArrayList<>(personCollection);
    }
```

# 終わりに。

なんか他に良さげな方法がありましたらコメントでご教示いただければ幸いです！