0
0
Lập trình
Admin Team
Admin Teamtechmely

Chương 3 - Phương Thức Chung Cho Tất Cả Đối Tượng

Đăng vào 3 tuần trước

• 11 phút đọc

Giới thiệu

Chào mừng bạn quay lại với chuỗi blog "Java Hiệu Quả"! Trong chương này, chúng ta sẽ đi sâu vào một phần quan trọng của lập trình hướng đối tượng: hiểu và triển khai đúng các phương thức kế thừa từ java.lang.Object. Mặc dù những phương thức này có vẻ đơn giản, nhưng việc triển khai sai có thể dẫn đến những lỗi tinh vi và khó phát hiện. Như thường lệ, chúng ta sẽ xem cách các mẫu thiết kế hiện đại của Kotlin có thể giúp chúng ta tránh những cạm bẫy này ngay từ đầu.

Chương 3: Phương Thức Chung Cho Tất Cả Đối Tượng

  • Mục 10: Ghi đè equals() khi bạn cần so sánh bằng giá trị
  • Mục 11: Luôn ghi đè hashCode() khi bạn ghi đè equals()
  • Mục 12: Luôn ghi đè toString() để cải thiện quá trình gỡ lỗi
  • Mục 13: Tránh clone() và sử dụng các constructor sao chép hoặc phương thức copy() trong Kotlin
  • Mục 14: Triển khai Comparable để xác định thứ tự tự nhiên

Mục 10: Ghi đè equals() khi bạn cần so sánh bằng giá trị

Tóm tắt

Phương thức equals() được sử dụng để xác định xem hai đối tượng có bằng nhau về mặt logic hay không. Triển khai mặc định trong Object chỉ kiểm tra sự bằng nhau theo tham chiếu (this == obj). Bạn phải ghi đè nó khi bạn muốn hai đối tượng khác nhau được coi là bằng nhau nếu dữ liệu của chúng giống nhau. Tuy nhiên, một triển khai đúng phải tuân theo một hợp đồng nghiêm ngặt.

Hợp đồng equals()

  • Phản xạ: x.equals(x) phải đúng.
  • Đối xứng: Nếu x.equals(y) đúng, thì y.equals(x) cũng phải đúng.
  • Truyền dẫn: Nếu x.equals(y) đúng và y.equals(z) đúng, thì x.equals(z) phải đúng.
  • Nhất quán: Nhiều cuộc gọi tới x.equals(y) trả về cùng một kết quả, giả sử các đối tượng không bị thay đổi.
  • Không null: x.equals(null) phải là sai.

Java

Triển khai thủ công trong Java là rất dài dòng và yêu cầu sự chú ý cẩn thận đến từng chi tiết.

java Copy
import java.util.Objects;

public final class User {
    private final String name;
    private final String email;
    private final String phoneNumber;

    public User(String name, String email, String phoneNumber) {
        this.name = name;
        this.email = email;
        this.phoneNumber = phoneNumber;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(name, user.name) &&
               Objects.equals(email, user.email) &&
               Objects.equals(phoneNumber, user.phoneNumber);
    }
}

Kotlin (Data Class)

Chính lúc này, data class của Kotlin tỏa sáng. Một data class tự động tạo các phương thức equals(), hashCode()toString() dựa trên các thuộc tính của constructor chính. Đây là cách idiomatic để xử lý sự bằng nhau dựa trên giá trị trong Kotlin.

kotlin Copy
data class User(val name: String, val email: String, val phoneNumber: String)

Chỉ với một dòng mã, chúng ta đã có một triển khai equals() mạnh mẽ và chính xác.

Kotlin (Non-Data Class)

Đối với một lớp thông thường, bạn phải ghi đè equals(), giống như trong Java. Nó có phần dài dòng hơn, nhưng mang lại cho bạn nhiều quyền kiểm soát hơn.

kotlin Copy
class User(val name: String, val email: String, val phoneNumber: String) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is User) return false
        return name == other.name &&
               email == other.email &&
               phoneNumber == other.phoneNumber
    }
}

Tóm tắt: Chỉ ghi đè equals() khi bạn cần sự bằng nhau về giá trị. Đối với các trường hợp đơn giản trong Kotlin, data class cung cấp giải pháp hoàn hảo, nhưng cũng quan trọng để biết cách thực hiện thủ công cho các lớp phức tạp hơn.

Mục 11: Luôn Ghi Đè hashCode() Khi Bạn Ghi Đè equals()

Tóm tắt

Phương thức hashCode() phải luôn được ghi đè nếu equals() được ghi đè. Hợp đồng trong Object quy định rằng hai đối tượng bằng nhau theo equals() phải có cùng một mã hash. Nếu không, sẽ phá vỡ các bộ sưu tập dựa trên hash như HashMapHashSet.

