4.Không định nghĩa equals như là một mối quan hệ tương đương

Quy tắc của phương thức equals trong lớp Object quy định rằng phương thức equals phải thực hiện một mối quan hệ tương đương trên các đối tượng non-null.

  • Tính phản xạ(reflexive): với bất kỳ giá trị x non-null, biểu thức x.equals(x) phải trả về true.
  • Tính đối xứng(symmetric): với bất kỳ giá trị non-null x và y, x.equals(y) trả về true nếu và chỉ nếu y.equals(x) trả về true.
  • Tính bắt cầu (transitive): với bất kỳ giá trị non-null x, y và z, nếu x.equals(y) trả về true và y.equals(z) trả về true thì x.equals(z) phải trả về true.
  • Tính kiên định(consitent): với bất kỳ giá trị non-null x và y, nhiều lời gọi x.equals(y) hoặc phải luôn luôn trả về true, hoặc phải luôn luôn trả về false, không cung cấp thông tin sử dụng trong so sánh equals trên các đối tượng được sửa đổi.
  • Với bất kỳ giá trị non-null x, thì x.equals(null) trả về false.

Trong các ví dụ trên, việc override phương thức equals() trong lớp Point có thể coi là đáp ứng được các quy tắc(contact) về equals. Tuy nhiên, mọi thứ sẽ trở nên phức tạp khi lớp Point còn được mở rộng thêm các lớp con của nó. Giả sử lớp ColoredPoint kế thừa từ lớp Point để thêm vào một field màu của kiểu Color, và giả sử Color là một enum.

public enum Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET;
}

ColorPoint ghi đè phương thức equals để nhận trường color mới.

public class ColoredPoint extends Point { // Problem: equals not symmetric

    private final Color color;

    public ColoredPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }

    @Override public boolean equals(Object other) {
        boolean result = false;
        if (other instanceof ColoredPoint) {
            ColoredPoint that = (ColoredPoint) other;
            result = (this.color.equals(that.color) && super.equals(that));
        }
        return result;
    }
}

Đây là những gì nhiều người lập trình có khả năng sẽ viết. Lưu ý rằng trong trường hợp này, lớp ColoredPoint không cần phải ghi đè hashCode. Bởi vì định nghĩa mới của equals trên ColoredPoint là khắt khe hơn so với lớp Point (có nghĩa là nó tương đương cặp ít hơn của các đối tượng), contact cho hashCode vẫn hợp lệ. Nếu hai điểm màu bằng nhau, thì chúng phải có cùng một tọa độ, do đó, mã hashCode của chúng được bảo đảm bằng nhau.

Việc định nghĩa lại phương thức equals trong ColoredPoint hoạt động có vẻ ổn. Tuy nhiên, contact của equals có thể bị phá vỡ khi points và colored points được trộn lẫn. Chẳng hạn:

Point p = new Point(1, 2);

ColoredPoint cp = new ColoredPoint(1, 2, Color.RED);

System.out.println(p.equals(cp)); // prints true

System.out.println(cp.equals(p)); // prints false

Việc so sánh “p với cp” sẽ gọi phương thức equals trong lớp Point, và phương thức này chỉ xét tọa độ (x,y) để so sánh, do đó kết quả bằng true. Ngược lại, việc so sánh “cp với p” sẽ gọi phương thức equals trong lớp ColoredPoint, và phương thức này trả về false bởi vì p không phải là ColoredPoint. Vì vậy các mối quan hệ định nghĩa trong phương thức equals không đối xứng(symetric).

Việc mất tính đối xứng cũng sẽ ảnh hưởng tới collection, xét ví dụ sau:

Set<Point> hashSet1 = new java.util.HashSet<Point>();
hashSet1.add(p);
System.out.println(hashSet1.contains(cp));    // prints false

Set<Point> hashSet2 = new java.util.HashSet<Point>();
hashSet2.add(cp);
System.out.println(hashSet2.contains(p));    // prints true

Làm thế nào bạn có thể thay đổi định nghĩa của phương thức equals để nó trở thành đối xứng? Về cơ bản có hai cách. Bạn hoặc làm cho mối quan hệ tổng quát hơn hoặc nghiêm ngặt hơn. Làm cho nó tổng quát hơn có nghĩa là một cặp hai đối tượng, a và b, chúng được xem là bằng nhau nếu so sánh a với b hay b với a cho kết quả true. Ví dụ như:

public class ColoredPoint extends Point { // Problem: equals not transitive

    private final Color color;

    public ColoredPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }

    @Override public boolean equals(Object other) {
        boolean result = false;
        if (other instanceof ColoredPoint) {
            ColoredPoint that = (ColoredPoint) other;
            result = (this.color.equals(that.color) && super.equals(that));
        }
        else if (other instanceof Point) {
            Point that = (Point) other;
            result = that.equals(this);
        }
        return result;
    }
}

Sau khi định nghĩa lại phương thức equals(), bây giờ bạn gọi:

System.out.println(p.equals(cp)); // prints true

System.out.println(cp.equals(p)); // prints true

Tuy nhiên, contact của equals có thể bị phá vỡ, bởi nó không có tính bắt cầu. Chẳng hạn, bạn tạo 2 đối tượng redP và blueP, sau đó so sánh chúng với p:

ColoredPoint redP = new ColoredPoint(1, 2, Color.RED);
ColoredPoint blueP = new ColoredPoint(1, 2, Color.BLUE);

System.out.println(redP.equals(p)); // prints true

System.out.println(p.equals(blueP)); // prints true

Kết quả OK, tuy nhiên, nếu bạn so sánh redP với blueP sẽ cho kết quả false.

System.out.println(redP.equals(blueP)); // prints false

Như vậy, ta kết luận rằng tính bắt cầu của equals bị vi phạm. Việc tạo ra mối quan hệ equals có tính tổng quát dường như không đạt hiệu quả. Tiếp theo, ta xét nó trong mối nghiêm ngặt để thay thế.

Đầu tiên, ta xây dựng lại phương thức equals trong lớp Point như sau:

// A technically valid, but unsatisfying, equals method
public class Point {

    private final int x;
    private final int y;

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

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    @Override public boolean equals(Object other) {
        boolean result = false;
        if (other instanceof Point) {
            Point that = (Point) other;
            result = (this.getX() == that.getX() && this.getY() == that.getY()
                    && this.getClass().equals(that.getClass()));
        }
        return result;
    }

    @Override public int hashCode() {
        return (41 * (41 + getX()) + getY());
    }
}

Tiếp theo là lớp ColoredPoint

public class ColoredPoint extends Point { // No longer violates symmetry requirement

    private final Color color;

    public ColoredPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }

    @Override public boolean equals(Object other) {
        boolean result = false;
        if (other instanceof ColoredPoint) {
            ColoredPoint that = (ColoredPoint) other;
            result = (this.color.equals(that.color) && super.equals(that));
        }
        return result;
    }
}

Bây giờ, bạn tạo ra các đối tượng:

ColoredPoint redP = new ColoredPoint(1, 2, Color.RED);
ColoredPoint blueP = new ColoredPoint(1, 2, Color.BLUE);

System.out.println(redP.equals(p)); // prints false

System.out.println(p.equals(blueP)); // prints false

System.out.println(redP.equals(blueP)); // prints true

Ở đây ta thấy rằng, nếu 2 thể hiện thuộc cùng một lớp(ở đây là redP và blueP thuộc cùng ColoredPoint) thì cho kết quả true, ngược lại cho false. Định nghĩa lại phương thức equals của 2 lớp Point và ColoredPoint đáp ứng được tính đối xứng và bắt cầu, tuy nhiên color point và point sẽ không bao giờ bằng nhau. Vì vậy mà ta nói rằng đây là một quan hệ có tính nghiêm ngặt.

