[Java] 11. StringのhashCodeとequals、そしてtoStringの再定義(override)


Study / Java    作成日付 : 2019/08/20 00:42:04   修正日付 : 2021/01/11 17:51:53

こんにちは。明月です。


この投稿はJavaのStringのhashCodeとequals、そしてtoStringの再定義(override)に関する説明です。


以前に私がメモリ割り当てに関して説明する時にメモリアドレスに関してhashCodeに関して説明したことがあります。

link - [Java] 10. メモリの割り当て(stackメモリとheapメモリ、そしてnew)とCall by reference(ポインタによる参照)


このhashCodeはメモリのアドレス値をハッシュしたことで割り当てたメモリたびに別に表示されます。(ハッシュアルゴリズムの限界で他のメモリに割り当てても同じハッシュ値が出る可能性がある。)ということに説明しました。

// クラス
public class Example {
  // 変数
  private int data;
  // コンストラクタ
  public Example(int data) {
    // 変数の値を設定
    this.data = data;
  }
  // 出力関数
  public Example print() {
    // コンソール出力
    System.out.println("data - " + this.data);
    // 自分のインスタンスポインタをリターン
    return this;
  }
  // 実行関数
  public static void main(String... args) {
    // Exampleインスタンス生成
    Example ex1 = new Example(1);
    Example ex2 = new Example(1);
    // コンソール出力
    System.out.println("ex1 hashCode - " + ex1.hashCode());
    System.out.println("ex2 hashCode - " + ex2.hashCode());
  }
}


それならStringタイプの変数のhashCodeを確認しましょう。

// クラス
public class Example {
  // 実行関数
  public static void main(String... args) {
    // 変数宣言
	String a = "hello world";
	String b = "hello world";
	String c = new String("hello world");
    // コンソール出力
    System.out.println("a hashCode - " + a.hashCode());
    System.out.println("b hashCode - " + b.hashCode());
    System.out.println("c hashCode - " + c.hashCode());
  }
}


同じ値をStringタイプの変数に格納しましたが、hashCodeの値が同じです。aとbは同じ二重引用符で格納したものは同じ値だと思ってもnewで格納したStringもhashCodeが同じです。

この関して私も知りたいから様々なところを調べてみましたが、ちょうどこれが正解だと思うほどの説明はないです。でも自分の経験だと次みたいになるではないかと思います。


まず、StringのhashCodeソースを見ればObjectクラスからStringのhashCode関数は再定義しています。


Stringの文字列はJavaでbyte(unsigned char)の配列のタイプです。

つまり、String aの値はa[0] = 'h'、a[1] = 'e'、a[2] = 'l'、a[3] = 'l'、a[4] = 'o'になっています。でも、このhはアスキーコードの定数値の104でeは101の値です。

この文字は一つずつ定義されているし、変わらない値です。これはhashで並べると"s[0]*31^(n-1)+s[1]*31^(n-2)+...+s[n-1]"の形で計算され、hashCodeになります。

link - https://www.tutorialspoint.com/java/java_string_hashcode.htm

これをプログラムで実装すると下記通りになります。

// クラス
public class Example {
  // 実行関数
  public static void main(String... args) {
    // 変数宣言 - 値が大きくなるとoverflowで値が変わる可能性があるので小さい値で表現する。
    String a = "hello";
    // コンソール出力
    System.out.println("a hashCode - " + a.hashCode());
    // aをchar配列に
    char[] c = a.toCharArray();
    // ハッシュコード計算
    int hashCode = calcHashCode(c, c.length - 1, 0);
    // コンソール出力
    System.out.println("calc hashCode = " + hashCode);
  }
  // ハッシュ値を計算
  public static int calcHashCode(char[] array, int p, int n) {
    if (p < 0) {
      return 0;
    }
    // s[0]*31^(n - 1) + s[1]*31^(n - 2) + ... + s[n - 1]
    return array[p] * (int) Math.pow(31, n) + calcHashCode(array, p - 1, n + 1);
  }
}


これがStringのhashCode関数から同じ値がリターンする理由です。


でも、実は私がこの理由で長い時間でJavaを理解できなかったことがあります。それがhashCodeはメモリアドレスか、違うかということです。

hashCodeはメモリアドレス値のStringの値はリテラル値なのでいつも同じ値をリターンしますと思いました。そう思えば矛盾になることが同じString値を宣言することは同じだと思っても、concatで文字列を併せる時やプログラムを実行するたびに値が同じだということが説明ができません。

それならJavaのhashCodeはメモリアドレス値ではないかと言えば実はメモリのアドレス値だといったことはJavaのドキュメントでは何処でも説明したことはありませんが、クラスたびに固有な値を持っていますということは合ってます。(Objectから再定義してない場合。)


