Access modifier, static, final và pass-by-value
Tiếp đi: access modifier, static và final.
shop-backend có class Order bị sửa lung tung: service nào cũng set status, có biến đếm dùng chung bị sai. Bài này dùng 3 công cụ để "khóa" code: access modifier (ai thấy), static (thuộc về ai), final (có đổi được không).
1. Bài toán: Order ai cũng sửa được
public class Order {
public String id;
public OrderStatus status;
public List<OrderLine> lines;
public static int count; // đếm đơn, nhưng là static...
public Order() { count++; }
}
order.status = OrderStatus.SHIPPED; // bỏ qua bước PAID
order.lines = null; // NPE ở chỗ khác
2. Vấn đề
- Field
public: không có chỗ nào kiểm tra rule chuyển trạng thái. static int countdùng chung mọi request, nhiều thread++cùng lúc sai số, và mất khi restart.idkhông nên đổi sau khi tạo nhưng không có gì ngăn.
3. Giải pháp
| Modifier | Cùng class | Cùng package | Class con (khác package) | Mọi nơi |
|---|---|---|---|---|
private | ✅ | ❌ | ❌ | ❌ |
| (default / package-private) | ✅ | ✅ | ❌ | ❌ |
protected | ✅ | ✅ | ✅ | ❌ |
public | ✅ | ✅ | ✅ | ✅ |
Vì sao protected rộng hơn default và khi nào cần nó đã giải thích ở OOP & SOLID. Ở đây tập trung vào cách áp dụng.
- static: thuộc về class, không thuộc object. Một bản duy nhất cho cả JVM (chính xác là mỗi ClassLoader).
- final: biến gán một lần, method không override được, class không extends được.
4. Code từng bước
Bước 1: Đóng field, mở hành vi
public class Order {
private final String id; // final: gán 1 lần trong constructor
private OrderStatus status = OrderStatus.NEW;
private final List<OrderLine> lines = new ArrayList<>();
public Order(String id) { this.id = Objects.requireNonNull(id); }
public void markPaid() { // rule nằm trong class
if (status != OrderStatus.NEW) throw new IllegalStateException("Order " + id + " is " + status);
status = OrderStatus.PAID;
}
public List<OrderLine> getLines() { return List.copyOf(lines); } // không lộ list nội bộ
void attachInvoice(Invoice inv) { ... } // package-private: chỉ service cùng package
}
Bước 2: static cho hằng số và factory, không cho state
public class Order {
public static final BigDecimal FREE_SHIP_THRESHOLD = new BigDecimal("500000"); // hằng số
private static final Logger log = LoggerFactory.getLogger(Order.class);
public static Order create(String customerId) { // static factory method
return new Order("ORD-" + UUID.randomUUID());
}
}
// Đếm đơn: dùng DB (count query) hoặc AtomicLong trong một bean, không dùng static int
Bước 3: static nested class vs inner class
public class Order {
public static class Builder { ... } // không cần Order nào tồn tại: new Order.Builder()
public class LineView { ... } // inner: giữ tham chiếu ẩn tới Order.this
}
Order.Builder b = new Order.Builder();
Order.LineView v = order.new LineView();
Mặc định chọn static nested: không giữ tham chiếu ẩn tới object ngoài, tránh leak bộ nhớ.
Bước 4: static initializer
public final class VatRates {
private static final Map<String, BigDecimal> RATES;
static { // chạy 1 lần khi class được load
Map<String, BigDecimal> m = new HashMap<>();
m.put("FOOD", new BigDecimal("0.05"));
m.put("ELECTRONICS", new BigDecimal("0.10"));
RATES = Map.copyOf(m);
}
private VatRates() {} // utility class: chặn new
}
5. Cách hoạt động
Heap Metaspace / class data
┌───────────── Order@1 ─┐ ┌──── class Order ─────────────┐
│ id = "ORD-1" (final) │──instance──▶│ static FREE_SHIP_THRESHOLD │
│ status = PAID │ │ static log │
└───────────────────────┘ │ static { } chạy 1 lần khi load│
┌───────────── Order@2 ─┐ └──────────────────────────────┘
│ id = "ORD-2" │──instance──▶ (cả 2 object dùng chung)
└───────────────────────┘
6. So sánh: final ở 3 chỗ
| Đặt ở | Ý nghĩa | Ví dụ |
|---|---|---|
| Biến / field | Gán đúng 1 lần | private final String id |
| Method | Không override | template method pay() ở bài 1.2 |
| Class | Không extends | String, VatRates |
7. Khi nào nên dùng / không nên dùng
- Mặc định
private, mở dần khi cần. Field gần như luônprivate. static: hằng số, util không state, factory method, nested class. Không dùng static cho state thay đổi (cache, counter) trong ứng dụng Spring; để bean singleton quản lý.final: field inject qua constructor, biến local dùng trong lambda, class không thiết kế để kế thừa.
lines đã final rồi, vậy là Order không đổi được nữa phải không?private final List<OrderLine> lines là final rồi, sao mình vẫn lines.add(...) được? Final không phải là immutable à?
Bạn hiểu gần đúng, chỉ cần chỉnh một điểm: final khóa biến tham chiếu, không khóa object mà nó trỏ tới.
private final List<OrderLine> lines = new ArrayList<>();
lines.add(line); // ✅ sửa nội dung object
lines = new ArrayList<>(); // ❌ compile error: gán lại biến
lines ──(khóa mũi tên)──▶ [ArrayList: line1, line2, ...] ← nội dung vẫn đổi được
Muốn immutable thật phải đủ các điều kiện:
- Class
final(không ai extends rồi thêm setter). - Mọi field
private final, không setter. - Field mutable (List, Date) thì copy khi nhận vào và khi trả ra (
List.copyOf).
public record Money(BigDecimal amount, String currency) {} // record: field final, nhưng
public record Cart(List<OrderLine> lines) {
public Cart { lines = List.copyOf(lines); } // vẫn phải tự copy list
}
Order vào method rồi sửa thì bên ngoài có thấy không? Java truyền tham chiếu à?Mình truyền order vào applyDiscount(order), bên trong sửa total thì ngoài thấy đổi. Vậy Java là pass-by-reference phải không?
Không, Java luôn pass-by-value. Với object, "value" được copy là giá trị của tham chiếu (địa chỉ). Hai biến cùng trỏ tới một object, nên sửa object thì thấy; nhưng gán biến mới thì không ảnh hưởng bên ngoài.
void applyDiscount(Order o) {
o.setTotal(o.getTotal().multiply(new BigDecimal("0.9"))); // ① sửa object chung
o = new Order("ORD-NEW"); // ② chỉ đổi bản copy
o.setTotal(BigDecimal.ZERO);
}
Order order = new Order("ORD-1"); // total = 100
applyDiscount(order);
order.getId(); // "ORD-1" → ② không ảnh hưởng
order.getTotal(); // 90 → ① có ảnh hưởng
caller: order ─┐
├──▶ Order@1 (ORD-1, total 100 → 90)
method: o ─────┘
sau ②: o ───────▶ Order@2 (ORD-NEW, total 0) // order vẫn trỏ Order@1
Nếu là pass-by-reference thật (như ref trong C#), dòng ② sẽ làm order bên ngoài trỏ sang object mới. Một hệ quả: không viết được hàm swap(a, b) đổi hai biến của caller trong Java.
Phỏng vấn hỏi static, final, pass-by-value thì trả lời sao?
Câu 1: static dùng khi nào?
🇻🇳 Tiếng Việt:
staticthuộc về class, một bản dùng chung, gọi không cần object. Em dùng cho hằng số, utility method không state, static factory, static nested class như Builder. Tránh static cho state thay đổi vì dùng chung giữa các thread, khó test, khó mock.
🇬🇧 English:
staticmembers belong to the class, are shared, and need no instance. I use them for constants, stateless utility methods, static factories and static nested classes like a Builder. I avoid static mutable state because it is shared across threads and hard to test or mock.
Câu 2: final có làm object immutable không?
🇻🇳 Tiếng Việt:
Không.
finalchỉ cấm gán lại biến; object nó trỏ tới vẫn sửa được. Muốn immutable cần class final, field private final, không setter và copy phòng thủ các field mutable.
🇬🇧 English:
No.
finalonly prevents reassigning the variable; the object it points to can still change. Immutability needs a final class, private final fields, no setters, and defensive copies of mutable fields.
Câu 3: Java pass-by-value hay pass-by-reference?
🇻🇳 Tiếng Việt:
Luôn pass-by-value. Với object, giá trị được copy là tham chiếu, nên method sửa được nội dung object nhưng gán tham số sang object khác thì caller không bị ảnh hưởng.
🇬🇧 English:
Always pass-by-value. For objects the value copied is the reference, so a method can mutate the object, but reassigning the parameter does not affect the caller's variable.
Cách nhớ ngắn gọn
- private < default < protected < public.
- static = của class; static nested không giữ
Outer.this. - final khóa mũi tên, không khóa object.
- Java copy giá trị; với object là copy địa chỉ.
Tự kiểm tra
Vì sao không nên dùng static int count để đếm đơn trong Spring app?
Dùng chung mọi thread nên ++ bị race condition, mất khi restart, không đúng khi chạy nhiều instance.
Static nested class khác inner class chỗ nào?
Inner class giữ tham chiếu ẩn tới object ngoài và cần object đó để tạo; static nested thì không.
Static block chạy khi nào?
Một lần khi class được load và khởi tạo, trước mọi constructor.
final List có add được không?
Được. final chỉ cấm gán lại biến, không cấm sửa object.
Trong method gán o = new Order() thì caller có thấy không?
Không. Chỉ bản copy của tham chiếu bị đổi.