Java — Từ Zero đến Senior/OOP cơ bản — class, object, encapsulation/Encapsulation — `private`, `public`, `protected` và lý do field nên `private`
4/7
~17 phútOOP cơ bản — class, object, encapsulation

Encapsulation — `private`, `public`, `protected` và lý do field nên `private`

Bốn access modifier của Java, nguyên tắc 'private by default, expose qua method', lợi ích cho maintenance + invariant, và ý nghĩa mới với record và JPMS.

Ở bài 01, bạn khai báo field không modifier — ai cũng truy xuất được. Với class đơn giản trong ví dụ thì OK, nhưng code production không làm thế. Tại sao? Vì field public đồng nghĩa mọi người đều sửa được — và sớm muộn ai đó sẽ đặt giá trị làm class ở trạng thái không hợp lệ.

Encapsulation (đóng gói) = giấu chi tiết bên trong, lộ ra giao diện có kiểm soát. Rule thực tế: field nên private, truy cập qua method. Bài này giải thích 4 access modifier, vì sao rule "private by default" đáng theo, và khi nào protected hay "package-private" có ý nghĩa.

1. Analogy — máy ATM

ATM không đưa ngăn chứa tiền ra cho bạn tự thò tay vào. Bạn chỉ tương tác qua màn hình/nút — rút 500K, chuyển khoản, xem số dư. Bên trong máy có đủ thứ cảm biến, khoá, két sắt — nhưng bạn không thấy, không sửa được. Cùng một tài khoản, mọi người tương tác qua cùng giao diện; ngân hàng có thể đổi công nghệ két mà không ảnh hưởng trải nghiệm.

Đời thườngJava
Ngăn tiền bên trong máy ATMField private
Màn hình/nútMethod public
Khoá két cảm biếnField private final + validation
Kỹ thuật viên có keyprotected / package-private

💡 💡 Cách nhớ

Encapsulation = private data + public interface. Class kiểm soát mọi lối vào/ra state của nó → class giữ invariant; người dùng không phá được trạng thái.

2. Bốn access modifier của Java

ModifierCùng classCùng packageSubclass khác packageBất cứ đâu
private
(không có) / package-private
protected✅ (qua kế thừa)
public

Áp dụng cho class, field, method, constructor. Bảng trên là rule chung.

2.1 private — chỉ chính class truy cập

public class Account {
    private double balance;

    public double getBalance() { return balance; }

    public void deposit(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("amount must be positive");
        balance += amount;
    }
}

Account a = new Account();
a.balance = -100;            // COMPILE ERROR — balance la private
a.deposit(-100);              // IllegalArgumentException tai runtime

Lợi ích rõ ràng: class kiểm soát mọi đường thay đổi balance — không ai set được số âm, không ai bypass validation.

2.2 Package-private (không modifier) — cùng package

package com.example.order;

class OrderLine {
    int quantity;   // khong modifier -> package-private
}

Truy cập được từ các class cùng package com.example.order, không được từ package khác. Đây là default nếu bạn quên gõ modifier. Dùng cho implementation detail chia sẻ trong một module nhỏ.

2.3 protected — package + subclass

package com.example.shape;

public class Shape {
    protected double area;
}
package com.example.shape;
class Circle extends Shape { ... }   // cung package -> thay area

package com.example.other;
public class Square extends Shape {
    void example() { area = 10.0; }    // subclass khac package, qua ke thua -> thay
}

protected rộng hơn package-private ở điểm: subclass khác package vẫn truy cập được — nhưng chỉ qua kế thừa, không qua reference khác:

Shape s = new Square();
s.area = 10;   // COMPILE ERROR — Square ke thua Shape, nhung truy cap qua reference kieu Shape thi tinh
               // "cross-package protected access" -> chi cho phep qua this hoac super

Rule chi tiết có trong JLS §6.6.2 — hiếm khi cần nhớ từng case, nhớ chung "protected = intended cho subclass".

2.4 public — khắp nơi

Mọi class, mọi package đều thấy. Dùng cho API của class: method muốn cho người ngoài gọi, class muốn cho người ngoài khởi tạo.

3. Rule thực tế — "private by default"

Khi khai báo một thành phần mới, bắt đầu từ mức hẹp nhất, chỉ mở rộng khi thực sự cần:

private  →  package-private  →  protected  →  public
(default)                                    (broadcast)

Lý do:

  1. Giảm bề mặt thay đổi — chỗ càng ít người dùng, sửa càng dễ.
  2. Bảo vệ invariant — class control hết mọi lối vào/ra state.
  3. Encapsulation cho phép refactor — đổi từ double balance sang long balanceCents không ảnh hưởng caller nếu chỉ lộ getBalance().
  4. Testing dễ hơn — public API ít → test case ít bề mặt cần phủ.

4. Getter / Setter — pattern chuẩn, có ngoại lệ

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

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public int getAge() { return age; }
    public void setAge(int age) {
        if (age < 0) throw new IllegalArgumentException("age must be non-negative");
        this.age = age;
    }
}

Pattern JavaBean: field private, lộ qua getX() / setX(). Nhiều framework (Spring, Jackson, JPA) dựa vào convention này qua reflection.

