未来エンジニア養成所Blog

月単価180万以上のプログラミング講師がプログラミングを皆に楽しんでもらうための情報をお届けします。

【Java】オブジェクト指向の応用問題3-6

title


問題3-6

あるグループに属するメンバーを表現するMemberクラスがあります。

フィールドとして、int型のidとString型のnameを持ち、基本的なコンストラクタを持ち、toStringメソッドをオーバーライドしているとても基本的なクラスです。


Memberクラスを使用するHashSetEqualsPracticeクラスのmainメソッドはすでに完成しています。
(変更は禁止です)


このプログラムを変更せずに、そのままコンパイル・実行すると次のように出力されます。


【正しくない実行結果(順不同)】

ID:3 NAME:村田
ID:1 NAME:ニセ山田
ID:5 NAME:川田
ID:1 NAME:山田
ID:2 NAME:高田
ID:4 NAME:吉田


メンバーを管理するためのグループを表現するHashSet型のgroup変数を作成しています。
このグループは、メンバーを管理するためにIDの値を使用し、同じIDの値を持つメンバーは加入できないようにしようと思っています。


しかし、先ほどの実行結果では、グループにIDが1の「ニセ山田」が加入できています。


Setコレクションは、本来等値のオブジェクトを登録できないように設計されています。
「同じIDの値を持つMemberオブジェクトは等値」となるように、Memberクラスを修正してください。


ヒントは、Objectクラスの持つメソッドを2つオーバーライドすることです。


【正しい実行結果(順不同)】

ID:1 NAME:山田
ID:2 NAME:高田
ID:3 NAME:村田
ID:4 NAME:吉田
ID:5 NAME:川田


【HashSetEqualsPractice.java】

import java.util.*;
public class HashSetEqualsPractice {
    public static void main(String[] args) {

        // グループを結成
        HashSet<Member> group = new HashSet<Member>();

        // メンバーを追加
        group.add(new Member(1, "山田"));
        group.add(new Member(2, "高田"));
        group.add(new Member(3, "村田"));
        group.add(new Member(4, "吉田"));
        group.add(new Member(5, "川田"));

        // 偽物がメンバーとして追加!
        group.add(new Member(1, "ニセ山田"));

        // メンバー紹介
        for(Member member : group) {
            System.out.println(member);
        }

    }
}


// メンバークラスは不完全です
class Member {

    private int id;        // ID
    private String name;         // 名前

    // コンストラクタ
    public Member(final int id, final String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "ID:" + id + " NAME:" + name;
    }

}


解答例

【HashSetEqualsPractice.java】

import java.util.*;
public class HashSetEqualsPractice {
    public static void main(String[] args) {

        // グループを結成
        HashSet<Member> group = new HashSet<Member>();

        // メンバーを追加
        group.add(new Member(1, "山田"));
        group.add(new Member(2, "高田"));
        group.add(new Member(3, "村田"));
        group.add(new Member(4, "吉田"));
        group.add(new Member(5, "川田"));

        // 偽物がメンバーとして追加!
        group.add(new Member(1, "ニセ山田"));

        // メンバー紹介
        for(Member member : group) {
            System.out.println(member);
        }

    }
}

// メンバークラス
class Member {

    private int id;        // ID
    private String name;         // 名前

    // コンストラクタ
    public Member(final int id, final String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "ID:" + id + " NAME:" + name;
    }

    @Override
    public boolean equals(Object obj) {

        // 同一ならtrue
        if(this == obj) {
            return true;
        }

        // Member型にキャストできないならfalse
        if(!(obj instanceof Member)) {
            return false;
        }

        // IDが同じなら等値とみなす
        return this.id == ((Member)obj).id;
    }

    @Override
    public int hashCode() {
        return id;
    }
}


解説

今回はSet系コレクションに関する問題です。

よく使用されるList系コレクションは「同一の要素を重複して保持できる・順序を管理している」のに対し、Set系コレクションは「同一および等値の要素を保持できない・順序を管理していない」という大きな違いがあります。



同じIDを持つメンバーをグループに入らせないためにはどうすれば良いでしょうか。


答えは「同じIDを持つメンバーオブジェクトは等値という扱いにする」わけです。


ここで、Objectクラスのequalsメソッドのオーバーライドに気付いた人は良い感じです。

しかし、くれぐれも次のようなミスはしないようにしましょう。

// オーバーライド失敗!
public boolean equals(Member m) {
    ...
}


いかにもオーバーライド風ですが、これは実際にはオーバーロードです。

Objectクラスのequalsメソッドの引数はObject型なので、オーバーライドする方も必ず引数がObject型でないといけません。

上記のコードは引数がMember型になっていますので、メソッド名が同じで引数が異なるのはオーバーロードです。



このようなミスは@Overrideアノテーションを使う習慣が身についていればすぐに気が付くことができます。



equalsメソッドをオーバーライドする書式パターンは、次のようなものが良く使われます。

@Override
public boolean equals(Object obj) {

    // 同一ならtrue
    if(this == obj) {
        return true;
    }

    // Member型にキャストできないならfalse
    if(!(obj instanceof Member)) {
        return false;
    }

    // IDが同じなら等値とみなす
    return this.id == ((Member)obj).id;

}


まず、引数が自分自身と同一オブジェクトであれば無条件にtrueを返します。

次に、引数を自分自身のクラス型にキャストできないようであれば無条件にfalseを返します。

最後に、引数を自分自身のクラス型にキャストした上で、等値とみなす条件を記述します。

今回は「IDの値が同じ」であればtrueを返します。



実は、ここまでは割とよく知られた話なので、記述できた人も多かったと思います。

しかし、このequalsメソッドのオーバーライドの追加だけでは正しく動作しません。

まだグループにIDが1のニセ山田が加入できてしまします。



Javaでいう「等値」とは何か?

次の説明は重要ですので、良く理解しておきましょう。



「等値とは、equalsメソッドによる比較の結果がtrueを返し、かつ2つのオブジェクトのhashCodeメソッドの戻り値が同じ値を返すこと」



ハッシュコードとは、オブジェクトを識別するためのint型の数値のことです。

equalsメソッドと同じく、hashCodeメソッドもObjectクラスに存在しています。



そして「equalsメソッドをオーバーライドしたなら、必ずhashCodeメソッドもオーバ-ライドしなければいけない」という暗黙のルールが存在します。



今回はint型のIDを等値の条件にしているので、hashCodeメソッドの戻り値をIDの値にすれば等値の条件を満たします。

つまり、次のコードを追加すれば良いわけです。

@Override
public int hashCode() {
    return id;
}


このhashCodeメソッドのオーバーライドを追加すれば、同じIDのメンバーは追加できなくなり、正しい動作になります。



Set系コレクションはList系コレクションほど使用頻度は高くないかもしれません。

しかし、今回の「等値」の考え方はしっかりと理解しておきましょう。

そうしないと、Map系コレクションのキーに使用すると不具合を起こしたりといったトラブルの元になります。



ちなみに、equalsメソッドの比較の結果がfalseを返す2つのオブジェクトが、同じハッシュコードを返すことは別に問題ありません。

もちろん、その場合はできるだけ異なるハッシュコードを返した方が良いとされています。


参考図書



LINE公式アカウント

仕事が辛くてたまらない人生が、仕事が楽しくてたまらない人生に変わります。
【登録いただいた人全員に、無料キャリア相談プレゼント中!】


LineOfficial

友だち追加



【まついのLINE公式アカウントはこちらから!】
Line公式アカウント