Skip to content
Playground

演習: 等価性

各問題ごとに .java ファイルを編集し、Main.java で動作確認します。

演習 2-CPersonequals 未オーバーライド)を使って、次のコードの出力を予測してください。予測したあとで実際にコンパイル・実行し、結果を確認します。

Main.java
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 == p2false です。p3 = p1 は参照のコピーなので p1 == p3true です。equals はオーバーライドしない限り == と同じ挙動(参照の比較)なので、p1.equals(p2)false になります。

演習 2-B で作った PointequalshashCode をオーバーライドしてください。xy がどちらも一致するときに等しいと判定します。

Main.java
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());

出力例:

true
false
true
ヒント

本文 equals のオーバーライドhashCode のオーバーライド のテンプレートを流用します。int 同士の比較は == を使います。hashCodeObjects.hash(x, y) で書けるので、import java.util.Objects; を追加します。

equals の中では、引数 other が同じ Point クラスなので other.x のように private フィールドへ直接アクセスできます。

演習 2-C で作った PersonequalshashCode をオーバーライドしてください。nameage の両方が一致するときに等しいと判定します。

Main.java
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));

出力例:

true
false
false
ヒント

演習 3-B で書いた Pointequals / hashCode と同じ構造です。違いは比較対象のフィールドが 2 つになることと、String を比較する点です。String の比較は Objects.equals(name, other.name) を使うと null でも安全に比較できます。int の比較は == を使います。

3-B で完成させた PointtoString をオーバーライドしてください。Point{x=3, y=5} の形式で出力されるようにします。

Main.java
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 が自動で呼び出されます。

ユーザー登録のデータクラス UserUser.java に定義してください。次のとおり定義します。

  • フィールド: email(String)、name(String)、age(int)の 3 つ。すべて private final
  • コンストラクターで以下を検査する
    • email が空文字("")のときは メールアドレスは必須です を出力して unknown@example.com を代入
    • age0 未満または 150 より大きいときは 不正な年齢: 値 を出力して 0 を代入
  • getter は 3 つすべて用意する
  • equalshashCodeemail のみ で判定する(同じメールアドレスは同一人物とみなす)
  • toStringUser{email=..., name=..., age=...} の形式

Main.java で登録済みリストに同じメールアドレスのユーザーを contains で弾く処理を書きます。

Main.java
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 だけにすると、nameage が違っても同じメールアドレスのインスタンスは「等しい」と判定されます。List.contains は内部で各要素に対して equals を呼ぶので、registered.contains(newUser)true になります。

hashCodeequals で使ったフィールドと同じものを Objects.hash に渡します(Objects.hash(email))。

実務では email のような自然なキーではなく、データベースが採番する id のような識別子で判定することが一般的です。本演習では簡略化のため email を使います。

演習 2-F で作った Student に対して、equals をオーバーライドしないまま List.contains を試して、参照比較の挙動を確認します。

Main.java
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));

出力例:

true
false

s1s2 は同じ name と同じ scores を持っていますが、contains(s2)false を返します。Studentequals がオーバーライドされていないため、contains の内部で行われる比較が参照比較になります。

ヒント

Studentequals をオーバーライドすれば、namescores が一致するインスタンスを等しいと判定できます。しかし Student の場合、判定軸は自明に決まりません。

  • 同姓同名の学生は別人ですが、name だけで判定すると同一とみなされる
  • scores まで含めて判定すると、点数が偶然一致する別人を同一とみなす
  • 学籍番号のような識別子があれば一意に決められますが、現状の Student にはない

判定軸がはっきり決まらない場合、equals をオーバーライドせずデフォルトの参照比較に任せる選択もあります。本演習では、equals をオーバーライドしないことで挙動がどう変わるかを確認します。

数値の範囲を表すデータクラス RangeRange.java に定義してください。次のとおり定義します。

  • フィールド: start(int)、end(int)。両方とも private final
  • コンストラクターで startend より大きい場合は 不正な範囲: start=値, end=値 を出力して startend を入れ替えて代入する
  • getter は 2 つ用意する
  • contains(int value): valuestart 以上 end 以下なら true
  • overlaps(Range other): 2 つの範囲が一部でも重なれば true
  • equalshashCodestartend の両方で判定する
  • toStringRange{start=..., end=...} の形式
Main.java
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=3
true
false
true
false
Range{start=3, end=8}
true
ヒント

overlaps の判定は、2 つの範囲が重なる条件を考えます。範囲 [a, b][c, d] が重なるのは、a <= d かつ c <= b のときです(一方の開始が他方の終了以下、かつ逆も成り立つ)。

equalsstartend の両方で判定します。hashCode も同じフィールドで計算するため、Objects.hash(start, end) のように 2 つの値を渡します。

コンストラクターでの入れ替えは、start > end のときに this.start = end; this.end = start; のように、引数の反対側を代入することで実現できます。本来は不正な引数を呼び出し元に伝えて修正させるのが望ましく、実務では例外を投げて拒否する書き方が一般的です(例外の扱いは 例外処理 で扱います)。