4.1 Khi nào KHÔNG cần setter?

  • Field immutable (không đổi sau khi construct): chỉ lộ getter.
    public class User {
        private final String id;   // final -> khong doi duoc
        public User(String id) { this.id = id; }
        public String getId() { return id; }
    }
    
  • Record (bài sau) tự sinh accessor, không có setter.

4.2 Khi nào KHÔNG cần getter?

  • Field là implementation detail người dùng không cần biết: private final Map<String, Object> cache.

Anti-pattern: sinh tự động cả getter và setter cho mọi field chỉ vì IDE bấm "Generate" — đây là anemic model. Nếu setter chỉ gán không validate, bạn vừa đánh mất lý do để field là private. Chọn có chủ đích.

⚠️ ⚠️ Setter là nơi validate, không chỉ gán

setAge nên từ chối âm, setEmail nên validate format. Nếu setter chỉ this.x = x, hỏi lại: có cần setter không? Hay nên để final + set trong constructor?

5. Encapsulation bảo vệ invariant — ví dụ thực tế

public class BankAccount {
    private double balance;
    private double overdraftLimit;

    public BankAccount(double overdraftLimit) {
        if (overdraftLimit < 0) throw new IllegalArgumentException(...);
        this.overdraftLimit = overdraftLimit;
    }

    public double getBalance() { return balance; }

    public void withdraw(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("amount must be positive");
        if (balance - amount < -overdraftLimit) {
            throw new InsufficientFundsException();
        }
        balance -= amount;
    }

    public void deposit(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("amount must be positive");
        balance += amount;
    }
}

Invariant (điều luôn đúng cho mọi instance): balance >= -overdraftLimit. Với field private + method validate, không ai phá được invariant. Nếu balance public, anyone có thể gán -1_000_000 phá luôn hợp đồng.

6. Cẩn thận với reference "private"

Giấu field chỉ giúp nếu reference không bị rò rỉ ra ngoài:

public class Cart {
    private List<Item> items = new ArrayList<>();

    public List<Item> getItems() { return items; }   // LO RA REFERENCE GOC!
}

Cart c = new Cart();
c.getItems().clear();   // ai cung xoa duoc items — pha encapsulation

getItems() trả reference nội bộ → bất kỳ ai gọi getItems() đều có thể add/remove/clear → không còn encapsulation.

Fix:

// Cach 1: tra ban copy
public List<Item> getItems() { return new ArrayList<>(items); }

// Cach 2: tra unmodifiable view (Java 9+)
public List<Item> getItems() { return Collections.unmodifiableList(items); }

// Cach 3 (tot nhat cho API moi): tra List.copyOf (immutable snapshot)
public List<Item> getItems() { return List.copyOf(items); }

List.copyOf trả immutable list — gọi .add ném UnsupportedOperationException. Đây là pattern khuyến nghị cho Java 10+.

7. Access modifier cho class

Modifier áp được cho top-level class:

  • public class Foo — dùng được khắp nơi.
  • class Foo (không modifier) — package-private, chỉ dùng trong package.
  • private class FooCOMPILE ERROR cho top-level. Chỉ cho inner class.
  • protected class FooCOMPILE ERROR cho top-level.

Inner class (class lồng trong class khác) mới có đủ 4 modifier.

8. JPMS — encapsulation cấp module (Java 9+)

Java 9 giới thiệu module system (JPMS). Một module khai báo trong module-info.java export những package nào; package không export thì class public trong đó vẫn không thấy được từ ngoài module.

// module-info.java
module com.example.myapp {
    exports com.example.myapp.api;       // export package nay
    // com.example.myapp.internal khong export -> an khoi ben ngoai
}

Đây là encapsulation strong hơn chỉ dùng public/private. Dự án lớn, library nghiêm túc thì đáng tìm hiểu. Tutorial ở đây dừng ở mức nhận diện — module Deep Dive sẽ có bài riêng.

9. Pitfall tổng hợp

Nhầm 1: Field public cho nhanh.

public String name;   // ai cung sua duoc

private + getter/setter (hoặc chỉ getter nếu immutable).

Nhầm 2: Setter chỉ gán mà không validate.

public void setAge(int age) { this.age = age; }   // nhan -5 luon, vo nghia

✅ Validate + throw exception cho input sai.

Nhầm 3: Trả reference mutable từ getter.

public List<Item> getItems() { return items; }

List.copyOf(items) hoặc Collections.unmodifiableList(items).

Nhầm 4: protected cho field của class không có subclass.

public class User {
    protected String email;   // tai sao protected? ai se ke thua?
}

private trừ khi có nhu cầu rõ ràng cho subclass.

Nhầm 5: "Generate getter/setter" cho mọi field bằng IDE rồi không suy nghĩ. ✅ Mỗi field hỏi: người ngoài có cần đọc? sửa? Field có nên final?

10. 📚 Deep Dive Oracle

ℹ️ 📚 Deep Dive Oracle (optional)

Spec / reference chính thức:

Ghi chú: JLS §6.6.2 về protected có lẽ là phần dễ rối nhất — rule "access từ outside package chỉ qua cùng subclass hierarchy" nhằm ngăn một subclass lộ field của class cha ra cho class không liên quan. Hiếm gặp trong code thường ngày, nhưng biết để debug khi "tại sao compile error với protected field".

11. Tóm tắt

  • 4 access modifier: private < package-private < protected < public.
  • Rule thực tế: bắt đầu private, mở rộng khi cần. Mỗi lần public là một cam kết API.
  • Field nên private, expose qua method (có validate) hoặc final + constructor cho immutable.
  • Getter/Setter không tự động cần có — xét từng field. Setter đúng nghĩa là nơi validate, không chỉ gán.
  • Trả collection từ getter → copy hoặc unmodifiable view → tránh rò rỉ reference.
  • protected dành cho subclass; dùng ít và có chủ đích.
  • JPMS (Java 9+) thêm tầng encapsulation ở cấp module.
  • Encapsulation cho phép refactor nội bộ mà không phá caller — đây là lý do kinh tế, không phải "best practice rỗng".

12. Tự kiểm tra

Tự kiểm tra
Q1
Đoạn sau có vấn đề gì dù tưởng "đã private"?
public class Cart {
    private List<Item> items = new ArrayList<>();
    public List<Item> getItems() { return items; }
}

Cart c = new Cart();
c.getItems().add(new Item(...));
c.getItems().clear();

Encapsulation bị phá. getItems() trả reference gốc đến items — ai gọi cũng có thể mutate list: thêm, xoá, clear. Class Cart mất kiểm soát nội dung của chính nó.

Fix (chọn 1):

  • return List.copyOf(items); — trả snapshot immutable (Java 10+).
  • return Collections.unmodifiableList(items); — trả view, cố mutate sẽ UnsupportedOperationException.
  • return new ArrayList<>(items); — trả bản copy mutable (người gọi mutate không ảnh hưởng gốc).

Đồng thời, thêm method kiểm soát thay đổi: addItem(Item i), removeItem(Item i) có validation — không cho "shove" bất kỳ gì vào list.

Q2
Khi nào dùng protected thay vì private?

Khi bạn có chủ đích cho phép subclass truy cập hoặc override:

  • Protected field: subclass cần đọc/ghi trực tiếp field của cha. Hiếm nên làm — phá encapsulation. Thường thay bằng protected getter/setter.
  • Protected method: hook cho subclass override (template method pattern). Vd AbstractListprotected void removeRange(int from, int to) — intended cho subclass custom.
  • Protected constructor: chỉ cho subclass khởi tạo — thường thấy ở abstract class.

Không dùng protected chỉ vì "biết đâu sau này có ai kế thừa". Mỗi protected cũng là cam kết API — subclass nào cũng có thể dựa vào. Refactor sau sẽ khó.

Q3
Đoạn sau compile không?
public class Outer {
    private class Helper { }
    private static class Builder { }
}

class Foo {
    private class Bar { }  // top-level class 'Foo' hay la inner class?
}

Cả đoạn đều compile.

  • Outer.HelperOuter.Builderinner class (khai báo trong class khác) — private hợp lệ cho inner.
  • Footop-level class (không nằm trong class khác) — không có modifier nên package-private. Foo.Bar là inner class của Foo, được phép private.

Rule: privateprotected không dùng được cho top-level class. Nếu đổi dòng đầu thành private class Outer {} → compile error.

Q4
Vì sao nhiều framework (Spring, Jackson) yêu cầu field private + setter public, thay vì đọc/ghi trực tiếp public field?

Hai lý do chính:

  • Validation hook: setter có thể chứa logic — validate, trigger event, audit log. Framework dựa vào đó để đảm bảo data đi vào object hợp lệ. Public field bypass hoàn toàn.
  • Refactor an toàn: public field khoá bạn vào format storage — đổi từ Date sang LocalDateTime phải đổi mọi caller. Với setter/getter, bạn đổi nội bộ, giữ chữ ký public, caller không biết.

Ngoại lệ hợp lý: public static final int MAX = 100 — constant, immutable, framework đồng ý. Hoặc record Point(int x, int y) {} — record có accessor tự sinh, framework hỗ trợ sẵn (Jackson 2.12+).

Q5
Đoạn sau có lỗ hổng encapsulation gì không?
public class Config {
    public static final Map<String, String> SETTINGS = new HashMap<>();
}

Có — và là lỗi nguy hiểm. public static final chỉ đảm bảo reference SETTINGS không gán lại. Nhưng nội dung Map vẫn mutable — bất kỳ ai cũng Config.SETTINGS.put("key", "attacker-value") hoặc .clear() được.

Fix (chọn 1):

  • Map.of("k1", "v1", "k2", "v2") — immutable map built-in (Java 9+), tối đa 10 entry dạng này.
  • Map.copyOf(mutableMap) — wrap một map mutable thành immutable snapshot.
  • Collections.unmodifiableMap(new HashMap<>(...)) — cũ hơn, tương đương.

Bài học: final chỉ ngăn re-assign, không ngăn mutation nội bộ. Với collection dùng chung, luôn wrap immutable.


Bài tiếp theo: toString, equals, hashCode — ba method phải override đúng