それで私がhashCodeの関数に関してこれが何かと思いました。これか重要だと思わない可能性がありますが、hashCodeは記号計算イコール(=)と関係あるので上のa == "hello"がtrueになるかfalseになるかの関係があることです。


改めて確認すればStringクラスはhashCode関数を再定義しました。hashCode関数を再定義。。。。なのでStringではメモリアドレス値ではないです。

そのため、StringのhashCodeはメモリアドレス値ではないです。ただ、文字列に関するハッシュ値です。

このStringのhashCode関数のせいでhashCodeはメモリアドレス値だ、違うんだということが多いですが、私の考えは正確にメモリアドレス値ではないですが、クラスの固有値では合ってると思います。

再.定.義.を.し.て.な.い.ならです。


次のequalsはクラスを比較関数です。

// クラス
public class Example {
  // 変数
  private int data;
  // コンストラクタ
  public Example(int data) {
    // 変数値を設定
    this.data = data;
  }
  // 出力関数
  public Example print() {
    // コンソール出力
    System.out.println("data - " + this.data);
    // 自分のインスタンスポインタをリターン
    return this;
  }
  // Objectクラスから再定義
  @Override
  public boolean equals(Object val) {
    // 比較クラスがExampleクラスでは無ければfalse
    if(val.getClass() != Example.class) {
      return false;
    }
    // Exampleクラスで
    Example d = (Example)val;
    // 値が同じならtrue、違うならfalse
    return d.data == this.data;
  }

  // 実行関数
  public static void main(String... args) {
    // Exampleクラス宣言
    Example ex1 = new Example(1);
    Example ex2 = new Example(1);
    // コンソール出力
    System.out.println("ex1 hashCode - " + ex1.hashCode());
    System.out.println("ex2 hashCode - " + ex2.hashCode());
    // ==の記号で比較する場合
    System.out.println("ex1 == ex2 - " + (ex1 == ex2));
    // equals関数で比較場合
    System.out.println("ex1 equals ex2 - " + (ex1.equals(ex2)));
  }
}


上の例を見れば関係演算子==とequalsが別の結果を出力します。Exampleクラスでequals関数を再定義しました。

なのでequalsを使えば同じExampleクラスだし、値が同じならtrueをリターンします。つまり、同じインスタンス(メモリアドレス値が同じ)かを確認することではなく、メンバー変数dataが同じならtrueになることにします。

でも、記号(==)を使う時には同じインスタンスを確認します。


また、Stringのことで戻りましょう。

// クラス
public class Example {
  // 実行関数
  public static void main(String... args) {
    // 変数宣言
    String a = new String("hello world");
    String b = "hello world";
    String c = "hello world";
    // コンソール出力
    System.out.println("a == b - " + (a == b));
    System.out.println("a equals b - " + (a.equals(b)));
    System.out.println("b == c - " + (b == c));
  }
}


上の結果をみればaとbは関係演算子で比較しましたが、falseになります。上の論理ならクラスが別に宣言しましたので別のことだと思います。なのでStringを比較する時には必ずequals関数を使わなければならないです。

ここで人を迷うことにすることがb==cみたいなことです。別に宣言しておりますが、b==cがの結果がtrueになります。それで我々は文字列で比較することができる時もあるしできない時もあるんだと思います。

私の考えはリテラルの差異見たいです。つまり、C/C++でchar[]とconst char*の差異かな。


改めてまとめるとStringを比較する時には必ずequals関数を使わなければならないです。

ここまでがhashCodeとequals関数に関する説明です。


Javaでは原始データタイプではなければすべてのクラスはObjectクラスを継承します。

親クラスから関数を再定義する時にはアトリビュート@Overrideを使えば再定義になります。ここで再定義というのは親クラスから宣言した関数を継承する時に再定義することです。

つまり、該当なクラスを割り当てして使えば再宣言した関数が呼び出されます。

// クラス
public class Example {
  // 変数
  private int data;

  // コンストラクタ
  public Example(int data) {
    // 変数値を設定
    this.data = data;
  }
  // toString関数の再定義
  @Override
  public String toString() {
    // コンソール出力
    System.out.println("data - " + data);
    // toString結果リターン
    return "Hello world";
  }
  // 実行関数
  public static void main(String... args) {
    // Exampleインスタンス生成
    Example ex1 = new Example(1);
    // toString結果出力
    // クラスがStringタイプではない場合に自動にtoStringを呼び出す。
    System.out.println("ex1.toString - " + ex1);
  }
}



ここまでJavaのStringのhashCodeとequals、そしてtoStringの再定義(override)に関する説明でした。


ご不明なところや間違いところがあればコメントしてください。

最新投稿