iT邦幫忙

2024 iThome 鐵人賽

DAY 10
0
Software Development

深入淺出Java 30天系列 第 10

Day 10: 覆寫equals時的注意事項和規範(下)

  • 分享至 

  • xImage
  •  

昨天說了注意事項,今天就來談談,覆寫equals要遵守的Reflexive、Symmetric、Transitive、Consistent和Non-nullity這五個規則,以及可以依照哪些步驟,一步一步完成覆寫equals

覆寫equals的規則

Reflexive

物件使用equals確認跟自己相不相同時,必須回傳true,也就是x.equals(x) = true。想像一下,如果x.equals(x)是false,用list的contains確認物件有沒有在list之中的時候,會很弔詭的回傳false。

Symmetric

x.equals(y) = true,則y.equals(x) = true,確認兩個物件相不相同時,必須要對稱,互相確認的結果都要一致。如果兩個物件的equals邏輯不一致,有可能會造成x.equals(y)y.equals(x)的結果不同。

以下面的範例來說,如果PersonEmployee這兩個物件的identityNumber(身分證字號)一致的話,表示這兩個物件是相同的,用equals判斷回傳的結果應該要是true,但是Person並沒有在覆寫equals的時候做這個處理,所以person.equals(employee)的結果是false,跟employee.equals(person)的結果不一樣。

import java.util.Objects;

class Person {
    private String name;
    private int age;
    private String identityNumber;

    public Person(String name, int age, String identityNumber) {
        this.name = name;
        this.age = age;
        this.identityNumber = identityNumber;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;

        if (obj instanceof Person) {
            Person person = (Person) obj;
            return Objects.equals(identityNumber, person.identityNumber);
        }
        return false;
    }

    public String getIdentityNumber() {
        return identityNumber;
    }
}

public class Employee {
    private int employeeId;
    private String employeeName;
    private int age;
    private String identityNumber;

    public Employee(int employeeId, String employeeName, int age, String identityNumber) {
        this.employeeId = employeeId;
        this.employeeName = employeeName;
        this.age = age;
        this.identityNumber = identityNumber;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;

        if (obj instanceof Person) {
            Person person = (Person) obj;
            return Objects.equals(identityNumber, person.getIdentityNumber());
        } else if (obj instanceof Employee) {
            Employee employee = (Employee) obj;
            return Objects.equals(employeeId, employee.employeeId);
        }
        return false;
    }

    public static void main(String[] args) {
        Person person = new Person("John", 25, "R123456");
        Employee employee = new Employee(123, "John", 25, "R123456");

        System.out.println(person.equals(employee)); // false
        System.out.println(employee.equals(person)); // true
    }
}

如果對稱性有問題,在使用list的contains時,一樣會有物件明明有在list裡面,卻找不到該物件的問題。

Transitive

若有x, y, z三個物件,x.equals(y) = truey.equals(z) = truex.equals(z) = true也要成立。但有的時候,邏輯上真的無法符合這個特性,也許就要改成別的寫法,舉例來說,有PointColorPoint兩個類別,ColorPoint繼承Point,但是多了顏色的屬性,在覆寫equals的時候,如果ColorPoint的位置跟Point一樣,就認定他們是相等的,但如果是兩個ColorPoint的物件,必須顏色和位置都一樣才會相等,這樣就無法滿足Transitive的特性,因為x.equals(y) = truey.equals(z) = truex.equals(z) = false

import java.util.Objects;

class Point {
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Point)) return false;
        Point point = (Point) obj;
        return x == point.x && y == point.y;
    }
}

class ColorPoint extends Point {
    private String color;

    public ColorPoint(int x, int y, String color) {
        super(x, y);
        this.color = color;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj instanceof ColorPoint) return super.equals(obj) && color.equals(((ColorPoint) obj).color);
        if (!(obj instanceof Point)) return false;
        super.equals(obj);
        return  super.equals(obj);
    }

    public static void main(String[] args) {
        Point point = new Point(1, 2);
        ColorPoint colorPoint1 = new ColorPoint(1, 2, "red");
        ColorPoint colorPoint2 = new ColorPoint(1, 2, "blue");

        // 比較傳遞性
        System.out.println(colorPoint1.equals(point)); // true
        System.out.println(point.equals(colorPoint2)); // true
        System.out.println(colorPoint1.equals(colorPoint2)); //false
    }
}

