Java : Object.hashCode (ハッシュ・コード) とは
ObjectクラスにはhashCodeというメソッドがあります。
ハッシュ・コード…いまいち良くわかっていない、というかたもいらっしゃるかもしれません。
本記事では、そんなハッシュ・コードの用途、使いかた、オーバーライド方法などをご紹介していきます。
ハッシュ・コード
用途
上記の引用にもあるように、HashMap や HashSet に使うのが主な用途です。
自分で作成したクラスをHashMapのキーとして使いたい場合には、hashCode(とequals)メソッドをオーバーライドする必要があります。
それ以外の用途としては、例えば、アプリでMD5やSHAのようなハッシュを使いたいけどそこまで厳密なものは必要ない…というときに使えるかもしれません。
注意
- MD5やSHA-1はすでに脆弱性のあることが明らかとなっているハッシュ関数です。
データ改竄のチェックなど、厳密性が必要となる用途には使用しないほうがよいでしょう。 - JavaのObject.hashCodeはMD5よりさらに強度は低い(はず)です。
こちらも厳密性が必要となる用途には使用しないほうがよいでしょう。
例
それでは実際にハッシュ・コードを取得してみましょう。
System.out.println("a".hashCode()); // 97
System.out.println("b".hashCode()); // 98
System.out.println("c".hashCode()); // 99
System.out.println("abcdefghijklmnopqrstuvwxyz".hashCode()); // 958031277
System.out.println("あいうえおかきくけこさしすせそたちつてとなにぬねの".hashCode()); // -1189905576
// 巨大な文字列
final var sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
sb.append(i);
}
final var s = sb.toString();
System.out.println(s); // "0123456789101112131415161718192021222324252627282930313..省略.."
System.out.println(s.hashCode()); // -893883282
// インスタンスが違っていても内容が同じであれば、同じハッシュ・コード値を返します。
final var s1 = new String("abcd");
final var s2 = new String("abcd");
System.out.println(s1 != s2); // true
System.out.println(s1.hashCode()); // 2987074
System.out.println(s2.hashCode()); // 2987074
いろいろな文字列からハッシュ・コードを取得してみました。
結果を見るとわかりますが、Object.hashCodeは int型の数値となります。
そして文字列の内容が違えば(基本的に)違うハッシュ・コードになります。(同じ値になる場合もあります)
内容が同じであれば必ず同じハッシュ・コードになります。
3つの規則
Object.hashCodeには3つの規則があります。
- 同じオブジェクトに対してhashCodeメソッドを複数回呼び出しても常に同じハッシュ・コードを返します。
- ただし、equalsで比較される情報が変更されたら(基本的に)違うハッシュ・コードを返します。(同じ値になる場合もあります)
- 同じ内容のオブジェクトでも、アプリケーションの実行ごとに同じハッシュ・コードを返すとは限りません。
final List<String> list = new ArrayList<>();
list.add("abcd");
// 内容に変更がない場合は常に同じハッシュ・コードとなります。
System.out.println(list.hashCode()); // 2987105
System.out.println(list.hashCode()); // 2987105
System.out.println(list.hashCode()); // 2987105
// 内容に変更を加えたらハッシュ・コードも変わります。
list.add("xyz");
System.out.println(list.hashCode()); // 92719448
final var obj = new Object();
System.out.println(obj.hashCode()); // 1307813949
↓アプリを再実行するとハッシュ・コードが変わることがあります。
final var obj = new Object();
System.out.println(obj.hashCode()); // 890275401
- equalsメソッドで2つのオブジェクトが等しくなる場合、その2つのオブジェクトは必ず同じハッシュ・コードを返します。
final var s1 = new String("abcd");
final var s2 = new String("abcd");
System.out.println(s1 != s2); // true (インスタンスが違うことを確認)
System.out.println(s1.equals(s2)); // true
System.out.println(s1.hashCode()); // 2987074
System.out.println(s2.hashCode()); // 2987074
final var p1 = Path.of("sample.txt");
final var p2 = Path.of("sample.txt");
System.out.println(p1 != p2); // true (インスタンスが違うことを確認)
System.out.println(p1.equals(p2)); // true
System.out.println(p1.hashCode()); // 628077580
System.out.println(p2.hashCode()); // 628077580
- equalsメソッドで2つのオブジェクトが異なる場合、その2つのオブジェクトは(基本的に)違うハッシュ・コードを返します。(同じ値になる場合もあります)
この3番目の規則が、少し直感的ではないかもしれません。
ハッシュ・コードはint型なので、例えば Long.hashCode はどうしても衝突する可能性があります。
{
final var value = Long.valueOf(0);
System.out.println(value); // 0
System.out.println(value.hashCode()); // 0
}
{
final var value = Long.valueOf(1);
System.out.println(value); // 1
System.out.println(value.hashCode()); // 1
}
{
final var value = Long.valueOf(2);
System.out.println(value); // 2
System.out.println(value.hashCode()); // 2
}
{
final var value = Long.valueOf(3);
System.out.println(value); // 3
System.out.println(value.hashCode()); // 3
}
...
{
final var value = Long.valueOf(Integer.MAX_VALUE);
System.out.println(value); // 2147483647
System.out.println(value.hashCode()); // 2147483647
}
...
{
final var value = Long.valueOf(0xffffffffL + 1L);
System.out.println(value); // 4294967296
System.out.println(value.hashCode()); // 1
}
{
final var value = Long.valueOf(0xffffffffL + 2L);
System.out.println(value); // 4294967297
System.out.println(value.hashCode()); // 0
}
{
final var value = Long.valueOf(0xffffffffL + 3L);
System.out.println(value); // 4294967298
System.out.println(value.hashCode()); // 3
}
{
final var value = Long.valueOf(0xffffffffL + 4L);
System.out.println(value); // 4294967299
System.out.println(value.hashCode()); // 2
}
HashMap
Object.hashCode の主な用途としてHashMapがあります。
hashCodeを適切に実装しないとHashMapのパフォーマンスに影響する、ということがAPI仕様からわかりますね。
標準API (String や Integer, Path など) をキーにするのであれば、それらのクラスは適切なhashCodeが実装されているので問題ありません。
問題となるのは、自分で独自のクラスを作成して、それをHashMapのキーとしたい場合になります。
それでは実際に最適ではない場合と最適な場合で、パフォーマンスを測定してみましょう。
最適ではないハッシュ・コードでのパフォーマンス例
最適ではないハッシュ・コードの例です。
わかりやすさのために非常にシンプルなクラスとしています。
public class Title {
private final String value;
public Title(String value) {
this.value = value;
}
@Override
public int hashCode() {
// ★わざと固定値を返します。
return 1;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Title title = (Title) o;
return Objects.equals(value, title.value);
}
@Override
public String toString() {
return "Title(" + value + ")";
}
}
Titleクラスは文字列を1つ保持するだけのクラスです。
hashCode は意図的に最適ではない固定値の1を返します。
内容の違うオブジェクトでも、同じハッシュ・コードを返してOKです。(パフォーマンスはさておき)
hashCode の3番目の規則ですね。
equals は正しく実装されています。(equalsの詳細は後述します)
HashMapのキーにTitleクラスを使う簡単な例です。
final var map = new HashMap<Title, String>();
map.put(new Title("タイトル=0"), "コンテンツ=0");
map.put(new Title("タイトル=1"), "コンテンツ=1");
map.put(new Title("タイトル=2"), "コンテンツ=2");
System.out.println(map); // {Title(タイトル=0)=コンテンツ=0, Title(タイトル=1)=コンテンツ=1, Title(タイトル=2)=コンテンツ=2}
System.out.println(map.get(new Title("タイトル=1"))); // "コンテンツ=1"
それではパフォーマンスを測定するために、10000件のputとgetを実行してみます。
final int count = 10000;
final var map = new HashMap<Title, String>();
// put
{
final var start = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
map.put(new Title("タイトル=" + i), "コンテンツ=" + i);
}
final var end = System.currentTimeMillis();
System.out.println(end - start); // 1010ミリ秒
}
// get
{
final var start = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
map.get(new Title("タイトル=" + i));
}
final var end = System.currentTimeMillis();
System.out.println(end - start); // 983ミリ秒
}
10000件の合計がput/getどちらも約1秒(1000ミリ秒)でした。(実行するマシンパワーによって測定結果は上下します)
20000件で約7秒、30000件で約20秒、40000件で約40秒、50000件で約90秒でした。
件数に比例するわけではなく、もっと急勾配で増えていくようです。
ここで注意したいのは、最適ではないけど間違いではないということです。
つまり、パフォーマンスだけに影響して、結果は正しくなります。
最適なハッシュ・コードでのパフォーマンス例
次に最適なハッシュ・コードを使います。
Java 7以降であれば Objects.hash という便利なAPIが利用できます。
public class Title2 {
private final String value;
public Title2(String value) {
this.value = value;
}
@Override
public int hashCode() {
// ★最適なハッシュ・コードを返します。
return Objects.hash(value);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Title2 title = (Title2) o;
return Objects.equals(value, title.value);
}
@Override
public String toString() {
return "Title(" + value + ")";
}
}
先ほどと同じように10000件のput/getを計測します。
final int count = 10000;
final var map = new HashMap<Title2, String>();
// put
{
final var start = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
map.put(new Title2("タイトル=" + i), "コンテンツ=" + i);
}
final var end = System.currentTimeMillis();
System.out.println(end - start); // 19ミリ秒
}
// get
{
final var start = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
map.get(new Title2("タイトル=" + i));
}
final var end = System.currentTimeMillis();
System.out.println(end - start); // 5ミリ秒
}
10000件ではput/get平均して約10ミリ秒。
最適ではないハッシュ・コードと比べてざっくりと約100倍速くなりました。
20000件で約20ミリ秒、30000件で約30ミリ秒、40000件で約40ミリ秒、50000件で約50ミリ秒…
件数に対しておおよそ比例の関係ですね。
(正確にはputよりgetのほうが全体的に速めでした)
put/get件数 | 最適なハッシュ・コード 処理時間(ミリ秒) |
最適ではないハッシュ・コード 処理時間(ミリ秒) |
---|---|---|
10000 | 10 | 1000 |
20000 | 20 | 7000 |
30000 | 30 | 20000 |
40000 | 40 | 40000 |
50000 | 50 | 90000 |
独自クラスでhashCodeをオーバーライド
レコードクラス
Java 16から言語仕様にレコードクラスが追加されました。
レコードクラスを使うと、単純なデータ構造のみのクラスをシンプルに記述することができます。
そして、equals や hashCode を自動でオーバーライドして適切に実装してくれます。
少しコード例を見てみましょう。
public record Book(String title, int totalPage) {
}
final var book = new Book("タイトル", 100);
System.out.println(book.getTitle()); // タイトル
System.out.println(book.getTotalPage()); // 100
Bookは、タイトルと総ページを持つレコードクラスです。
final var book1 = new Book("タイトル", 100);
final var book2 = new Book("タイトル", 100);
System.out.println(book1.equals(book2)); // true
System.out.println(book1.hashCode()); // -976936516
System.out.println(book2.hashCode()); // -976936516
book1とbook2は、インスタンスは違いますが内容は同じです。
equlasは true を返し、hashCodeは同じ値となります。
もしJava 16以降を使っているのであれば、レコードクラスを使うことを検討してみましょう。
レコードクラスについては「レコードクラスの基本」にもまとめていますので、そちらもご参照ください。
Objects.hashを使う
Java 7以降の環境であれば、Objects.hash を使うことをおすすめします。
複数の値のハッシュコードも適切に計算してくれます。
public class Sample {
private final String a;
private final int b;
...
@Override
public int hashCode() {
return Objects.hash(a, b);
}
}
Java 7より前の環境だと、足したり 31 をかけたりと…理解しにくい計算がからんできて大変です。
public class Sample {
private final String a;
private final int b;
...
@Override
public int hashCode() {
int result = a != null ? a.hashCode() : 0;
result = 31 * result + b;
return result;
}
}
equalsもオーバーライドが必要
hashCode をオーバーライドする場合は、equals もオーバーライドする必要があります。
API仕様では5つの規則について記載があります。
- 反射性: null以外の参照値xについて、x.equals(x)はtrueを返します。
- 対称性: null以外の参照値xおよびyについて、y.equals(x)がtrueを返す場合に限り、x.equals(y)はtrueを返します。
- 推移性: null以外の参照値x、y、およびzについて、x.equals(y)がtrueを返し、y.equals(z)がtrueを返す場合、x.equals(z)はtrueを返します。
- 一貫性: null以外の参照値xおよびyについて、x.equals(y)の複数の呼出しは、このオブジェクトに対するequalsによる比較で使われた情報が変更されていなければ、一貫してtrueを返すか、一貫してfalseを返します。
- null以外の参照値xについて、x.equals(null)はfalseを返します。
もしIDEを使っているのであれば、equalsやhashCodeメソッドを自動生成してくれる機能があるのでおすすめです。
下記はIntelliJ IDEA 2021.3 (Community Edition)で自動生成した例です。
public class Sample {
private String a;
private int b;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Sample sample = (Sample) o;
return b == sample.b && Objects.equals(a, sample.a);
}
@Override
public int hashCode() {
return Objects.hash(a, b);
}
}
インスタンス自体が同じか確認して、比較相手がnullかどうか、クラスが違うか…あとはフィールドを確認して…という感じですね。
このあたりはある程度定型化しています。
配列のハッシュ・コードは注意
配列のハッシュ・コードは注意が必要です。
配列オブジェクトの hashCode メソッドでは配列の内容が考慮されません。
final int[] array1 = {1, 2, 3};
final int[] array2 = {1, 2, 3};
// 内容が同じでもハッシュ・コードが異なります。
System.out.println(array1.hashCode()); // 1531369723
System.out.println(array2.hashCode()); // 1269511312
内容を考慮したハッシュ・コードを取得したい場合は、Arrays.hashCode を使いましょう。
final int[] array1 = {1, 2, 3};
final int[] array2 = {1, 2, 3};
// Arrays.hashCodeは内容を考慮したハッシュ・コードとなります。
System.out.println(Arrays.hashCode(array1)); // 30817
System.out.println(Arrays.hashCode(array2)); // 30817
まとめ
ハッシュ・コード、なんとなくわかりましたでしょうか?
独自クラスをHashMapのキーとして使いたい場合は、このあたりの理解が必要となります。
ぜひ正しく実装して、HashMapのパフォーマンスを最適にしましょう。