CodeGym /Các khóa học /JAVA 25 SELF /Immutability — tính bất biến của các lớp record

Immutability — tính bất biến của các lớp record

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Các lớp record và tính bất biến

Tính bất biến — là thuộc tính của một đối tượng, trong đó trạng thái của nó không thể bị thay đổi sau khi được tạo. Nói cách khác: khi đối tượng đã được tạo, bạn không thể thay đổi dữ liệu bên trong của nó. Hết! Như khắc vào đá.

Hãy hình dung một tấm vé tàu. Ngay khi nó được in ra, bạn không thể thay đổi ngày hoặc nơi khởi hành (trừ khi dùng mánh khóe với Photoshop). Vé — là một đối tượng bất biến. Nếu bạn muốn tấm vé khác — bạn mua tấm mới.

Trong lập trình, các đối tượng như vậy được gọi là immutable objects. Chúng bảo vệ chương trình khỏi các thay đổi vô tình, giúp mã an toàn và dễ dự đoán hơn.

Dấu hiệu của một đối tượng bất biến

  • Tất cả các trường của đối tượng — final (chỉ được gán một lần, thường là trong constructor).
  • Không có setter (các phương thức thay đổi giá trị trường).
  • Tất cả các phương thức trả về dữ liệu nội bộ, hoặc trả về bản sao, hoặc chính dữ liệu đó cũng là bất biến.

Các lớp record trong Java được tạo ra chính để xây dựng các cấu trúc dữ liệu bất biến một cách đơn giản và nhẹ nhàng.

Vì sao record là bất biến?

  • Tất cả các thành phần của record — final.
    Khi bạn khai báo record, trình biên dịch tự động tạo tất cả trường của nó là private final. Điều này có nghĩa là sau khi tạo đối tượng, bạn sẽ không thể thay đổi các trường của nó.
  • Không có setter.
    Trong lớp record bạn không thể thêm phương thức setX(int x) — trình biên dịch sẽ không cho phép bạn thay đổi giá trị trường sau khi đối tượng được tạo.
  • Constructor chỉ gán giá trị một lần.
    Tất cả giá trị được đặt chỉ tại thời điểm tạo đối tượng.

Ví dụ

public record Point(int x, int y) {}

Point p = new Point(5, 10);
// p.x = 7; // Lỗi biên dịch: trường x có quyền truy cập private và là final
// p.x(7);  // Lỗi: không có setter!
System.out.println(p.x()); // 5

Thử thay đổi trường hoặc gọi setter không tồn tại sẽ dẫn đến lỗi biên dịch. Java giám sát nghiêm ngặt hợp đồng về tính bất biến.

2. Lợi ích của các đối tượng bất biến

An toàn trong môi trường đa luồng

Trong các chương trình đa luồng (mà hiện nay là phần lớn!), các đối tượng bất biến giống như áo giáp. Nếu đối tượng không thể thay đổi, nhiều luồng có thể đọc nó thoải mái mà không sợ ai đó đồng thời sửa dữ liệu. Không cần đồng bộ hóa truy cập và lo lắng về race condition.

Sự thật: nhiều lớp trong thư viện tiêu chuẩn Java, được dùng nhiều trong các kịch bản đa luồng, hoặc là bất biến, hoặc được bảo vệ đặc biệt khỏi việc bị thay đổi.

Dễ hiểu hơn

Nếu đối tượng là bất biến, bạn luôn chắc chắn: truyền nó sang phương thức hoặc lớp khác — và nó sẽ không bị “đổi khác sau lưng” bạn. Điều này giúp việc đọc và gỡ lỗi mã dễ hơn rất nhiều. Không cần đoán ai và ở đâu có thể đổi một trường — không ai có thể làm thế!

Tiện dùng làm khóa trong collection

Các đối tượng bất biến rất phù hợp để dùng làm khóa trong các collection như HashMap hoặc HashSet. Tại sao? Bởi vì equalshashCode của chúng chỉ phụ thuộc vào các trường không thay đổi. Nghĩa là đối tượng sẽ không “bị lạc” trong collection do trạng thái của nó thay đổi.

Ít lỗi ẩn hơn

Đối tượng mutable rất dễ bị hỏng khi vô tình truyền nhầm tham chiếu. Còn đối tượng bất biến — giống như một cuốn sách in: không ai có thể xé hoặc viết lại trang.

3. So sánh với các lớp thông thường

Hãy so sánh hành vi của một lớp thông thường và một lớp record. Ví dụ ta lấy mô hình đơn giản của một điểm trên mặt phẳng.

Lớp thông thường (mutable)

public class PointClass {
    private int x;
    private int y;

    public PointClass(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) { 
        this.x = x; 
    }
    public void setY(int y) { 
        this.y = y; 
    }
}

Có thể tạo đối tượng rồi thay đổi trạng thái của nó bao nhiêu cũng được:

