Javaで equalsと hashCodeを使ってみた（+ HashMap）

こんにちは。
この記事では、私が勉強した内容を実際に業務にどのように活かしたのかについて説明していきます。

Javaを勉強し、業務で使用していても**equals**と**hashCode**をoverrideして使う場面は、それほど多くはないと思います。
私も、**equals**と**hashCode**について勉強したことはありますが、実際に使ったことは全くありませんでした。
しかし、今回のプロジェクトで使う機会があって、その経験を皆さんに共有したいと思います。

# 問題状況（案件）
病院で診療情報を保存する状況があると想定します。
医師は患者の診療を終え、その診療情報を保存するたびに、以下のような形式でDBに保存されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678234/fde42ef5-e09a-db64-e2fa-e0f063895380.png)

各カラムは
- 患者ID：患者に付与されるID pk
- 作成日：診療記録作成日 pk
- 項目：診療に関する項目
- 項目値：項目に関する診療情報

そして、上記のテーブルの情報を加工し、以下のようにCSVで出力をします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678234/1b4577a6-59c7-cdb1-bf5d-c2ea90e5fdfd.png)
- 患者ID：患者に付与されるID
- 検査日：項目「**検査日**」の項目値
- 診療科：項目「**診療科**」の項目値
- 症状：項目「**症状**」の項目値

ここで、CSVデータはテーブルの「**患者ID**」と「**作成日**」をKEYとして一つのデータとみなし、加工したものです。

今回は、**診療記録テーブルのデータをCSVの出力データに変換**する処理の説明です。
変換処理で、どこで**equals**と**hashCode**を使ったのか書いていきます。

# 実装
### EntityとDTO作成
まず、診療情報テーブルとマッピングするEntityクラスを実装します。
<font color="red">**※ソースコードは略して書きました。**</font>

**MedicalRecord.java**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678234/78f324db-cfac-957f-0d6c-3df7d7dc8695.png)

Code enumを MedicalRecordの内部に作成しました。
このような書き方をすると、enumがMedicalRecordと直接的な関係性があることが一目で分かります。
また、外部で使用する際は、**MedicalRecord.Code**で使うため重複の心配はないはずです。

次に、CSV出力用のDTOを作成します。

**MedicalRecordDTO.java**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678234/d41758aa-c231-75b7-7ff5-09d3e386d450.png)

簡単にデータが入る変数だけ定義しておきました。
必要な処理はこれから説明しながら詰めていきたいと思いまし。

これで処理の元となるEntityとDTOは作成しました。

### ロジック実装
ロジックを実装するためには、まず、データをどのようにグルーピングするのかについて考えていきます。
MedicalRecordの **patientId**と**createDate**を基準として、MedicalRecordをグルーピングし、グルーピングしたデータをCSV出力用DTO（MedicalRecordDTO）に変換しなければなりません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678234/cf2c0c97-a3ca-d7d0-80c0-4952de05a271.png)
（絵で表したデータのグループ）

では、どのようにデータをグルーピングし、データを変換すればいいでしょうか？

#### 方法① Listで実装（Badケース）
まず、グルーピングしたデータをListに保存して変換することで想定してみます。
データの形式は恐らく`List<List<MedicalRecord>>`になるでしょう。

この方法は、外のListをループさせ、グルーピングしたListの値と比較を行います。次に、**patientId**と**createDate**が一致すればそのListに保存し、一致しない場合は新しいListを作成し、外のListに保存します。

しかし、この方法は効率的ではありません。
実際にコードで作成してみます。
```java
List<MedicalRecord> records; // DBから取得したデータ

List<List<MedicalRecord>> group = new ArrayList<>(); // グルーピング用List

for (MedicalRecord record : records) {
    // MedicalRecordをグルーピングするため、Loopさせる。
    boolean added = false;
    
    if (group.isEmpty()) {
        // １つ目のデータ処理
        group.add(new ArrayList<>(Collections.singletonList(record)));
        added = true;
    } else {
        // ２つ目以降のデータ処理
        for (List<MedicalRecord> items : group) {
            // グループをLoopさせる。
            if (items.get(0).getPatientId().equals(record.getPatientId())
                    && items.get(0).getCreateDate().equals(record.getCreateDate())) {
                // グループでpatientIdとcreateDateが一致すれば、該当グループに追加
                items.add(record);
                added = true;
                break;
            }
        }
    }
    
    // 一致するグループがない場合、新たなグループを作る。
    if (!added) {
        group.add(new ArrayList<>(Collections.singletonList(record)));
    }
}

```
上記のコードは、グルーピングされたMedicalRecordのListをループさせて、**pateintId**と**createDate**が一致するかしないかを判断する処理です。
データが一致するかをLoopで確認をしているため、MedicalRecordの数が多ければ多いほど、グループが多ければ多いほど非効率的なコードになります。

