Java OO & Functional
36/38
Bài 36 / 38~12 phútStream API & LambdaLộ trình · chặng 06/21Miễn phí… lượt xem

Immutability và Functional Style: Thiết Kế Code Java Không Có Bất Ngờ

Thiết kế hệ thống bất biến (Immutability), Pure Functions, Record trong Java 16+, kỹ thuật Copy-then-Validate và cạm bẫy final reference.

TL;DR: Tính bất biến (Immutability) là trụ cột cốt lõi của phong cách lập trình hàm trong Java. Khi trạng thái đối tượng không thể bị thay đổi sau khi khởi tạo, hệ thống đạt được sự an toàn luồng tự nhiên (Thread-safety) mà không cần khóa đồng bộ (lock). Java hiện đại cung cấp 4 cấp độ kiểm soát bất biến: từ final fields (bất biến nông - shallow), sao chép phòng vệ (Defensive Copy với List.copyOf), bộ sưu tập bất biến (List.of), cho đến Java Record (Java 16+). Điểm mấu chốt là nhận biết final reference chỉ khóa con trỏ chứ không khóa nội dung bên trong, và luôn áp dụng nguyên tắc Copy-then-Validate để phòng chống lỗ hổng TOCTOU.

Sau khi đã nắm vững Lambda Expression và các bài học về xử lý luồng song song tại Parallel Stream, bài học này sẽ đúc kết triết lý thiết kế giúp bạn viết mã nguồn Java ổn định, dễ suy luận và không gặp lỗi ngầm.

1. Pure Function và tính minh bạch tham chiếu là gì?

Một hàm thuần túy (Pure Function) phải đáp ứng 2 điều kiện tiên quyết:

  1. Tính tất định (Deterministic): Cùng một tham số đầu vào luôn trả về cùng một giá trị đầu ra (không phụ thuộc đồng hồ hệ thống, biến ngẫu nhiên hay cơ sở dữ liệu).
  2. Không có tác vụ phụ (No Side-effects): Không thay đổi dữ liệu của tham số truyền vào, không ghi đè biến ngoài phạm vi và không làm biến đổi môi trường ngoại cảnh.
Java
// 1. IMPURE FUNCTION: Mutates caller input parameter directly (Side-effect)
public void removeOverdue(List<LoanRecord> records) {
    records.removeIf(LoanRecord::isOverdue); // Destroys caller data!
}

// 2. PURE FUNCTION: Emits fresh list, preserves input intact
public List<LoanRecord> filterOverdue(List<LoanRecord> records) {
    return records.stream()
        .filter(LoanRecord::isOverdue)
        .toList(); // Fresh unmodifiable list
}

Hệ quả của Pure Function là Tính minh bạch tham chiếu (Referential Transparency): Ta có thể thay thế lời gọi hàm bằng chính giá trị kết quả của nó tại bất kỳ vị trí nào mà không làm thay đổi hành vi của toàn bộ chương trình.

2. JavaBeans cổ điển và Functional Java hiện đại khác biệt ra sao?

Sự chuyển dịch từ kiến trúc nguyên khối sang Cloud-Native và Microservices đã thay đổi căn bản cách thức thiết kế đối tượng trong Java:

Tiêu chíJavaBeans (1996)Functional Java Hiện Đại (2020+)
Khởi tạoConstructor rỗng (no-arg), điền dữ liệu qua SetterConstructor đầy đủ tham số, tạo trạng thái nguyên tử
Trạng tháiMutable (Dễ thay đổi), rủi ro Race ConditionImmutable (Bất biến), an toàn đa luồng tự nhiên
Đóng góiGetter/Setter truyền thốngJava Record, accessor bất biến
Môi trườngĐơn luồng, ứng dụng Desktop/MonolithĐa luồng quy mô lớn, Cloud-Native, Microservices

3. Bốn cấp độ thiết kế Immutability trong Java bao gồm những gì?

Java cung cấp nhiều cơ chế kiểm soát mức độ bất biến tăng dần:

Bốn cấp độ Immutability từ nông đến sâu: từ khóa final đến Defensive Copying và Java Records

4. Tại sao từ khóa final chỉ tạo ra Shallow Immutability?

Một nhầm lẫn kinh điển của lập trình viên Java là cho rằng khai báo final sẽ làm dữ liệu bất biến:

Biến của caller và field final cùng trỏ vào một đối tượng ArrayList trên heap, final chỉ khoá mũi tên nên nội dung hộp vẫn sửa được từ cả hai phía

Java
public class LibraryCatalog {
    private final List<String> categories; // Locks only the reference pointer!

    public LibraryCatalog(List<String> categories) {
        this.categories = categories; // Danger: Retains mutable external reference
    }

    public List<String> getCategories() { return categories; }
}