Java

Một triển khai tốt cho hashCode() trong Java là sự kết hợp giữa một số nguyên tố và các mã hash của các trường của đối tượng. Công thức là: kết quả = 31 * kết quả + c. Phương thức tiện ích Objects.hash là cách thuận tiện để triển khai điều này trong Java hiện đại.

java Copy
import java.util.Objects;

public final class User {
    // ...
    @Override
    public int hashCode() {
        return Objects.hash(name, email, phoneNumber);
    }
}

Kotlin (Data Class)

Một lần nữa, data class đơn giản hóa hoàn toàn điều này. Phương thức hashCode() được tự động tạo ra và nhất quán với equals(), ngăn ngừa một nguồn lỗi phổ biến.

kotlin Copy
// Phương thức hashCode() được tự động tạo ra và chính xác.
data class User(val name: String, val email: String, val phoneNumber: String)

Kotlin (Non-Data Class)

Đối với một lớp thông thường, bạn cũng sẽ cần thực hiện thủ công hashCode(). Điều này là rất quan trọng để nó nhất quán với triển khai equals() của bạn.

kotlin Copy
class User(val name: String, val email: String, val phoneNumber: String) {
    override fun hashCode(): Int {
        var result = name.hashCode()
        result = 31 * result + email.hashCode()
        result = 31 * result + phoneNumber.hashCode()
        return result
    }
}

Tóm tắt: Các phương thức equals()hashCode() là một cặp. Nếu bạn ghi đè một phương thức, bạn phải ghi đè phương thức còn lại. Trong Kotlin, hãy sử dụng data class cho điều này, hoặc hãy cẩn thận trong các triển khai thủ công của bạn.

Mục 12: Luôn Ghi Đè toString()

Tóm tắt

Mặc dù không quan trọng như equals()hashCode(), một triển khai tốt cho toString() là rất cần thiết cho quá trình gỡ lỗi và ghi log. Triển khai mặc định từ Object không hữu ích.

Java

Một triển khai thủ công cho toString() nên rõ ràng, ngắn gọn và cung cấp tất cả thông tin liên quan về trạng thái của đối tượng.

java Copy
public final class User {
    // ...
    @Override
    public String toString() {
        return "User{" +
               "name='" + name + '\'' +
               ", email='" + email + '\'' +
               ", phoneNumber='" + phoneNumber + '\'' +
               '}';
    }
}

Kotlin (Data Class)

data class của Kotlin cũng tạo ra một phương thức toString() hữu ích, bao gồm tất cả các thuộc tính trong constructor chính.

kotlin Copy
// Tự động tạo ra một phương thức toString() dễ đọc.
data class User(val name: String, val email: String, val phoneNumber: String)

Kotlin (Non-Data Class)

Đối với một lớp không phải là data, bạn sẽ cần ghi đè toString() để có được một đại diện có ý nghĩa.

kotlin Copy
class User(val name: String, val email: String, val phoneNumber: String) {
    override fun toString(): String {
        return "User(name=$name, email=$email, phoneNumber=$phoneNumber)"
    }
}

Tóm tắt: Một phương thức toString() hữu ích là cần thiết. Khi bạn tạo một lớp, hãy luôn xem xét cách mà biểu diễn chuỗi của nó có thể giúp bạn hiểu trạng thái của nó.

Mục 13: Ghi Đè clone() Một Cách Cẩn Thận

Tóm tắt

Phương thức clone() là một phần của một framework phức tạp và có nhiều thiếu sót. Giao diện Cloneable là một giao diện đánh dấu, và Object.clone() là một phương thức được bảo vệ thực hiện sao chép nông. Cách tiếp cận này rất dễ gãy và có thể dẫn đến lỗi. Ví dụ, nếu một đối tượng chứa một trường có thể thay đổi, một bản sao nông sẽ dẫn đến hai đối tượng chia sẻ cùng một trạng thái có thể thay đổi, điều này gần như không bao giờ là điều bạn muốn.

Java

Để triển khai đúng clone(), bạn phải xử lý cả giao diện đánh dấu Cloneable và các ngoại lệ CloneNotSupportedExceptions tiềm năng. Ngay cả như vậy, nó thường phức tạp hơn là giá trị.

java Copy
// Một triển khai clone() có thiếu sót trong Java.
public class MyObject implements Cloneable {
    private List<String> list;

    // ...

    @Override
    public MyObject clone() throws CloneNotSupportedException {
        // Đây là một bản sao nông! Đối tượng được sao chép chia sẻ cùng một danh sách.
        return (MyObject) super.clone();
    }
}

Một giải pháp tốt hơn là sử dụng một constructor sao chép hoặc một nhà máy sao chép, đây là một mẫu an toàn và dễ đọc hơn.

Kotlin (Data Class)

data class của Kotlin cung cấp một phương thức copy() mà là một ví dụ hoàn hảo của cách tiếp cận idiomatic này. Phương thức copy() tạo ra một thể hiện mới với cùng các thuộc tính như bản gốc, cho phép bạn tùy chọn thay đổi các trường cụ thể. Nó xử lý hành vi sao chép sâu và nông đúng như bạn mong đợi.

kotlin Copy
data class MyObject(val name: String, val list: MutableList<String>)

// Tạo một bản sao sâu bằng cách sử dụng phương thức copy().
val original = MyObject("A", mutableListOf("1", "2"))
val copy = original.copy(list = ArrayList(original.list))

Kotlin (Non-Data Class)

Các lớp thông thường trong Kotlin không có phương thức copy() tự động. Bạn sẽ cần tạo một cái cho chính mình, hoặc một constructor sao chép, để cung cấp chức năng này.

kotlin Copy
class MyObject(val name: String, val list: MutableList<String>) {
    fun copy(name: String = this.name, list: MutableList<String> = this.list): MyObject {
        return MyObject(name, list)
    }
}

// Tạo một bản sao bằng cách sử dụng chức năng tùy chỉnh của chúng tôi.
val original = MyObject("A", mutableListOf("1", "2"))
val copy = original.copy(list = ArrayList(original.list))

Tóm tắt: Tránh sử dụng phương thức clone(). Sử dụng một constructor sao chép hoặc, tốt hơn nữa, phương thức copy() trong một data class của Kotlin. Đối với các lớp không phải là data, hãy tạo một chức năng copy() của riêng bạn.

Mục 14: Cân Nhắc Triển Khai Comparable

Tóm tắt

Giao diện Comparable được sử dụng để xác định thứ tự tự nhiên. Nó chứa một phương thức duy nhất, compareTo(), so sánh đối tượng this với một đối tượng khác. Triển khai đúng compareTo() cho phép các đối tượng được sắp xếp và làm việc với các bộ sưu tập như TreeSetTreeMap.

Java (Java 21)

Java hiện đại cung cấp một cách ngắn gọn hơn để triển khai Comparable bằng cách sử dụng Comparator.comparingthenComparing, điều này sạch sẽ hơn và ít sai sót hơn so với việc so sánh thủ công.

java Copy
import java.util.Comparator;

public final class User implements Comparable<User> {
    private final String name;
    private final String email;
    private final String phoneNumber;

    public User(String name, String email, String phoneNumber) {
        this.name = name;
        this.email = email;
        this.phoneNumber = phoneNumber;
    }

    private static final Comparator<User> COMPARATOR =
        Comparator.comparing(User::getName)
                  .thenComparing(User::getEmail)
                  .thenComparing(User::getPhoneNumber);

    public String getName() { return name; }
    public String getEmail() { return email; }
    public String getPhoneNumber() { return phoneNumber; }

    @Override
    public int compareTo(User user) {
        return COMPARATOR.compare(this, user);
    }
}

Kotlin

Đối với một lớp thông thường, bạn triển khai Comparable và ghi đè compareTo() theo cách tương tự. Hàm compareBy cung cấp một lựa chọn thay thế rõ ràng và idiomatic cho logic so sánh thủ công.

kotlin Copy
class User(val name: String, val email: String, val phoneNumber: String) : Comparable<User> {
    override fun compareTo(other: User): Int {
        return compareBy<User> { it.name }
            .thenBy { it.email }
            .thenBy { it.phoneNumber }
            .compare(this, other)
    }
}

Tóm tắt: Sử dụng giao diện Comparable để xác định thứ tự tự nhiên cho các đối tượng của bạn, nhưng hãy chắc chắn tuân thủ hợp đồng nghiêm ngặt của nó.

Kết luận

Các phương thức kế thừa từ java.lang.Object là cơ bản cho nền tảng Java. Bằng cách triển khai đúng equals(), hashCode(), toString(), và compareTo(), bạn đảm bảo rằng các đối tượng của bạn hoạt động theo cách dự đoán và làm việc chính xác với phần còn lại của hệ sinh thái Java. Những bài học mà chúng ta đã học ở đây là một ví dụ hoàn hảo về lý do tại sao data class của Kotlin lại là một công cụ mạnh mẽ và hiệu quả, vì nó tự động hóa việc triển khai các phương thức quan trọng này, giúp bạn tránh được những cạm bẫy phổ biến.

Những gì tiếp theo

Trong bài viết tiếp theo, chúng ta sẽ bắt đầu khám phá Chương 4: Các Lớp và Giao Diện, và đi sâu vào cách thiết kế chúng để rõ ràng và mạnh mẽ. Hãy theo dõi!

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào