等値比較、等価比較、とは何か?
2つの値、または2つのオブジェクトが等しいか判定することを同一性判定と言います。
同一性判定の判定方法は2つの考え方があります。
1つは等値比較です。
等値比較は、両者の値が同じだとすれば同一と見なす判定方法です。
もう1つが等価比較です。
等価比較は、両者が意味的に同じであれば同一と見なす判定方法です。
「意味的に」が意味することは、オブジェクトによって異なります。
そのため、equalsメソッドをオーバーライドして、equalsメソッドの中で何を持って同一と見なすか、つまりtrueとするかをロジックで表現する必要があります。
基本データ型(プリミティブ型)は等値比較を使う
基本データ型を比較するときは迷う必要なく、比較演算子==で比較します。
Javaの基本データ型にはint,short,long,double,float,char,boolean,byteがあります。
それらはメソッドを持ちませんので、同一性の判定は==による等値比較しかないからです。
比較演算子== は値が同じか判定するものです。
基本データ型の変数には値そのものが格納されているため、==で比較できます。
int num1 = 123;
int num2 = 123;
System.out.println(num1 == num2); // true
参照型(クラス型)は等価比較を使う
参照型(クラス型)のオブジェクト同士が同じか調べたいとき、あなたは何を持って同じオブジェクトと判定したいでしょうか?
その質問がポイントです。
比較演算子で同一性を判定して良いケース
参照型の変数には、オブジェクトそのものではなく、オブジェクトのメモリ上の位置情報が格納されています。
そのため、参照型(クラス型)を比較演算子==で比較すると、オブジェクトのメモリ上の位置情報が同じかどうかを判定します。(等値比較)
言い換えると、メモリ上のオブジェクトが同じものかを判定しているのです。
もしあなたがそれで良ければ、参照型を比較演算子==で判定しても良いです。
オブジェクトの意味的な内容で同一性を判定するには?
次のようなStudentクラスを考えてみてください。
class Student {
int id; // 学生番号
String name; // 名前
}
学生番号と名前というフィールドがあります。
下図の時、あなたは2つの学生オブジェクトを同一としますか?
idが同じなので、同一生徒を指し示していると考えられます。
しかし、nameが違うのでまったく別人の可能性もあります。
等価比較をするときは、「等価」の基準は自分で決める必要があるということです。
どのように決めるかというと、Object#equalsメソッドをオーバーライドします。
具体的には、次のようなコードになります。
// サンプル Student1
class Student {
int id; // 学生番号
String name; // 名前
@Override
public boolean equals(Object object) {
Student target = (Student) object;
if (this.id == object.id) {
return true;
}
return false;
}
}
上のコードは、IDが同じなら等価とする。と自分で決めた時の例です。
もし、IDと名前が同じなら等価とする場合は、下のコードのようになります。
// サンプル Student2
class Student {
int id; // 学生番号
String name; // 名前
@Override
public boolean equals(Object object) {
Student target = (Student) object;
if (this.id == target.id && this.name.equals(target.name)) {
return true;
}
return false;
}
}
ここでお伝えしたいことは、等価判定とは、自分の考えた基準で意味的に等しいかを判定する方法であるということです。
上のようなコードを書いた上で、比較するときは、次のように比較します。
// サンプルコード1
Student student1 = new Student();
student1.id = 100;
student1.name = "滝沢";
Student student2 = new Student();
student2.id = 100;
student2.name = "翼";
System.out.println(student1 == student2); // false
System.out.println(student1.equals(student2)); // サンプル Student1 なら true。サンプル Student2 なら false。
現場ではどうか?
現場では、実はequalsメソッドをオーバーライドして等価比較しているのを見たことはありません。
その理由の推測は、その方法を知らない人が多いということと、そうしなくても等価比較ができるからです。
上で示したサンプルコード1は次のようにすることでequalsメソッドをオーバーライドしなくても同じ等価比較が出来ます。
// サンプルコード1
Student student1 = new Student();
student1.id = 100;
student1.name = "滝沢";
Student student2 = new Student();
student2.id = 100;
student2.name = "翼";
if (student1.id == student2.id && student1.name.equals(student2.name)) {
System.out.println(true);
} else {
System.out.println(false);
}
フィールドは最終的には基本データ型やString型の比較になるので、個別のフィールドを取り出して比較することで、等価比較が実現できます。
もちろん、理想はequalsメソッドをオーバーライドすることです。
同じ比較ロジックを1箇所に書く必要がなくなるからです。
上のサンプルコード群は不完全で、本来であればstudent2やstudent1.nameのNullチェックが必要になり、もう少し煩雑なコードになります。
その煩雑なコードを比較したい箇所ごとに同じコードを書くのは可読性が下がりますし、DRY原則に反しているからです。
紛らわしい、String型の比較
String型の同一性判定は誤った解説をしているサイトが多いので注意してください。
String型は参照型なので、現場ではequalsメソッドで比較するのが一般的です。
しかし、比較演算子 == でも意図した比較が行えます。
String str1 = "hoge";
String str2 = "hoge";
System.out.println(str1 == str2); // true ここが紛らわしい
System.out.println(str1.equals(num2)); // true
str1 == str2 がtrueになる理由が大事です。
通常の参照型では、比較演算子==による比較は参照先オブジェクトが一致するかどうかの判定のため、falseになるからです。
つまり、str1 == str2 がtrueになる理由は、str1とstr2は実は同じStringオブジェクトを指しているのです。(下図の状態)
これは、Javaの仕組みがString型を特別扱いする仕様になっているからです。
String型を「String strX = "xxx"」のように、一般的に利用される形式で書いたとき、"xxx"の部分が既に存在するStringオブジェクト同じなら、そのオブジェクトと同じ参照先を参照するような仕組みになっているのです。
str1 == str2 の結果がfalseとなるには、Stringオブジェクトの生成方法を変える必要があります。
String str1 = new String("hoge");
String str2 = new String("hoge");
System.out.println(str1 == str2); // false
System.out.println(str1.equals(num2)); // true
このように、new String("xxx")と書くと、同じ文字列でも別のオブジェクトとして扱われます。(下図の状態です)
そのため、比較演算子==による等値比較は、参照先オブジェクトが異なるため、falseになります。
ただし、わざわざ new String("xxx")と書く必要はまったくありません。
現場でも見たことありませんし、見たら指摘が入るでしょう。
そのため、String型同士の比較は比較演算子==で文字列の比較になりますが、一般的には他の参照型同様にequalsメソッドを使います。
equalsメソッドで比較するようにしましょう。
現場でも通用する使い分け
現場でも通用する比較演算子==とequalsメソッドの使い分けはとても簡単です。
詳しい仕組みを知っておく必要はありません。
基本型の比較は、比較演算子==。
参照型(クラス型)の比較は、equalsメソッド。
これだけです。