Hướng dẫn Kỹ Thuật Lập Trình Hướng Đối Tượng – Phần 1,2,3,4,5

Kỹ Thuật Lập Trình Hướng Đối Tượng – Phần 1

Mặc dù Object là một lớp cụ thể, nó được thiết kế chủ yếu để các lớp các kế thừa. Tất cả các phương thức non-final của nó (equals(), hashCode(), toString(), clone(), và finalize()) đều có các quy tắc chung bởi vì chúng được thiết kế để được overrride. Tất cả các lớp kế thừa từ Object mà overrride các phương thức này đều phải tuân theo các quy tắc chung của chúng.

I.Override phương thức toString()

1.Đặt vấn đề:

Bạn có một định dạng mặc định hữu ích.

2.Giải pháp

Override phương thức toString() kế thừa từ java.lang.Object.

3.Lý giải

Khi bạn gán một đối tượng vào phương thức System.out.println() hoặc bất kỳ phương thức nào tương đương, Java sẽ tự động gọi phương thức toString(). Bởi vì tất cả các lớp trong Java đều có một lớp cha cao nhất là java.lang.Object, và cài đặt mặc định của phương thức toString() trong lớp java.lang.Object là: tên lớp, ký tự @ và giá trị hascode của đối tượng.

Ví dụ

/* Demonstrate toString( ) without an override */


public class ToStringWithout {

int x, y;

/** Simple constructor */

public ToStringWithout(int anX, int aY) {

x = anX; y = aY;

}

/** Main just creates and prints an object */

public static void main(String[] args) {

System.out.println(new ToStringWithout(42, 86));

}

}

Đoạn code trên cho ra kết quả: ToStringWithout@990c747b

Để in tên lớp và các giá trị mong muốn, bạn có thể override lại phương thức toString() như ví dụ dưới đây:

/* Demonstrate toString( ) with an override */
 public class ToStringWith {
     int x, y;
     /** Simple constructor */
     public ToStringWith(int anX, int aY) {
         x = anX; y = aY;
     }
     /** Override toString */
     public String toString( ) {
         return "ToStringWith[" + x + "," + y + "]";
     }
     /** Main just creates and prints an object */
     public static void main(String[] args) {
         System.out.println(new ToStringWith(42, 86));
     }
 }

Kết quả: ToStringWith[42,86].

Kỹ Thuật Lập Trình Hướng Đối Tượng – Phần 2

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.

Kỹ Thuật Lập Trình Hướng Đối Tượng – Phần 3

2.Thay đổi equals mà không thay đổi hashcode

Trong ví dụ trên, tại sao sau khi viết lại phương thức equals, sử dụng phương thức contrains của HashSet vẫn cho kết quả false. Đó là bởi vì bạn chỉ ghi đè lại phương thức equals mà không ghi đè phương thức hashCode.

Lưu ý rằng, các lớp trong collection thường lưu địa chỉ của các phần tử của nó bằng các mã băm(hash code) để giúp cho việc tìm kiếm hay sắp xếp nhanh hơn. Collection trong ví dụ trên là HashSet. Điều này có nghĩa rằng các thành phần của collection được đặt trong các đoạn băm(hash buckets) để xác định các giá trị hash code của chúng. Phương thức contains đầu tiên xác định một hash bucket để tìm kiếm và sau đó so sánh với các phần tử đã cho với tất cả các phần tử trong bucket đó. Trong ví dụ trên, bạn đã ghi đè phương thức equals nhưng không ghi đè phương thức hashCode, cho nên hashCode vẫn chứa các giá trị được lưu trong lớp cha của nó là Object. Điều này dẫn đến các hash code của p1 và p2 khác nhau mặc dù các trường của nó có giá trị giống nhau. Các hash code khác nhau có nghĩa các hash bucket khác nhau trong tập hợp, điều này có nghĩa là p1 và p2 nằm trong các bucket khác nhau và cho kết quả false.

Như vậy, vấn đề ở đây là việc thực hiện cuối cùng của Point vi pham quy tắc(contact) hashCode được định nghĩa trong Object:

Nếu hai đối tượng được gọi là bằng nhau theo phương thức equals, sau đó gọi phương thức hashCode trên mỗi đối tượng đó đều phải trả về kết quả cùng một số nguyên.

Như vậy, ta xây dựng lại lớp Point như sau:

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());
        }
        return result;
    }

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

Trong ví dụ trên, giá trị trả về trong phương thức equals được tính bằng

41*(41 + getX() + getY());

Bây giờ, bạn thử gọi lại phương thức contains xem nhé:

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

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

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

Kỹ Thuật Lập Trình Hướng Đối Tượng – Phần 4

3.Định nghĩa phương thức equals với các trường(field) có thể sữa đổi

Ta xây dựng lại lớp Point như sau:

public class Point { 

    private int x;
    private int y;

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

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public void setX(int x) { // Problematic
        this.x = x;
    }
 
    public void setY(int y) {
        this.y = 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());
        }
        return result;
    }

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

Lớp Point được xây dựng lại có sự khác nhau so với ban đầu: bỏ từ khóa final khi khai báo 2 trường x và y, các phương thức equals và hashCode được định nghĩa theo điều khoản các trường của chúng có thể thay đổi giá trị, vì vậy kết quả của chúng thay đổi khi thay đổi giá trị của các trường. Điều này có thể gây ra những hiệu ứng kỳ lạ một khi bạn đặt points trong collection:

Point p = new Point(1, 2);

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

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

Nhưng bây giờ, bạn thay đổi giá trị một trường trong p:

p.setX(p.getX() + 1);

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

Điều này trông rất kỳ lạ, p đã ở đâu? Kết quả lạ hơn nếu bạn kiểm tra xem Iterator của tập chứa p:

Iterator<Point> it = coll.iterator();
boolean containedP = false;
while (it.hasNext()) {
    Point nextP = it.next();
    if (nextP.equals(p)) {
        containedP = true;
        break;
    }
}

System.out.println(containedP); // prints true

Điều gì xảy ra, tất nhiên, là sau khi thay đổi trường x, điểm p có hash bucket nằm ngoài tập coll. Đó là, bản gốc hash bucket của coll không còn tương ứng với giá trị mới hash bucket của p.
Bài học rút ra từ ví dụ này là khi phương thức equals và hashCode phụ thuộc vào trạng thái có thể thay đổi, nó có thể gây ra nhiều vấn đề cho người dùng. Khi bạn put các đối tượng này vào collecition, bạn cần lưu ý là không sửa đổi các trạng thái phụ thuộc này.

Tóm lại, nếu bạn put một đối tượng tạo ra từ lớp mà bạn xây dựng vào collection, bạn không nên override 2 phương thức equals và hashCode, để lớp mà bạn xây dựng kế thừa 2 phương thức equals và hashCode mặc định từ lớp Object. Trong trường hợp cần so sánh các giá trị trong lớp của bạn, thì bạn nên đặt một tên khác equals, chẳng hạn equalsContent. Ngược lại, bạn phải sử dụng các giá trị không thể sữa đổi trong phương thức equals và hashCode.

Kỹ Thuật Lập Trình Hướng Đối Tượng – Phần 5

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.

Post a Comment

Mới hơn Cũ hơn