// Subtle trap:
List<String> raw = new ArrayList<>(List.of("Science", "History"));
LibraryCatalog catalog = new LibraryCatalog(raw);

raw.add("Malicious Category"); // 1. Mutated from external reference!
catalog.getCategories().clear(); // 2. Mutated directly via getter!

Từ khóa final chỉ bảo đảm biến categories không bị gán lại sang một vùng nhớ khác (this.categories = otherList sẽ báo lỗi biên dịch). Tuy nhiên, đối tượng ArrayList trên Heap vẫn hoàn toàn có thể bị chỉnh sửa nội dung!

5. Java Record và kỹ thuật Copy-then-Validate bảo vệ dữ liệu thế nào?

Từ Java 16, Java Record trở thành tiêu chuẩn vàng để xây dựng các đối tượng mang giá trị (Value Objects). Khi khai báo Record, trình biên dịch tự động tạo ra một lớp final với tất cả các thuộc tính là private final.

Tuy nhiên, nếu Record chứa Collection, bạn bắt buộc phải thực hiện sao chép phòng vệ qua Compact Constructor theo nguyên tắc Copy-then-Validate để phòng chống lỗ hổng TOCTOU (Time-of-Check to Time-of-Use):

Java
public record BookCatalog(String id, List<String> tags) {
    public BookCatalog {
        // STEP 1: Defensive copy IMMEDIATELY to establish isolated snapshot
        tags = List.copyOf(tags);

        // STEP 2: Validate ON IMMUTABLE SNAPSHOT (Anti-TOCTOU)
        if (tags.isEmpty()) {
            throw new IllegalArgumentException("Tags must not be empty");
        }
    }
}

Nếu đảo ngược lại (kiểm tra tags.isEmpty() trước rồi mới gọi List.copyOf), một luồng khác chạy song song có thể gọi tags.clear() ngay giữa thời điểm kiểm tra và thời điểm sao chép, dẫn đến việc lưu trữ một danh sách rỗng vi phạm ràng buộc nghiệp vụ.

6. Khi nào nên chấp nhận Mutable Local Variable thay vì Immutability?

Bất biến không phải là giải pháp vạn năng. Mỗi lần "thay đổi" dữ liệu, một đối tượng mới lại được sinh ra trên Heap, gây áp lực thu gom rác (GC Allocation Pressure) trong các vòng lặp lớn.

Java
// 1. Inefficient: Allocates 100,000 short-lived String objects on Heap
String report = "";
for (int i = 0; i < 100_000; i++) {
    report = report + "Line " + i + "\n";
}

// 2. Idiomatic: Use mutable local variable inside method scope
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100_000; i++) {
    sb.append("Line ").append(i).append("\n"); // High-speed in-place mutation
}
String finalReport = sb.toString(); // Expose immutable result externally

Nguyên tắc vàng: Mặc định thiết kế bất biến tại các ranh giới giao tiếp (API Boundaries, DTO, Entity, Shared State). Cho phép sử dụng các biến có thể thay đổi trong phạm vi nội bộ của hàm (Local Scope) khi cần tối ưu hóa hiệu năng tính toán.

7. Tóm tắt các điểm cốt lõi

  • Pure Function không có tác vụ phụ và có tính tất định, giúp mã nguồn dễ dàng kiểm thử và an toàn đa luồng.
  • Từ khóa final chỉ tạo ra Shallow Immutability; cần kết hợp List.copyOf() để đạt Deep Immutability.
  • Sử dụng Java Record kết hợp nguyên tắc Copy-then-Validate để triệt tiêu lỗi đa luồng TOCTOU.
  • Sử dụng biến mutable cục bộ bên trong hàm khi cần tối ưu hiệu năng tính toán trong các vòng lặp lớn.

8. Câu hỏi tự kiểm tra (Self-Check)

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Tại sao khai báo final List<String> list = new ArrayList<>() không bảo đảm rằng danh sách đó không thể bị sửa đổi?
  2. Q2
    Nguyên tắc Copy-then-Validate trong Constructor của Record giải quyết lỗi đa luồng TOCTOU như thế nào?
  3. Q3
    Điểm khác biệt cốt lõi giữa Collections.unmodifiableList(source) và List.copyOf(source) là gì?
  4. Q4
    Pure Function mang lại những lợi ích cụ thể nào cho việc kiểm thử phần mềm (Unit Testing)?
  5. Q5
    Khi nào việc áp dụng tính bất biến (Immutability) lại trở thành một điểm nghẽn hiệu năng tiêu cực?

Bài tiếp theo: Mini Challenge: Xây Dựng Báo Cáo Thư Viện Với Stream API

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Lộ trình Junior Java Backend Developer

Chặng 6/21 · Lambda & Stream API — bài 10/12 của chặng

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

Mini-challenge: Sales report với stream pipeline