Tính đóng gói trong Java: private, public và protected
Bốn access modifier trong Java và nguyên tắc đóng gói encapsulation. Bảo vệ invariant dữ liệu, thiết kế getter setter chuẩn và tránh lộ reference.
TL;DR: Encapsulation (đóng gói) là nguyên lý giấu kín dữ liệu nội bộ (private data) và chỉ cung cấp các phương thức truy cập có kiểm soát (public interface). Bốn mức truy cập gồm private, package-private, protected và public. Luôn tuân thủ nguyên tắc "private by default" để bảo vệ tính bất biến (invariant) và tránh rò rỉ tham chiếu mutable collection qua getter. Ở bài Constructor và this, ta đã thấy cách khởi tạo an toàn bằng việc ném ngoại lệ khi dữ liệu không hợp lệ.
Ở bài học về Class và object, bạn khai báo field không modifier nên code bên ngoài có thể can thiệp tùy ý. Trong môi trường thực tế, field public đồng nghĩa ai cũng sửa được, dẫn tới nguy cơ dữ liệu rơi vào trạng thái sai lệch.
Encapsulation (đóng gói) nghĩa là giấu chi tiết cài đặt bên trong và chỉ cung cấp giao diện truy cập có kiểm soát. Bài này phân tích 4 mức độ truy cập, lý do nên ưu tiên "private by default", và cách thức bảo vệ dữ liệu nội bộ.
1. Máy ATM minh họa cho nguyên lý đóng gói encapsulation ra sao?
Cây ATM không để lộ ngăn chứa tiền ra ngoài cho bạn tự lấy. Bạn chỉ tương tác qua màn hình và bàn phím: rút tiền, chuyển khoản hoặc xem số dư. Toàn bộ két sắt, cảm biến và mã khóa bên trong đều được bảo vệ kín đáo.
| Đời thường | Java |
|---|---|
| Ngăn tiền bên trong máy ATM | Field private |
| Màn hình và nút bấm | Method public |
| Khóa két cảm biến | Field private final và validation |
| Kỹ thuật viên có chìa khóa | protected hoặc package-private |
Encapsulation = private data + public interface. Class kiểm soát mọi lối vào/ra dữ liệu của nó để duy trì trạng thái toàn vẹn (invariant).
2. Bốn mức độ truy cập (access modifier) trong Java hoạt động như thế nào?
| Modifier | Cùng class | Cùng package | Subclass khác package | Bấ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. Vì sao nguyên tắc private by default là kim chỉ nam trong thiết kế?
Khi khai báo một thành phần mới, hãy 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:
- Giảm diện tích thay đổi: Thành phần càng ít bên phụ thuộc thì càng dễ sửa đổi nội bộ.
- Bảo vệ tính toàn vẹn (invariant): Class toàn quyền kiểm soát mọi cổng vào và ra của state.
- Thuận lợi cho việc tái cấu trúc (refactor): Chuyển đổi kiểu dữ liệu nội bộ không ảnh hưởng tới caller nếu giữ nguyên phương thức public.
- Viết test dễ dàng hơn: Bề mặt API public tinh gọn giúp giảm số lượng test case cần bao phủ.
Thử ngẫmnhìn lại một class bạn viết gần đây: field hay method nào đang để public chỉ vì lúc viết bạn ngại suy nghĩ, chứ không thật sự có ai bên ngoài cần dùng?
4. Getter và setter nên được thiết kế như thế nào để tránh anemic model?
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;
}
}
Mô hình JavaBean sử dụng field private và cung cấp getX() / setX(). Nhiều framework dựa vào quy ước này qua reflection.
4.1 Khi nào KHÔNG cần setter?
- Field immutable (không đổi sau khi khởi tạo): chỉ cung cấp getter.
Java
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 đọc dữ liệu, hoàn toàn không có setter.
4.2 Khi nào KHÔNG cần getter?
- Field là chi tiết nội bộ mà bên ngoài không cần biết: ví dụ
private final Map<String, Object> cache.
Anti-pattern: Sinh tự động cả getter và setter cho mọi field bằng IDE dẫn đến anemic model (mô hình thiếu máu). Nếu setter chỉ gán thuần túy mà không validate, bạn đã vô tình đánh mất ý nghĩa của tính đóng gói.
5. Encapsulation bảo vệ tính bất biến (invariant) của đối tượng ra sao?
public class BankAccount {
private double balance;
private double overdraftLimit;
public BankAccount(double overdraftLimit) {
if (overdraftLimit < 0) throw new IllegalArgumentException("Invalid limit");
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 kiện luôn đúng cho mọi instance): balance >= -overdraftLimit. Nhờ field private và các method có validation, không ai bên ngoài có thể can thiệp phá vỡ điều kiện này.
6. Cạm bẫy rò rỉ tham chiếu từ getter private phòng tránh thế nào?
Giấu field chỉ có tác dụng 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(); // ben ngoai co the xoa sach items, pha vo dong goi
Phương thức getItems() trả về chính reference nội bộ, cho phép bất kỳ ai can thiệp sửa đổi trực tiếp dữ liệu của Cart.
Cách khắc phục:
// Cach 1: tra ban copy
public List<Item> getItems() { return new ArrayList<>(items); }
// Cach 2: tra unmodifiable view
public List<Item> getItems() { return Collections.unmodifiableList(items); }
// Cach 3 (khuyen nghi Java 10+): tra snapshot bat bien
public List<Item> getItems() { return List.copyOf(items); }
7. Quy tắc áp dụng access modifier cho class trong Java là gì?
Modifier áp dụng cho top-level class:
public class Foo: truy cập được từ mọi nơi.class Foo(không modifier): package-private, chỉ dùng trong cùng package.private class Foohoặcprotected class Foo: COMPILE ERROR cho top-level class (chỉ hợp lệ với inner class).
8. JPMS tăng cường mức độ đóng gói ở cấp module như thế nào?
Từ Java 9, hệ thống module JPMS cho phép khai báo trong module-info.java chỉ định package nào được export ra ngoài. Một class dù là public nhưng nằm trong package không được export thì vẫn hoàn toàn vô hình đối với bên ngoài module.
module com.example.myapp {
exports com.example.myapp.api; // chi export package api
}
9. Những sai lầm kinh điển khi triển khai tính đóng gói là gì?
❌ Nhầm 1: Khai báo field public để truy cập cho tiện.
public String name;
✅ Đổi thành private và cung cấp getter/setter có kiểm soát.
❌ Nhầm 2: Setter chỉ gán thuần túy mà không kiểm tra tính hợp lệ.
public void setAge(int age) { this.age = age; } // chap nhan ca tuoi am
✅ Thẩm định dữ liệu đầu vào và ném exception nếu vi phạm.
❌ Nhầm 3: Trả về reference của mutable collection trong getter.
public List<Item> getItems() { return items; }
✅ Sử dụng List.copyOf(items) hoặc Collections.unmodifiableList(items).
❌ Nhầm 4: Dùng protected bừa bãi khi không có ý định kế thừa.
public class User { protected String email; }
✅ Luôn dùng private trừ khi có yêu cầu kiến trúc cụ thể cho lớp con.
❌ Nhầm 5: Tự động sinh getter/setter cho mọi field mà không cân nhắc tính bất biến.
✅ Đánh giá xem field có nên là final và chỉ khởi tạo trong constructor hay không.
10. Oracle JLS quy định chi tiết về kiểm soát truy cập ra sao?
Spec / reference chính thức:
- JLS §6.6 — Access Control: quy tắc chi tiết cho 4 mức truy cập.
- JLS §6.6.2 — Details on protected Access: cơ chế truy cập protected ngoài package qua kế thừa.
- JLS §7.7 — Module Declarations: cơ chế đóng gói mạnh mẽ cấp module của JPMS.
- JEP 260 — Encapsulate Most Internal APIs: tăng cường tính đóng gói cho các API nội bộ JDK.
- Oracle Guide: Controlling Access to Members: bản hướng dẫn chính thức.
Ghi chú: JLS §6.6.2 về protected quy định quyền truy cập ngoài package chỉ được thực thi thông qua kế thừa.
11. Những điểm cốt lõi nào cần ghi nhớ về tính đóng gói?
- Bốn mức độ truy cập:
private(nội bộ class), package-private (cùng package),protected(package + subclass),public(toàn cầu). - Áp dụng nguyên tắc "private by default": luôn bắt đầu từ phạm vi hẹp nhất và chỉ mở rộng khi thực sự cần cam kết API.
- Encapsulation bảo vệ tính bất biến (invariant) của đối tượng thông qua việc kiểm tra và chặn dữ liệu sai tại constructor/setter.
- Luôn trả về bản sao hoặc unmodifiable view đối với mutable collection để tránh lộ tham chiếu nội bộ ra ngoài.
12. Tự kiểm tra mức độ nắm vững encapsulation và access modifier
- 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(); - Q2Khi nào dùng
protectedthay vìprivate? - 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? } - Q4Vì sao nhiều framework (Spring, Jackson) yêu cầu field private + setter public, thay vì đọc/ghi trực tiếp public field?
- Q5Đoạn sau có lỗ hổng encapsulation gì không?
public class Config { public static final Map<String, String> SETTINGS = new HashMap<>(); }
Bài tiếp theo: toString, equals, hashCode — ba method phải override đúng
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.
Field, local và static trong Java: phân biệt ba vòng đời biến
Bài tiếpBộ ba toString, equals và hashCode: ghi đè chuẩn
Lộ trình Junior Java Backend Developer
Chặng 3/21 · OOP: class, kế thừa, đa hình, abstraction — bài 4/16 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này