Giả sử ta định nghĩa một đối tượng Point có tọa độ (1,2) như sau:

Point p=new Point(1,2);
Point pAnon = new Point(1, 1) {
    @Override public int getY() {
        return 2;
    }
};

pAnon có bằng p hay không ? Câu trả lời là không bởi vì lớp java.lang.Object tương ứng giữa pAnon và p khác nhau; p là một thể hiện của Point, pAnon là một subclass giấu tên(anonymous) của Point. Vì vậy mà chúng không bằng nhau.

Xây dựng phương thức canEquals

Vì vậy, có vẻ như chúng ta đang bị mắc kẹt. Có cách nào để định nghĩa lại equals mà vẫn giữ được contact của nó? Trong thực tế, có một cách như vậy, nhưng nó đòi hỏi có một phương thức khác định nghĩa lại cùng kết hợp với equals và hashCode. Ý tưởng cho giải pháp này là ngay sau khi một lớp định nghĩa lại phương thức equals( và hashCode), nó nên được cài đặt tường minh rằng các đối tượng của lớp này không bao giờ bằng các đối tượng của superclass thực hiện một phương thức equals khác. Điều này được thực hiện bằng cách thêm một phương thức canEqual để mỗi lớp có định nghĩa lại phương thức equals.

public class Point {

    private final int x;
    private final int y;

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

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    @Override public boolean equals(Object other) {
        boolean result = false;
        if (other instanceof Point) {
            Point that = (Point) other;
            result = (that.canEqual(this) && this.getX() == that.getX() && this.getY() == that.getY());
        }
        return result;
    }

    @Override public int hashCode() {
        return (41 * (41 + getX()) + getY());
    }

    public boolean canEqual(Object other) {
        return (other instanceof Point);
    }
}

Phương thức equals của lớp Point có thêm vào một yêu cầu, đó là so sánh một đối tượng other có thể bằng với đối tượng được so sánh hay không, điều này được xác định thông qua phương thức canEqual. Việc cài đặt phương thức canEqual trong lớp Point đảm bảo rằng tất cả các thể hiện của lớp Point có thể bằng nhau.

Tiếp theo là cài đặt cho lớp ColoredPoint:

public class ColoredPoint extends Point { // No longer violates symmetry requirement

    private final Color color;

    public ColoredPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }

    @Override public boolean equals(Object other) {
        boolean result = false;
        if (other instanceof ColoredPoint) {
            ColoredPoint that = (ColoredPoint) other;
            result = (that.canEqual(this) && this.color.equals(that.color) && super.equals(that));
        }
        return result;
    }

    @Override public int hashCode() {
        return (41 * super.hashCode() + color.hashCode());
    }

    @Override public boolean canEqual(Object other) {
        return (other instanceof ColoredPoint);
    }
}

Định nghĩa mới này của lớp Point và ColoredPoint cho phép phương thức equals giữ được contact của nó, đó là tính đối xứng và bắt cầu. Việc so sánh một đối tượng Point và ColoredPoint luôn luôn cho kết quả false.

Point p = new Point(1, 2);

ColoredPoint cp = new ColoredPoint(1, 2, Color.INDIGO);

Point pAnon = new Point(1, 1) {
    @Override public int getY() {
        return 2;
    }
};

Set<Point> coll = new java.util.HashSet<Point>();
coll.add(p);

System.out.println(p.equals(cp)); // prints false

System.out.println(cp.equals(p)); // prints false

System.out.println(coll.contains(p)); // prints true

System.out.println(coll.contains(cp)); // prints false

System.out.println(coll.contains(pAnon)); // prints true

Thật vậy, với bất kỳ p và cp, “p.equals(cp)” sẽ trả về false bởi vì “cp.canEqual(p)” luôn luôn trả về false. Ngược lại, cp.equals(p) cũng trả về false bởi vì p không phải là một intance của ColoredPoint.

Các bài viết liên quan:
Kỹ thuật lập trình hướng đối tượng – Phần 4