システムエンジニア兼IT講師の備忘録

技術やトレーニングテクニックなどを思いのままに発信していきます。

HashSetの利用とhashCodeメソッド、equalsメソッドの実装

こんにちは、お久しぶりです。

本日は、JavaのHashSetの簡単な使い方をご紹介していきます。

HashSetの特徴

HashSetは、ArrayListやLinkedList、HashMapなどと並んで「コレクションフレームワーク」と呼ばれる
Javaのライブラリの一つであり、色々な場面で多用されています。

超初心者の方は、とりあえず「Javaでの配列のようなもの」と考えておけば大丈夫でしょう。

上述した通り、コレクションフレームワークにはいくつかの種類があるのですが HashSetの大きな特徴は一つ、「重複を許さないこと」です。

では、書き方を見ていきましょう。

書き方

書き方はこんな感じです。

HashSet<String> sampleSet = new HashSet<String>();

ちなみに、Java SE 7 以降は右辺側の<>の中身を省略してもOKです。
俗に、「ダイアモンド演算子」と呼ばれています。

HashSet<String> sampleSet = new HashSet<>();

こんな感じですね。

<>の中には、配列に入れたいモノのデータ型を入れることができます。 文字列ならStringといった感じですね。

ちなみに、int型やdouble型等の「基本データ型」と呼ばれるものを指定したい場合は、
ラッパークラスと呼ばれるものを使っていきます(まあ、単純に置き換えて頂ければとりあえずOKです)。

基本データ型とラッパークラスの対応は以下を参考にしてください。

byte → Byte
short → Short
int → Integer
long → Long
float → Float
double → Double
char → Character
boolean → Boolean

基本的に、データ型名の先頭を大文字にすればOKなのですが、intとcharだけちょっと違いますのでご注意を。

要素の追加

HashSetクラスでは、要素の追加をaddメソッドを用いて簡単に行うことができます。
例えば、上述のsetに要素を追加するとしたら、こんな感じです。

HashSet<String> sampleSet = new HashSet<String>();

sampleSet.add("APPLE");
sampleSet.add("ORANGE");

ちなみに、冒頭でもご紹介した通り、HashSetは同一の要素がaddされた場合、無視します。
これによって、「重複を許さない」という特徴を実現しているのですね。

sampleSet.add("APPLE");
sampleSet.add("APPLE");

System.out.println(sampleSet);

上記コードでは、"APPLE"という要素を二回addしていますが、最終的にsetに格納されているオブジェクトは一つのみになります。

どのように「同一のオブジェクトである」とみなしているのか?

「同一のオブジェクトである」とみなされる要因は2つあります。

  • ハッシュコードが一致する
  • equalsメソッドの戻り値がtrueである

javaの内部で処理される順番は、
①「ハッシュコードが一致するかどうか」
②「equalsメソッドの戻り値がtrueかどうか」

となっています。


例えば、上記で挙げたStringオブジェクトの実装を見てみると・・・
hashCodeメソッドはこんな感じ。

    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            hash = h = isLatin1() ? StringLatin1.hashCode(value)
                                  : StringUTF16.hashCode(value);
        }
        return h;
    }

続いて、equalsメソッドです。

    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String aString = (String)anObject;
            if (coder() == aString.coder()) {
                return isLatin1() ? StringLatin1.equals(value, aString.value)
                                  : StringUTF16.equals(value, aString.value);
            }
        }
        return false;
    }

何言ってるんだ?と思うでしょうけれども、とりあえず
「同じ文字列だったらhashCodeでは同じハッシュコードを返し、equalsではtrueを返すように」
という実装になっていると思って頂ければOKです。

このように、Java側で実装してくれているStringクラスのようなものであれば、 既にequalsやhashCodeは実装済みであるケースが多いですので、あまり気にせずHashSetを使っても問題ないと思います。

自作クラスにhashCodeとequalsを実装する

では、自作クラスにこれら2つのメソッドを実装するときにはどうしたら良いでしょうか?

題材として、自作のこんなクラスを使っていきたいと思います。

public class Human {

    private String name; //名前
    private int age; //年齢
    private String address; //住所
    private String email; //メールアドレス

        //コンストラクタ
    public Human(String name, int age, String address, String email) {
        this.name = name;
        this.age = age;
                this.address = address;
                this.email = email;
    }
      
        //getter、setterなどは省略
}

はい、よくある「人間クラス」的な感じですかね。

人間には年齢、性別、住所等様々な情報があると思いますが、「同一人物であるかどうか」を判別する場合、 すべての情報を使っているとは限らないですよね。
例えば、クレジットカードの審査等を行う場合には、氏名と携帯電話の番号等が主なキーになるようです。

今回は、仮に名前と年齢が同一であれば同じ人物であるとしましょう。

hashCodeの実装

public int hashCode(){
        return (name + age).hashCode();
}

equalsの実装

public boolean equals(Object obj){
        Human tmpHuman = (Human)obj;
        
        if(tmpHuman.getName().equals(this.getName()) && tmpHuman.getAge == this.getAge()){
                return true;
        }else{
                return false;
       }
}

はい、かなり簡易的なものですが、簡単な実装はこんな感じになると思います。
hashCodeでは、氏名と年齢を連結した文字列のハッシュ値を返すようにしています。
必然的に、同一の氏名・年齢を持つオブジェクトであれば、同一のハッシュ値になります。

equalsでは、比較対象のオブジェクトを受取り、そのオブジェクトの氏名・年齢が自分のものと同一であれば
trueを返すような実装になっています。

開発環境でEclipse等を使っている場合には、自動生成の機能がありますので、
そちらを使っても良いでしょう。

まとめ

  • HashSetは、同一のオブジェクトを排除しつつ管理していくコレクション・フレームワーク
  • String等の既存ライブラリオブジェクトであれば、同一オブジェクトは勝手に排除される(基本的には)
  • 自作クラスの場合は、勝手に排除されないので自分でhashCodeおよびequalsメソッドを実装する
  • 評価順序はhashCode→equalsなので、両方の実装が必要