這個例子在邏輯上合理,在覆寫equals的規則不合理,比較適合的方式就是調整ColorPointPoint的關係,讓他們從繼承變成composition,並且唯有pointcolor都一致的情形下,兩個物件才會相等。

import java.util.Objects;

class Point {
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

class Color {
    private String code;

    public Color(String code) {
        this.code = code;
    }
}

class ColorPoint {
    private Point point;
    private Color color;

    public ColorPoint(int x, int y, Color color) {
        point = new Point(x, y);
        this.color = color;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof ColorPoint)) return false;
        ColorPoint colorPoint = (ColorPoint) obj;
        return point.equals(colorPoint.point) && color.equals(colorPoint.color);
    }

    public static void main(String[] args) {
        Color color1 = new Color("red");
        ColorPoint colorPoint1 = new ColorPoint(1, 2, color1);
        Color color2 = new Color("blue");
        ColorPoint colorPoint2 = new ColorPoint(1, 2, color2);
        Point point = new Point(1, 2);

        System.out.println(colorPoint1.equals(colorPoint2)); //false
        System.out.println(colorPoint2.equals(colorPoint1)); //false
        System.out.println(colorPoint1.equals(point)); // false
    }
}

Consistent

不管觸發幾次x.equals(y)的結果必須一致。這個規則對immutable的物件沒有什麼問題,但如果是mutable的物件,由於物件內的值有可能隨時間和不同使用方式而有不同,所以有可能不是每一次觸發equals()的結果都會一樣,舉例來說,如果java.net.URLequals()確認相同的依據是IP address,就有可能發生一開始是相同後面卻變成不同的問題,除非是固定IP,不然IP有可能會重新分配,這樣就不符合Consistent這個規則。如果真的需要對mutable的物件覆寫equals,判斷的依據就不能是值會一直變動的欄位。

Non-nullity

任何物件用equals檢查跟null有沒有相等時,必須是false。這個可以用檢查物件型別的方式去確認,null不等於任何一個型別,所以會回傳false

@Override
public boolean equals(Object obj) {
  if (!(obj instanceof ColorPoint)) return false;
  ColorPoint colorPoint = (ColorPoint) obj;
  return point.equals(colorPoint.point) && color.equals(colorPoint.color);
}

覆寫equals的步驟

綜合以上的規則和幾個範例,覆寫equals的時候可以參考下面的步驟:

  1. ==檢查傳進來的物件是否等同於目前這個物件,如果是的話,直接回傳true
@Override
public boolean equals(Object obj) {
  if (this == obj) return true;
}
  1. instanceof檢查傳進來的物件的型別,是否等同於指定的型別,如果不是的話,直接回傳false
@Override
public boolean equals(Object obj) {
  if (this == obj) return true;
  if (!(obj instanceof Point)) return false;
}
  1. 將傳進來的物件強制轉換成指定的型別
@Override
public boolean equals(Object obj) {
  if (this == obj) return true;
  if (!(obj instanceof Point)) return false;
  Point point = (Point) obj;
}
  1. 檢查傳進來物件的指定欄位的值跟當前物件的指定欄位的值是否一致,如果一致就回傳true。檢查的方式,對於primitive型別的欄位(ex: int),除了floatdouble之外,可以直接使用==floatdouble可以使用Float.compareDouble.compare,因為floatdouble有NaN和-0.0f的問題,所以交由套件處理會比較好。array的話,在Java 1.5開始,可以使用Arrays.equals判斷。
@Override
public boolean equals(Object obj) {
  if (this == obj) return true;
  if (!(obj instanceof Point)) return false;
  Point point = (Point) obj;
  return x == point.x && y == point.y;
}
  1. 最後,記得撰寫unit test確保equals有符合Symmetric、Transitive和Consistent。

上一篇
Day 9: 覆寫equals時的注意事項和規範(上)
下一篇
Day 11: 覆寫equals時,也要覆寫hashCode(上)
系列文
深入淺出Java 30天30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言