II.Phương thức Equals() trong Java

Trong bài trước, chúng ta đã tìm hiểu về phương thức toString() trong lớp Object. Tiếp theo, trong bài viết này sẽ mô tả một kỹ thuật để ghi đè phương thức equals() trong Java có hiệu quả.

Các pitfalls chung trong phương thức equals()

  • Định nghĩa equals() với signature sai.
  • Thay đổi equals mà không thay đổi hashcode.
  • Định nghĩa equals với các trường có thể thay đổi.
  • Không xác định equals như là một mối quan hệ tương đương.

1.Định nghĩa equals với signature sai

Xét ví dụ: Xây dựng lớp Point với 2 trường x,y kiểu nguyên, xây dựng 2 phương thức getX(), getY() để lấy giá trị của chúng, sau đó override 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;
    }

          // An utterly wrong definition of equals
          public boolean equals(Point other) {
  return (this.getX() == other.getX() && this.getY() == other.getY());
          }

}

Phương thức equals() trên sai điều gì? Bởi thoại nhìn, nó dường như làm việc rất ổn:

Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);

Point q = new Point(2, 3);

System.out.println(p1.equals(p2)); // prints true

System.out.println(p1.equals(q)); // prints false

Tuy nhiên, rắc rối bắt đầu khi bạn put nó vào một Collection, chẳng hạn:

import java.util.HashSet;

HashSet<Point> coll = new HashSet<Point>();
coll.add(p1);

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

Tại sao coll.contrains(p2) lại cho kết quả false, mặc dù p1 đã được bổ xung vào nó, p1và p2 là các đối tượng bằng nhau. Lý do trở nên rõ ràng trong sự tương tác sau đây, nơi mà kiểu của một trong những đối tượng Point so sánh được đánh dấu. Định nghĩa p2a như là một bí danh(alias) của p2, nhưng nó có kiểu Object thay vì Point:

Object p2a = p2;

Bây giờ, chúng ta thử sử dụng phương thức equals():

       System.out.println(p1.equals(p2a)); // prints false

Sở dĩ kết quả sai trong ví dụ trên, bởi vì phương thức equals trong lớp Point không ghi đè phương thức equals trong lớp Object vì đối số của chúng khác kiểu, ta cùng xem lại phương thức equals trong lớp Object:

        public boolean equals(Object other)

Như vậy, phương thức equals trong lớp Point sử dụng Point làm đối số thay vì phải là Object, nên nó không ghi đè phương thức equals, thay vào đó nó chỉ là một dạng nạp chồng (overloading). Cơ chế overloading trong Java chỉ giải quyết bởi những kiểu đối số tĩnh chứ không phải là kiểu run-time. Vì vậy, một khi kiểu đối số tĩnh là Point thì phương thức equals trong lớp Point được gọi, ngược lại nếu là Object thì phương thức equals trong Object được gọi. Điều này giải thích tại sao lời gọi “p1.equals(p2a)” trả về false mặc dù p1 và p2a có cùng các giá trị x và y. Điều này cũng giải thích tại sao phương thức contrains trong HashSet trả về false. Một khi phương thức đó hoạt động trên tập hợp Generic, nó sẽ gọi phương thức equals Generic trong Object thay vì Point.

Phương thức equals trong lớp Point nên được viết lại như sau:

// A better definition, but still not perfect
@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());
    }
    return result;
}

Bây giờ nếu bạn gọi:

       System.out.println(p1.equals(p2a)); // prints true

Và lại gọi:

import java.util.HashSet;

HashSet<Point> coll = new HashSet<Point>();
coll.add(p1);

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

Kết quả vẫn là false, tại sao lại như vậy? Ta sẽ tìm hiểu trong phần tiếp theo.

Bạn thấy bài viết này thế nào?

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