職場で若者に「なぜ"=="じゃなくで"equals"を使うんですか?」と聞かれ(この時はStringでの話)、何気なく「オブジェクトの比較はequalsメソッド内で値の比較をするからだよ」と答えました。しかし、正直なところザックリとしか理解していなかったので、初心に戻り少し調べてみました。初心に戻っているので、当たり前の話も偉そうに話します。そして間違いがあればご指摘お願いします。
プリミティブ型と参照型
まず、Javaには大きく分けてプリミティブ型と参照型があります。プリミティブ型は主にデータを格納するだけで、メソッドを持ちません。なので基本データ型とも呼ばれます。プリミティブ型の値を作ると直接メモリに値が書き込まれます。プリミティブ型には以下の8つがあります。
- byte
- short
- int
- long
- char
- float
- double
- boolean
小文字で始まるのがプリミティブ型だと思えば良いですね。この他の大文字で始まる型はString型も含め全て参照型となります。参照型はメモリに参照しているオブジェクトのアドレスが書き込まれます。ではこれらの比較をしてみたいと思います。
==演算子
実際にプリミティブ型と参照型の比較を、intとIntegerを使って"==演算子"比較してみます。
// プリミティブ型の比較
int intA = 1;
int intB = 1;
System.out.println(intA == intB); // ture
// 参照型の比較
Integer integerA = new Integer(1);
Integer integerB = new Integer(1);
System.out.println(integerA == integerB); // false
プリミティブ型では値の比較がされているようです。参照型では値の比較はされていないようで、integerAとintegerBは同じ1のはずなのにfalseと処理されてしまいます。これはIntegerの内部で持つ値を比較しているのではなく、integerAとintegerBの参照先のインスタンスを比較し、同じインスタンスかを判定しているからなのです。つまり参照型に"==演算子"比較をすると、変数が保持する参照アドレスの比較を行い同一のインスタンスかどうかを判定するのです。このように変数どうしの値(参照型の場合参照アドレス)が同じことを同値と言います。
equalsメソッド
"==演算子"の動作仕様は理解しました。でも内部で持つ値を比較したい...ここで"equalsメソッド"の出番です。
// 参照型の比較
Integer integerA = new Integer(1);
Integer integerB = new Integer(1);
System.out.println(integerA.equals(integerB)); // true
"equalsメソッド"を使用すると、どうやら値の比較を行っているように見えます。では実際メソッド内ではどんな処理をしているのでしょう。
public final class Integer extends Number implements Comparable<Integer> {
/* 中略 */
private final int value;
public int intValue() {
return value;
}
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
/* 中略 */
}
メソッド内で自分の持つ値(int)と、引数のオブジェクト内の値(int)を"==演算子"で比較しています。つまりメソッド内で本当に比較したい値(プリミティブ型)を"==演算子"で比較しているのです。このようにインスタンスは違うがインスタンスの価値は同じことを等価と言います。
ちなみにString型の場合はどうなっているのか見てみました。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/* 中略 */
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
/* 中略 */
}
String型でも内部に持つ値(char[])を一文字ずつ比較していますね。
ちなみに次の書き方をすると、結果が少し異なります。
String stringA = "文字列";
String stringB = "文字列";
System.out.println(stringA == stringB); // true
これは話が違うように見えますが、実はこの書き方をするとstringAもstringBも同じ"文字列"なので、コンパイル時に同一のインスタンスを参照するように書き換えられるからです。
おまけ
値の比較をするなら、java.util.Objects#equalsもあります。
String stringA = new String("文字列");
String stringB = new String("文字列");
String stringC = null;
System.out.println(java.util.Objects.equals(stringA, stringB)); // true
System.out.println(java.util.Objects.equals(stringA, stringC)); // false
オブジェクト自身の"equalsメソッド"はしばしばNullPointerExceptionの原因となるので、こちらのメソッドを使用するのも良いかもしれません。
最後に
Javaではデータ値をプリミティブ型が保持していて、それを参照型が参照しているようです。生のデータ(プリミティブ型)を比較する場合は"==演算子"、参照型経由で値を比較をしたい場合は"equalsメソッド"を使用し内部で"==演算子"比較させます。ザックリだなんて恥ずかしいくらい同値と等価は基本的なことでした...これからも日々精進したいと思います。