これはListの特徴のせいです。
Listの場合、値が一致するかしないかを判断するためにはLoopですべてのデータと比較するしかありません。

List以外のコレクションフレームワークの中、**Keyとなる値があるかないか１回で確認できる**のは何があるでしょうか？

それは**Map**です。

#### 方法② Mapで実装 (Goodケース)

Mapは`get(key)`でkeyの値が登録されているのかを１回目で分かるため、Loop処理を行う必要はありません。
ここで、疑問に思うのは、どうすれば**patientId**と**createDate**この２つを同時にMapのkeyで使えるのかだと思います。
ここで<font color="red">**hashCode**</font>と<font color="red">**equals**</font>を使えば、解決できます。

Mapを実装した HashMapや LinkedHashMapのコードを見ると、インスタンスの**hashCode**と**equals**でkeyを比較していることが分かります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678234/582b5d80-4fcf-4107-5746-f989e2d9a8ee.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678234/d1fa7f16-a8e9-d962-6fe1-9b51ad668e48.png)
つまり、「**インスタンスのhashCodeとequalsが一致すれば、同じkeyとする。**」ということです。

:::note info
JavaのMapで使用しているHashTalbeの仕組みについては[この記事](https://qiita.com/alswnd/items/428014509e52297b3a1b)を参照してください。
:::

私はこの点を利用し、MedicalRecordを分類していきます。

まず、MedicalRecordをMapのkeyで使うため、hashCodeとequalsを追加します。

**MedicalRecord.java**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678234/2c2384b8-c804-c125-9079-7944aab57d5b.png)
lombokを活用し、`@EqualsAndHashCode(exclude = {"id", "content", "value"})`を追加しました。
EqualsAndHashCodeアノテーションでid, content, valueを除いたフィールド(patientId, createDate)でhashCodeとequalsを実装するようにしました。

内部では、以下のようなコードが作成されたように動作します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678234/8788ea5b-7e77-5f55-4adc-24b43859e958.png)
**equals**と**hashCode**をオーバーライドしたため、
このインスタンスをkeyで使うと、**patientId**と**createDate**を利用してデータの比較が行われます。

つまり、**equals**と**hashCode**が一致すれば同じ値としてみなすということです。

※<font color="red">**equals**と**hashCode**をオーバーライドをしていない状態で、hashCode()を呼び出すと、メモリ上に保存されているインスタンスのアドレスをもとにhashCodeが作られるので注意してください。</font>

> この記事ではEntityに直接equals()とhashCode()をオーバーライドしていますが、フィールドのデータが変われば、副作用(side effect)の起こる可能性があるため、
VO(Value Object)などの不変オブジェクトで使うのを推奨します。



次にMedicalRecordDTOにそれぞれの項目を分岐で該当するフィールドに保存する処理を追加します。

**MedicalRecordDTO.java**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678234/7ca1fbbc-dd05-d9f3-b42b-13663de049d4.png)
値を外に出してループと分岐処理をするよりは、DTOの中で処理するように作成しました。

最後にMedicalRecordを分類し、CSV出力用のMedicalRecordDTOリストを生成するコードを実装します。

**MedicalRecordConverter.java**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678234/8c6bfc51-5c1d-5813-d63c-638666652341.png)

MedicalRecordConverterにはclassify()と()createMedicalRecordDTOListが定義されています。

classify()ではMedicalRecordリストを引数として、データを分類する作業を行い、
createMedicalRecordDTOList()では分類されたデータをDTOに変換する処理を行っています。

これらのコードがちゃんと動くのかテストコードで確認してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678234/67db2286-1fc3-46ba-905b-ff128f59b75c.png)
分類されたMedicalRecordがちゃんとDTOに変換されたことが確認できました。


# おわりに
いかがでしたか。
hashCodeとequalsについてこういう使い方もあるんだなぐらいで考えていただけると幸いです。

ちょうど、Javaのコレクションフレームワークについて勉強した内容を実務で機会ができたので、遠慮なく使わせていただきます。
やはり、勉強だけだと頭に長く残らないので、実務で活用してみるのが最もいい勉強方法だと思いました。

これからも勉強した内容や、実務での経験したものを共有していきたいと思います。
最後までご覧いただきありがとうございました。