PointClass p = new PointClass(1, 2);
p.setX(10); // p.x bây giờ là 10

Lớp record (immutable)

public record Point(int x, int y) {}

Tạo đối tượng — và xong, nó sẽ luôn như lúc được tạo:

Point p = new Point(1, 2);
// p.x = 10;   // Lỗi! Không có quyền truy cập vào trường
// p.x(10);    // Lỗi! Không có setter

Bảng: so sánh hành vi

Lớp thông thường Lớp record
Trường bất kỳ chỉ private final
Setter có thể thêm không thể thêm
Tính thay đổi có thể thay đổi (mutable) bất biến
Tự sinh không có (equals, hashCode, toString)

4. Thực hành: cách sử dụng tính bất biến của các lớp record

Hãy thử áp dụng lớp record trong một ví dụ nhỏ. Giả sử chúng ta có một ứng dụng ngân hàng và muốn lưu thông tin về giao dịch:

public record Transaction(String fromAccount, String toAccount, double amount) {}

Tạo đối tượng:

Transaction t = new Transaction("12345", "67890", 1500.0);
System.out.println(t);
// Transaction[fromAccount=12345, toAccount=67890, amount=1500.0]

Thử “chuyển” tiền sang tài khoản khác:

// t.toAccount = "11111"; // Lỗi! Trường là final, không có quyền truy cập
// t.toAccount("11111");  // Lỗi! Không có setter

Nếu cần một giao dịch khác — tạo đối tượng mới:

Transaction t2 = new Transaction(t.fromAccount(), "11111", t.amount());

Quan trọng: bất biến không có nghĩa là “bất tiện”. Đây chỉ là một phong cách làm việc khác: nếu cần trạng thái mới — hãy tạo đối tượng mới.

5. Những đặc điểm của tính bất biến: cần ghi nhớ gì

Tính bất biến không phải lúc nào cũng tuyệt đối!

Lớp record đảm bảo rằng các trường của nó không thay đổi. Nhưng nếu một trường là tham chiếu đến đối tượng mutable (ví dụ mảng hoặc một lớp thông thường), thì nội dung của đối tượng đó có thể bị thay đổi.

Ví dụ với mảng

public record DataHolder(int[] data) {}

int[] arr = {1, 2, 3};
DataHolder holder = new DataHolder(arr);
arr[0] = 99;
System.out.println(holder.data()[0]); // 99! Mảng đã bị thay đổi

Kết luận: nếu muốn bất biến “thật sự”, hãy dùng chỉ các kiểu bất biến (String, Integer, các record khác, v.v.) hoặc tạo bản sao của các đối tượng mutable bên trong canonical constructor của lớp record. Ví dụ:

int[] copy = Arrays.copyOf(data, data.length);

6. Tự làm một lớp thông thường trở nên bất biến

Nếu bạn muốn biến một lớp thông thường thành immutable (bất biến), bạn sẽ phải tự làm thủ công:

  • Đánh dấu tất cả trường là private final,
  • Không thêm setter,
  • Khởi tạo tất cả trường chỉ qua constructor,
  • Nếu trường là đối tượng mutable, hãy tạo bản sao bảo vệ (defensive copy).

Ví dụ

public final class User {
    private final String name;
    private final int age;

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

    public String name() { 
        return name; 
    }
    public int age() { 
        return age; 
    }
}

Công nhận là với lớp record thì làm sẽ ngắn gọn và đơn giản hơn:

public record User(String name, int age) {}

7. Các lỗi thường gặp khi làm việc với lớp record bất biến

Lỗi №1: cố gắng thay đổi trường sau khi tạo.
Người mới thường cố viết p.x = 42; hoặc p.x(42); cho đối tượng record. Nhưng trình biên dịch sẽ nói ngay: “Không được! Trường final, không có setter”.

Lỗi №2: dùng các đối tượng mutable làm thành phần của record.
Nếu bạn thêm vào record một trường kiểu List, Map, mảng hoặc đối tượng mutable khác, thì bản thân record sẽ không bảo vệ bạn khỏi việc nội dung của đối tượng này bị thay đổi. Ví dụ, nếu bạn có record User(List<String> hobbies), ai đó có thể thêm hoặc xóa phần tử khỏi danh sách, và điều đó sẽ làm thay đổi trạng thái đối tượng record của bạn. Để tránh điều này, hãy sử dụng các collection bất biến (List.copyOf, Collections.unmodifiableList) hoặc tạo bản sao của collection bên trong constructor của record.

Lỗi №3: hiểu sai về tính bất biến.
Một số người nghĩ rằng nếu đối tượng là record thì nó được bảo vệ khỏi mọi thay đổi. Thực tế, nếu các trường là tham chiếu đến đối tượng mutable, nội dung của chúng có thể bị đổi và điều này dẫn đến các lỗi khó lường.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION