Access modifier, static, final và pass-by-value

4 câu hỏi~25 phútTừ khóa: public, protected, package-private, private, static, static nested class, static init, final, immutable, pass-by-value, reference

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 đề

3. Giải pháp

ModifierCùng classCùng packageClass 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.

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ĩaVí dụ
Biến / fieldGán đúng 1 lầnprivate final String id
MethodKhông overridetemplate method pay() ở bài 1.2
ClassKhông extendsString, VatRates

7. Khi nào nên dùng / không nên dù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:

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
}

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:

static thuộ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:

static members 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. final chỉ 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. final only 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


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.