演習: 等価性
各問題ごとに .java ファイルを編集し、Main.java で動作確認します。
3-A. == と equals の出力予測
Section titled “3-A. == と equals の出力予測”演習 2-C の Person(equals 未オーバーライド)を使って、次のコードの出力を予測してください。予測したあとで実際にコンパイル・実行し、結果を確認します。
Person p1 = new Person("田中", 25);Person p2 = new Person("田中", 25);Person p3 = p1;
System.out.println(p1 == p2);System.out.println(p1 == p3);System.out.println(p1.equals(p2));ヒント
本文 参照と値の等価性 を参照してください。== はオブジェクトに対しては参照の比較になります。new Person(...) を 2 回実行すると別々のオブジェクトを指すため、p1 == p2 は false です。p3 = p1 は参照のコピーなので p1 == p3 は true です。equals はオーバーライドしない限り == と同じ挙動(参照の比較)なので、p1.equals(p2) も false になります。
3-B. Point の equals + hashCode
Section titled “3-B. Point の equals + hashCode”演習 2-B で作った Point に equals と hashCode をオーバーライドしてください。x と y がどちらも一致するときに等しいと判定します。
Point a = new Point(3, 5);Point b = new Point(3, 5);Point c = new Point(3, 6);
System.out.println(a.equals(b));System.out.println(a.equals(c));System.out.println(a.hashCode() == b.hashCode());出力例:
truefalsetrueヒント
本文 equals のオーバーライド と hashCode のオーバーライド のテンプレートを流用します。int 同士の比較は == を使います。hashCode は Objects.hash(x, y) で書けるので、import java.util.Objects; を追加します。
equals の中では、引数 other が同じ Point クラスなので other.x のように private フィールドへ直接アクセスできます。
3-C. Person の equals + hashCode
Section titled “3-C. Person の equals + hashCode”演習 2-C で作った Person に equals と hashCode をオーバーライドしてください。name と age の両方が一致するときに等しいと判定します。
Person p1 = new Person("田中", 25);Person p2 = new Person("田中", 25);Person p3 = new Person("田中", 30);Person p4 = new Person("鈴木", 25);
System.out.println(p1.equals(p2));System.out.println(p1.equals(p3));System.out.println(p1.equals(p4));出力例:
truefalsefalseヒント
演習 3-B で書いた Point の equals / hashCode と同じ構造です。違いは比較対象のフィールドが 2 つになることと、String を比較する点です。String の比較は Objects.equals(name, other.name) を使うと null でも安全に比較できます。int の比較は == を使います。
3-D. Point の toString
Section titled “3-D. Point の toString”3-B で完成させた Point に toString をオーバーライドしてください。Point{x=3, y=5} の形式で出力されるようにします。
Point p = new Point(3, 5);System.out.println(p);出力例:
Point{x=3, y=5}ヒント
本文 toString のオーバーライド と同じパターンです。文字列連結(+)で Point{x=、x の値、, y=、y の値、} をつなげます。System.out.println(p) のように、オブジェクトを直接渡すと toString が自動で呼び出されます。
3-E. User の重複登録チェック
Section titled “3-E. User の重複登録チェック”ユーザー登録のデータクラス User を User.java に定義してください。次のとおり定義します。
- フィールド:
email(String)、name(String)、age(int)の 3 つ。すべてprivate final - コンストラクターで以下を検査する
emailが空文字("")のときはメールアドレスは必須ですを出力してunknown@example.comを代入ageが0未満または150より大きいときは不正な年齢: 値を出力して0を代入
- getter は 3 つすべて用意する
equalsとhashCodeはemailのみ で判定する(同じメールアドレスは同一人物とみなす)toStringはUser{email=..., name=..., age=...}の形式
Main.java で登録済みリストに同じメールアドレスのユーザーを contains で弾く処理を書きます。
import java.util.ArrayList;import java.util.List;
List<User> registered = new ArrayList<>();registered.add(new User("tanaka@example.com", "田中", 25));registered.add(new User("suzuki@example.com", "鈴木", 30));
User newUser = new User("tanaka@example.com", "田中太郎", 26);if (registered.contains(newUser)) { System.out.println("登録済み: " + newUser.getEmail());} else { registered.add(newUser); System.out.println("登録しました: " + newUser.getEmail());}出力例:
登録済み: tanaka@example.comヒント
equals の判定軸を email だけにすると、name や age が違っても同じメールアドレスのインスタンスは「等しい」と判定されます。List.contains は内部で各要素に対して equals を呼ぶので、registered.contains(newUser) が true になります。
hashCode は equals で使ったフィールドと同じものを Objects.hash に渡します(Objects.hash(email))。
実務では email のような自然なキーではなく、データベースが採番する id のような識別子で判定することが一般的です。本演習では簡略化のため email を使います。
3-F. Student の参照比較
Section titled “3-F. Student の参照比較”演習 2-F で作った Student に対して、equals をオーバーライドしないまま List.contains を試して、参照比較の挙動を確認します。
import java.util.ArrayList;import java.util.List;
List<Integer> scores = new ArrayList<>();scores.add(70);scores.add(85);
Student s1 = new Student("田中", scores);Student s2 = new Student("田中", scores);
List<Student> students = new ArrayList<>();students.add(s1);
System.out.println(students.contains(s1));System.out.println(students.contains(s2));出力例:
truefalses1 と s2 は同じ name と同じ scores を持っていますが、contains(s2) は false を返します。Student に equals がオーバーライドされていないため、contains の内部で行われる比較が参照比較になります。
ヒント
Student に equals をオーバーライドすれば、name と scores が一致するインスタンスを等しいと判定できます。しかし Student の場合、判定軸は自明に決まりません。
- 同姓同名の学生は別人ですが、
nameだけで判定すると同一とみなされる scoresまで含めて判定すると、点数が偶然一致する別人を同一とみなす- 学籍番号のような識別子があれば一意に決められますが、現状の
Studentにはない
判定軸がはっきり決まらない場合、equals をオーバーライドせずデフォルトの参照比較に任せる選択もあります。本演習では、equals をオーバーライドしないことで挙動がどう変わるかを確認します。
3-G. Range の定義
Section titled “3-G. Range の定義”数値の範囲を表すデータクラス Range を Range.java に定義してください。次のとおり定義します。
- フィールド:
start(int)、end(int)。両方ともprivate final - コンストラクターで
startがendより大きい場合は不正な範囲: start=値, end=値を出力してstartとendを入れ替えて代入する - getter は 2 つ用意する
contains(int value):valueがstart以上end以下ならtrueoverlaps(Range other): 2 つの範囲が一部でも重なればtrueequalsとhashCodeはstartとendの両方で判定するtoStringはRange{start=..., end=...}の形式
Range r1 = new Range(1, 10);Range r2 = new Range(5, 15);Range r3 = new Range(20, 30);Range r4 = new Range(8, 3);
System.out.println(r1.contains(5));System.out.println(r1.contains(15));System.out.println(r1.overlaps(r2));System.out.println(r1.overlaps(r3));System.out.println(r4);System.out.println(r1.equals(new Range(1, 10)));出力例:
不正な範囲: start=8, end=3truefalsetruefalseRange{start=3, end=8}trueヒント
overlaps の判定は、2 つの範囲が重なる条件を考えます。範囲 [a, b] と [c, d] が重なるのは、a <= d かつ c <= b のときです(一方の開始が他方の終了以下、かつ逆も成り立つ)。
equals は start と end の両方で判定します。hashCode も同じフィールドで計算するため、Objects.hash(start, end) のように 2 つの値を渡します。
コンストラクターでの入れ替えは、start > end のときに this.start = end; this.end = start; のように、引数の反対側を代入することで実現できます。本来は不正な引数を呼び出し元に伝えて修正させるのが望ましく、実務では例外を投げて拒否する書き方が一般的です(例外の扱いは 例外処理 で扱います)。