Immutability và Functional Style — thiết kế Java không có surprise
Pure function, referential transparency, cách Java implement immutability qua final/record/List.of, pitfall final reference vs immutable content, và tổng kết module Stream & Lambda.
TL;DR: Immutability — trạng thái không thay đổi sau khi tạo — là nền tảng của functional programming trong Java. Khi không method nào sửa được object, code dễ reasoning, thread-safe không cần lock, và equals/hashCode đơn giản hơn. Java cung cấp nhiều cấp độ: final field (shallow), defensive copy trong constructor (deep), List.of/Map.of (immutable collections), và record (Java 16+, tự generate immutable value class). Pitfall quan trọng nhất: final List<X> làm reference constant nhưng content vẫn mutate được. Bài này kết thúc module với checklist 7 skills functional Java bạn đã tích lũy.
1. Scenario — bug mất 2 ngày vì mutable list
Team phát triển order management. Một service trả về list orders "active" cho nhiều caller khác nhau:
public class OrderService {
private List<Order> activeOrders = new ArrayList<>();
public List<Order> getActiveOrders() {
return activeOrders; // tra truc tiep reference noi
}
public void processOrders(List<Order> orders) {
orders.removeIf(o -> o.isExpired()); // modify input
// process remaining...
}
}
// Caller A:
List<Order> orders = service.getActiveOrders();
// Caller B (cung thread, sau Caller A):
service.processOrders(service.getActiveOrders()); // removeIf tren list noi!
// Caller A gio co it order hon -- bi modify boi Caller B ma khong biet
System.out.println(orders.size()); // khac voi luc lay ra -- SURPRISE!
Bug này mất 2 ngày debug vì không có exception, không có log lỗi — chỉ dữ liệu im lặng biến mất. Functional approach ngăn hoàn toàn: không method nào được sửa input của method khác.
2. Pure function — định nghĩa và lý do tại sao
Pure function là hàm có 2 thuộc tính:
- Deterministic — với cùng input, luôn trả cùng output. Không phụ thuộc state ngoài (global variable, database, system time, random).
- No side effects — không thay đổi state ngoài: không modify tham số, không ghi file, không gọi network, không update biến ngoài scope.
// PURE -- chi phu thuoc input, khong modify gi
public static int add(int a, int b) {
return a + b; // output chi phu thuoc a, b
}
// PURE -- tra list moi, khong modify input
public static List<Order> filterActive(List<Order> orders) {
return orders.stream()
.filter(o -> !o.isExpired())
.toList(); // list moi, orders goc khong doi
}
// IMPURE -- phu thuoc state ngoai (System.currentTimeMillis)
public static boolean isExpired(Order o) {
return o.expiryTime() < System.currentTimeMillis(); // state ngoai: dong ho
}
// IMPURE -- side effect: modify input
public static void removeExpired(List<Order> orders) {
orders.removeIf(Order::isExpired); // modify tham so -- bieu hien impure
}
Referential transparency là hệ quả của pure function: bạn có thể thay thế mọi lời gọi hàm bằng giá trị kết quả của nó mà program không thay đổi behavior.
// add(2, 3) la referential transparent -- co the thay bang 5 bat cu dau
int x = add(2, 3) + add(2, 3); // = 5 + 5 = 10
int y = 5 + 5; // tuong duong hoan toan
// System.currentTimeMillis() KHONG transparent -- 2 lan goi cho ket qua khac nhau
long t1 = System.currentTimeMillis();
long t2 = System.currentTimeMillis(); // t1 != t2
Tại sao quan trọng: code referentially transparent dễ test (không cần mock state ngoài), dễ parallelize (không race condition), dễ cache (memoize kết quả), và dễ reasoning (đọc từng hàm độc lập).
3. Tại sao Java enterprise mặc định mutable
Java được thiết kế năm 1995 với quan điểm mutable-by-default, có lý do lịch sử:
-
JavaBean spec (1996) — định nghĩa bean như "class có no-arg constructor + getters + setters". Mọi framework lúc đó (Struts, Spring 1.x, Hibernate 3.x) dựa vào setter injection và reflection để tạo và điền giá trị. Immutable object không có setter → không tương thích JavaBean.
-
Hibernate/JPA yêu cầu mutable entity —
@Entityphải có no-arg constructor và setter để proxy subclass override. Hibernate tạo proxy extends entity class, override getter để lazy-load. Không thể extend final class. -
Performance quan niệm cũ — quan điểm "tạo object mới mỗi lần modify = memory leak" phổ biến khi JVM garbage collector còn thô sơ (trước Java 7 G1GC). Thực tế JVM hiện đại (G1, ZGC, Shenandoah) tối ưu short-lived object allocation rất tốt.
-
Spring DI truyền thống — field injection
@Autowiredtrên field mutable không final là default của Spring Boot 1.x-2.x. Constructor injection (immutable final field) chỉ được recommend mạnh từ Spring 4.3+ và Spring Boot 2.x.
Kết quả: cả thế hệ developer Java học pattern mutable-by-default vì "framework yêu cầu vậy". Với Java hiện đại (16+), record, constructor injection, và JPA 2.2+ hỗ trợ immutable entity một phần — mutable-by-default không còn là best default.
4. Cách Java implement immutability — 4 cấp độ
4.1 final field — shallow immutable
final đảm bảo reference không thay đổi sau khi gán trong constructor. Đây là cấp độ shallow — reference không đổi nhưng object được trỏ đến có thể vẫn mutable.
public class Config {
private final String host; // reference khong doi sau constructor
private final int port;
private final List<String> tags; // reference final -- nhung tags.add() van duoc!
public Config(String host, int port, List<String> tags) {
this.host = host;
this.port = port;
this.tags = tags; // chi copy reference, khong copy content
}
public List<String> getTags() { return tags; }
}
Config cfg = new Config("localhost", 8080, new ArrayList<>(List.of("web", "api")));
cfg.getTags().add("admin"); // THANH CONG -- tags van mutable du field la final!
final ngăn this.tags = otherList nhưng không ngăn tags.add(...). Đây là pitfall quan trọng nhất về immutability trong Java.
4.2 Defensive copy — deep immutable
Để ngăn mutation từ ngoài, phải copy defensive trong constructor và trong getter:
public class Config {
private final String host;
private final int port;
private final List<String> tags;
public Config(String host, int port, List<String> tags) {
this.host = host;
this.port = port;
// Defensive copy: copy noi dung, khong giu reference goc
this.tags = List.copyOf(tags); // Java 10+ -- unmodifiable deep copy
}
public List<String> getTags() {
return tags; // safe: tra ve unmodifiable list
}
}
Config cfg = new Config("localhost", 8080, new ArrayList<>(List.of("web", "api")));
cfg.getTags().add("admin"); // throw UnsupportedOperationException -- PROTECTED
List.copyOf() (Java 10+) tạo unmodifiable snapshot — caller không thể sửa content, và thay đổi trên list gốc sau khi pass vào constructor cũng không ảnh hưởng đến tags bên trong.
Nếu constructor nhận Date, List, Set, Map hay bất kỳ mutable object nào, luôn copy defensive. Caller có thể giữ reference gốc và modify sau khi constructor chạy xong. Thiếu defensive copy là security vulnerability (injection vào trusted object) và correctness bug (shared mutable state).
4.3 Record (Java 16+, JEP 395) — immutable value class tự động
Record là class đặc biệt Java 16 (stable): compiler tự generate constructor, accessor (getter), equals, hashCode, toString từ danh sách component. Mọi component là private final.
// Khai bao record
public record Point(double x, double y) {}
// Tuong duong viet tay:
public final class Point {
private final double x;
private final double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double x() { return x; } // accessor, khong phai getX()
public double y() { return y; }
@Override public boolean equals(Object o) { ... } // compare theo value
@Override public int hashCode() { ... }
@Override public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
}
Record với validation trong canonical constructor:
public record Order(String id, int amount, List<String> tags) {
// Compact canonical constructor -- validate + defensive copy
public Order {
if (amount < 0) throw new IllegalArgumentException("amount phai >= 0");
tags = List.copyOf(tags); // defensive copy component collection
}
}
Order o = new Order("ORD-001", 500, List.of("web", "mobile"));
o.tags().add("admin"); // UnsupportedOperationException -- protected
Record không extend class khác (extends Record ngầm), không cho setter, component luôn final. Phù hợp cho: DTO, value object, configuration, API response/request model.
4.4 List.of / Map.of / Set.of — immutable factory (Java 9+)
Factory method tạo true immutable collection — không cho add, remove, set, put, không cho phép null value:
List<String> immutable = List.of("a", "b", "c");
immutable.add("d"); // UnsupportedOperationException
immutable.set(0, "x"); // UnsupportedOperationException
immutable.get(0); // "a" -- read OK
Map<String, Integer> map = Map.of("one", 1, "two", 2);
map.put("three", 3); // UnsupportedOperationException
Khác Collections.unmodifiableList(list) (wrapper, view, gốc vẫn mutable) và List.copyOf(list) (snapshot unmodifiable). Xem bảng so sánh ở module 03 bài immutable collections.
5. Pitfall — final reference KHÔNG có nghĩa là immutable content
Đây là hiểu lầm phổ biến nhất, cần nhắc lại rõ:
final List<String> list = new ArrayList<>(List.of("a", "b"));
// final ngan reassign reference:
// list = new ArrayList<>(); // compile error: cannot assign final variable
// Nhung KHONG ngan modify content:
list.add("c"); // OK -- list gio la [a, b, c]
list.remove(0); // OK -- list gio la [b, c]
list.clear(); // OK -- list rong
final Map<String, Integer> map = new HashMap<>();
map.put("key", 1); // OK -- final khong lien quan den map content
map.clear(); // OK
Để có immutable list, phải dùng:
List.of(...)— factory immutable từ đầu.List.copyOf(mutableList)— snapshot immutable.Collections.unmodifiableList(list)— view (gốc vẫn mutate được qua reference gốc).
| Pattern | Reference immutable | Content immutable |
|---|---|---|
final List<X> x = new ArrayList<>() | Có | Không |
final List<X> x = List.of(...) | Có | Có |
final List<X> x = Collections.unmodifiableList(source) | Có | View only (source vẫn mutate được) |
final List<X> x = List.copyOf(source) | Có | Có (snapshot) |
6. Builder pattern + immutable result
Khi cần tạo object phức tạp nhiều field tùy chọn mà vẫn immutable, dùng builder:
public record ServerConfig(
String host,
int port,
int timeoutMs,
boolean ssl,
List<String> allowedOrigins
) {
// Builder nested trong record
public static class Builder {
private String host = "localhost";
private int port = 8080;
private int timeoutMs = 5000;
private boolean ssl = false;
private List<String> allowedOrigins = new ArrayList<>();
public Builder host(String host) { this.host = host; return this; }
public Builder port(int port) { this.port = port; return this; }
public Builder timeoutMs(int ms) { this.timeoutMs = ms; return this; }
public Builder ssl(boolean ssl) { this.ssl = ssl; return this; }
public Builder addOrigin(String origin) { allowedOrigins.add(origin); return this; }
public ServerConfig build() {
return new ServerConfig(host, port, timeoutMs, ssl,
List.copyOf(allowedOrigins)); // defensive copy
}
}
}
// Su dung:
ServerConfig config = new ServerConfig.Builder()
.host("api.example.com")
.port(443)
.ssl(true)
.addOrigin("https://frontend.example.com")
.build();
// config la immutable sau build()
7. Functional style trong Java practice
Các quy tắc functional style áp dụng trong code Java hàng ngày:
7.1 Stream pipeline thay manual loop có side effect
// IMPURE -- manual loop modify shared state
List<String> result = new ArrayList<>();
for (Order o : orders) {
if (o.amount() > 100) {
result.add(o.customerId()); // side effect: add vao list ngoai
}
}
// PURE -- stream pipeline, khong shared state
List<String> result = orders.stream()
.filter(o -> o.amount() > 100)
.map(Order::customerId)
.toList(); // tao list moi, khong modify gi
7.2 Collectors.toUnmodifiableList() cho output rõ semantic
// Intent khong ro -- caller co the modify result
Map<String, List<Order>> byCustomer = orders.stream()
.collect(Collectors.groupingBy(Order::customerId));
// Intent ro rang -- caller biet result la immutable
Map<String, List<Order>> byCustomer = orders.stream()
.collect(Collectors.groupingBy(
Order::customerId,
Collectors.toUnmodifiableList()
));
7.3 Optional thay null returns
// IMPURE -- caller phai nho check null, de NullPointerException
public Order findById(String id) {
return orderDb.get(id); // co the tra null
}
// FUNCTIONAL -- explicit representation of "not found"
public Optional<Order> findById(String id) {
return Optional.ofNullable(orderDb.get(id));
}
// Caller xu ly explicitly:
service.findById("ORD-001")
.map(Order::amount)
.orElse(0);
7.4 Pure function cho business logic, side effect tập trung tại boundary
// SAI -- business logic lan sang side effect
public void processOrder(Order o) {
if (o.amount() > 10000) {
sendEmail(o.customerId(), "VIP order detected"); // side effect trong business
}
saveToDb(o); // side effect trong business
}
// TOT HON -- tach pure logic khoi side effect
public boolean isVipOrder(Order o) {
return o.amount() > 10000; // pure function, testable khong mock
}
// Side effect chi o boundary (caller):
if (isVipOrder(order)) {
emailService.send(order.customerId(), "VIP order");
}
orderRepository.save(order);
8. Imperative accumulator vs functional reduce
So sánh trực tiếp 2 style cho cùng bài toán — tính tổng amount của order amount vượt 100:
// IMPERATIVE -- mutable counter, side effect
int total = 0;
for (Order o : orders) {
if (o.amount() > 100) {
total += o.amount(); // modify shared state mutable
}
}
// FUNCTIONAL -- pure reduction, khong shared state
int total = orders.stream()
.filter(o -> o.amount() > 100)
.mapToInt(Order::amount)
.sum(); // sum() la reduce associative: (a, b) -> a + b, identity = 0
Functional version: không có loop variable, không mutation trong loop, dễ parallel hóa (mapToInt(...).parallel().sum() hoạt động đúng vì sum là associative).
// Functional version neu can collect nhieu metric 1 lan:
record SalesStats(long count, int totalAmount) {}
SalesStats stats = orders.stream()
.filter(o -> o.amount() > 100)
.collect(Collector.of(
() -> new int[2], // [count, total]
(arr, o) -> { arr[0]++; arr[1] += o.amount(); },
(a, b) -> new int[]{a[0]+b[0], a[1]+b[1]},
arr -> new SalesStats(arr[0], arr[1])
));
9. Tradeoff — khi nào KHÔNG dùng immutability
Immutability không phải silver bullet. Mỗi "modification" tạo object mới → pressure lên garbage collector:
// Tao 1 trieu String moi -- memory pressure cao
String result = "";
for (int i = 0; i < 1_000_000; i++) {
result = result + i; // moi + tao String moi, cu bi GC
}
// DUNG StringBuilder -- mutable nhung local, khong shared
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1_000_000; i++) {
sb.append(i); // modify in-place, 1 object duy nhat
}
String result = sb.toString();
Khi nào immutability có tradeoff không đáng:
- Hot loop performance-critical với số lần lặp cao (vượt hàng triệu iteration).
- Large dataset incremental update — cập nhật liên tục 1 ô trong matrix 10000×10000.
- Low-latency systems (trading, gaming) nơi GC pause không được phép.
Nguyên tắc cân bằng: mặc định immutable tại API boundary và shared state. Cho phép mutable local variable bên trong method khi cần performance — mutable local không share ra ngoài không tạo race condition hay surprise.
10. Deep Dive
- Effective Java item 17 (Bloch, 3rd ed) — "Minimize mutability" — 5 quy tắc thiết kế immutable class, giải thích vì sao immutable object đơn giản hơn, thread-safe tự nhiên, và có thể share freely.
- Effective Java item 50 (Bloch, 3rd ed) — "Make defensive copies when needed" — khi nào cần copy, vì sao Date là ví dụ điển hình attack vector, và cách copy đúng theo từng kiểu.
- Effective Java item 18 (Bloch, 3rd ed) — "Favor composition over inheritance" — liên quan đến immutable design: class
finalhoặc có constructor private khuyến khích composition thay inheritance, giúp enforce immutability. - JEP 395 — Records (Java 16) — openjdk.org/jeps/395 — spec đầy đủ về record: semantic, compact constructor, pattern matching (Java 21+ preview), restriction (no inheritance, component final). Bắt buộc đọc trước khi dùng record cho domain model.
- JEP 8287163 — Value Classes (preview, ongoing) — openjdk.org/jeps/8287163 — hướng tiếp theo của record trong Project Valhalla: value type không có identity, stack-allocated, không có null. Mang Java gần hơn với immutable-first language như Kotlin/Scala.
11. Tổng kết module — 7 skills functional Java
Qua 8 bài Stream API và Lambda (bài 01-08), bạn đã tích lũy các kỹ năng sau:
Skill 1 — Đọc và viết lambda + functional interface
Hiểu @FunctionalInterface, viết lambda (args) -> expr, method reference Class::method, và biết khi nào lambda capture effectively-final variable.
Skill 2 — Xây dựng Stream pipeline idiomatic
Kết hợp filter, map, flatMap, reduce, collect, distinct, sorted, limit, skip, takeWhile, dropWhile đúng thứ tự. Biết stateful op nào phải để cuối. Không dùng peek cho logic chính.
Skill 3 — Chọn đúng Collector
toList vs toUnmodifiableList, toMap với merge function, groupingBy với downstream (counting, summingInt, mapping, filtering), partitioningBy, joining. Tự xây Collector.of khi cần.
Skill 4 — Dùng Optional đúng cách
Optional thay null return, chain map/flatMap/filter/orElse/orElseGet. Không get() không check, không Optional cho field trong class, không Optional cho parameter.
Skill 5 — Debug và quyết định parallel stream
Áp công thức N×Q > 10000 trước khi parallel. Biết 4 điều kiện: CPU-bound, no shared state, source splittable, no I/O-bound commonPool. Dùng CompletableFuture + executor riêng cho I/O concurrency.
Skill 6 — Thiết kế immutable value class
Dùng record (Java 16+) cho value object. Defensive copy với List.copyOf trong constructor. Nhận biết final reference != immutable content. Biết khi nào tradeoff immutability cho performance (local mutable OK).
Skill 7 — Tư duy functional trong Java
Pure function không side effect cho business logic. Side effect tập trung tại boundary. Optional thay null. Stream reduce thay mutable accumulator. Test pure function không cần mock. Functional style là về reasoning clarity, không phải "dùng stream cho có".
flowchart LR
A[Bài 01\nLambda &\nFunctional Interface] --> B[Bài 02\nStream Basics]
B --> C[Bài 03\nmap-filter-reduce]
C --> D[Bài 04\nStream nâng cao\nflatMap takeWhile]
D --> E[Bài 05\nOptional]
E --> F[Bài 06\nCollectors Deep]
F --> G[Bài 07\nParallel Stream]
G --> H[Bài 08\nImmutability &\nFunctional Style]
H --> I[Mini Challenge\nSales Report]12. Self-check
Q1final List<String> list = new ArrayList<>() — câu nào sau đây đúng? (1) `list.add("x")` compile error. (2) list = new ArrayList<>() compile error. (3) Cả hai compile error. (4) Cả hai đều OK.▸
final List<String> list = new ArrayList<>() — câu nào sau đây đúng? (1) `list.add("x")` compile error. (2) list = new ArrayList<>() compile error. (3) Cả hai compile error. (4) Cả hai đều OK.list = new ArrayList<>() là compile error vì cố gán lại biến `final`. Nhưng `list.add("x")` hoàn toàn hợp lệ vì `final` không liên quan đến việc gọi method trên object mà reference đang trỏ vào. `ArrayList` là mutable — `add`, `remove`, `clear` đều được dù reference là `final`. Để có immutable content, cần List.of(...) hoặc List.copyOf(...) thay vì new ArrayList<>(). Hiểu lầm này rất phổ biến — `final` ở Java chỉ lock reference, không lock state.Q2Viết immutable class `Address` có 3 field: `street` (String), `city` (String), `zipCode` (String). Không dùng record. Giải thích tại sao code của bạn đủ immutable.▸
public final class Address { private final String street; private final String city; private final String zipCode; public Address(String street, String city, String zipCode) { this.street = Objects.requireNonNull(street); this.city = Objects.requireNonNull(city); this.zipCode = Objects.requireNonNull(zipCode); } public String street() { return street; } public String city() { return city; } public String zipCode() { return zipCode; } } Giải thích: (1) class `final` — không thể extend, ngăn subclass override method để introduce mutability; (2) mọi field `private final` — không gán lại sau constructor; (3) không có setter; (4) String là immutable trong Java — không cần defensive copy cho String field; (5) getter trả String trực tiếp an toàn. Nếu có field là `List` hay `Date`, phải defensive copy trong constructor và getter. Với Java 16+, dùng record thay — `public record Address(String street, String city, String zipCode) ` tự động làm tất cả ở trên.Q3Phân biệt 3 cách tạo "immutable-ish" list: `Collections.unmodifiableList(source)`, `List.copyOf(source)`, và `List.of(...)`. Khi nào mỗi cách phù hợp?▸
Q4Vì sao record (Java 16+) là immutable theo mặc định? Record có thể chứa mutable component như `List` không, và nếu có thì có nguy hiểm không?▸
List<String>. Nếu viết record Order(List<String> tags) {} và không defensive copy, caller có thể: giữ reference list gốc và modify → `order.tags()` thay đổi; hoặc gọi order.tags().add("x") trực tiếp vì `tags()` trả reference thật. Record không tự động defensive copy. Giải pháp: dùng compact canonical constructor để copy: public Order { tags = List.copyOf(tags); }. Sau đó `tags()` trả unmodifiable snapshot — record thực sự immutable về value.Q5Khi nào nên dùng mutable local variable thay vì functional immutable approach?▸
Q6Pure function cho business logic vs side effect ở boundary — áp dụng refactor đoạn code sau: public void approveOrder(Order o) { if (o.amount() < 0) throw ex; o.setStatus("APPROVED"); db.save(o); email.send(o.customerId()); }▸
public void approveOrder(Order o) { if (o.amount() < 0) throw ex; o.setStatus("APPROVED"); db.save(o); email.send(o.customerId()); }public static void validateOrder(Order o) { if (o.amount() < 0) throw new IllegalArgumentException("amount phai >= 0"); } — pure function, testable không mock. (2) Pure transformation (nếu dùng immutable record): public static Order approve(Order o) { return new Order(o.id(), o.amount(), "APPROVED"); } — trả Order mới với status APPROVED, không modify input. (3) Side effect chỉ ở boundary (caller hay application service): `validateOrder(o); Order approved = approve(o); db.save(approved); email.send(approved.customerId());` Lợi ích: `validateOrder` và `approve` test bằng unit test thuần không cần mock DB hay email. Chỉ boundary layer (application service) cần integration test. Logic business tách biệt hoàn toàn khỏi infrastructure.Q7Module Stream API & Lambda đã trang bị 7 skills functional Java. Bạn có thể kể tên 5 trong 7 skills và giải thích khi nào dùng mỗi skill đó?▸
N×Q > 10000, CPU-bound, no shared state, source array-backed. (6) Immutable value class — khi thiết kế DTO, configuration, domain value object; dùng record Java 16+. (7) Functional tư duy — pure function cho business logic, side effect chỉ ở boundary, Optional thay null, Stream reduce thay mutable accumulator. Mỗi skill có điều kiện áp dụng riêng — không phải lúc nào cũng dùng tất cả, nhưng biết khi nào dùng là dấu hiệu developer Java mature.Bài tiếp theo: Mini Challenge — Sales Report
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên