1. Tổng quan về bộ sửa đổi truy cập
Trong Java có bốn mức độ truy cập dành cho lớp, trường, phương thức và constructor:
| Bộ sửa đổi | Truy cập ở đâu? |
|---|---|
|
Khắp nơi: bên trong lớp, trong các lớp khác, trong các package khác |
|
Bên trong lớp, trong các lớp con (kế thừa), trong các lớp cùng một package |
|
Chỉ bên trong package (nếu không chỉ định bộ sửa đổi rõ ràng) |
|
Chỉ bên trong lớp hiện tại |
Hãy cùng tìm hiểu chi tiết từng loại — kèm ví dụ, chút hài hước và vài khúc ngoặt bất ngờ.
public — truy cập công khai
public giống như một thông báo cho cả thế giới: “Mọi người đều được vào!”. Nếu một lớp, trường, phương thức hoặc constructor được khai báo là public, bạn có thể truy cập nó từ bất kỳ lớp nào khác, kể cả từ package khác.
Ví dụ:
public class Cat {
public String name;
public void sayMeow() {
System.out.println("Meo!");
}
}
Có thể truy cập lớp này và các trường/phương thức của nó từ bất cứ đâu. Điều này tiện lợi nếu bạn muốn lớp của mình khả dụng cho tất cả: chẳng hạn khi bạn viết thư viện.
Nhưng! Trường public không phải lúc nào cũng tốt (xem bài trước). Thông thường chỉ các phương thức cần truy cập từ bên ngoài mới để public, còn các trường gần như luôn để private.
private — chỉ truy cập trong nội bộ lớp
private giống như két sắt có mã: không ai ngoài chính lớp đó có thể truy cập các thành viên này. Ngay cả lớp con (kế thừa) cũng không nhìn thấy các trường và phương thức private!
Ví dụ:
public class Cat {
private String secretName;
public void setSecretName(String name) {
secretName = name;
}
public String getSecretName() {
return secretName;
}
}
Ở đây secretName không thể đọc hoặc thay đổi trực tiếp từ lớp khác. Chỉ chính Cat (hoặc các phương thức của nó) mới làm được điều đó. Đây là nền tảng của đóng gói: ta ẩn chi tiết bên trong và chỉ cung cấp quyền truy cập thông qua các phương thức.
protected — truy cập được bảo vệ
protected giống như thẻ VIP: lớp, các lớp con của nó (kể cả ở package khác) và tất cả các lớp trong cùng package hiện tại đều được phép truy cập.
Ví dụ:
public class Animal {
protected int age;
protected void growOlder() {
age++;
}
}
Bây giờ bất kỳ lớp nào kế thừa Animal đều có thể truy cập trường age và phương thức growOlder().
public class Cat extends Animal {
public void haveBirthday() {
growOlder();
System.out.println("Con mèo đã tròn " + age + " tuổi!");
}
}
Ngoài ra, tất cả các lớp trong cùng package cũng có quyền truy cập đến các thành viên protected.
(package-private) — truy cập trong nội bộ package
Nếu bạn không chỉ định bộ sửa đổi truy cập, thành viên của lớp sẽ được coi là package-private (hay “truy cập mặc định”). Giống như cánh cửa không khóa nhưng chỉ dành cho người nhà: chỉ các lớp trong cùng package mới được phép.
Ví dụ:
class Dog {
String name; // package-private
void bark() { // package-private
System.out.println("Gâu!");
}
}
Lớp Dog, trường name và phương thức bark() chỉ truy cập được trong cùng package. Cố gắng truy cập từ package khác sẽ gây lỗi biên dịch.
2. Áp dụng bộ sửa đổi cho trường và phương thức
Tại sao trường gần như luôn để private
Các trường của lớp chính là trạng thái nội tại của nó. Nếu để public, bất kỳ mã bên ngoài nào cũng có thể thay đổi chúng bất cứ lúc nào. Giống như bạn cho phép những đứa trẻ lạ chơi đồ nội thất nhà bạn như Lego: một ngày bạn thức dậy thấy tủ lạnh ở trong phòng tắm và bị lật ngược.
Ví dụ về đóng gói tệ:
public class Person {
public String name;
public int age;
}
Ví dụ về đóng gói tốt:
public class Person {
private String name;
private int age;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
Các phương thức (getter và setter) cho phép kiểm soát cách các lớp bên ngoài có thể thay đổi trường. Ví dụ, bạn có thể cấm tuổi âm.
Khi nào nên để phương thức public, protected hoặc package-private
- public — nếu phương thức cần được tất cả sử dụng. Thường là chức năng chính của lớp.
- protected — nếu phương thức chỉ cần cho lớp con hoặc trong nội bộ package (ví dụ các phương thức hỗ trợ có thể hữu ích trong lớp con).
- package-private — nếu phương thức chỉ cần trong nội bộ package, không nên lộ ra ngoài (ví dụ chi tiết hiện thực).
- private — nếu phương thức chỉ được dùng bên trong chính lớp (ví dụ các phương thức tiện ích cho logic nội bộ).
Ví dụ:
public class BankAccount {
private double balance;
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
protected void applyInterest() {
balance *= 1.05;
}
void internalAudit() {
// package-private: chỉ cho các lớp trong cùng gói
}
private void logAction(String action) {
// chỉ dùng cho nhu cầu nội bộ của lớp
}
}
3. Ví dụ mã: lớp với các mức độ truy cập khác nhau
Hãy tạo một lớp có đủ mọi thứ: thành viên public, private, protected và package-private. Đồng thời thử truy cập chúng từ các lớp khác và xem kết quả.
package zoo;
public class Animal {
public String publicName = "Truy cập được từ mọi nơi";
protected String protectedName = "Chỉ dành cho lớp con và cùng gói";
String packageName = "Chỉ dành cho gói";
private String privateName = "Chỉ dành cho Animal";
public void publicMethod() {
System.out.println("Phương thức public");
}
protected void protectedMethod() {
System.out.println("Phương thức protected");
}
void packageMethod() {
System.out.println("Phương thức package-private");
}
private void privateMethod() {
System.out.println("Phương thức private");
}
}
Bây giờ thử truy cập các thành viên này từ một lớp khác trong cùng package:
package zoo;
public class Main {
public static void main(String[] args) {
Animal animal = new Animal();
System.out.println(animal.publicName); // OK
System.out.println(animal.protectedName); // OK
System.out.println(animal.packageName); // OK
System.out.println(animal.privateName); // Lỗi: private
animal.publicMethod(); // OK
animal.protectedMethod(); // OK
animal.packageMethod(); // OK
animal.privateMethod(); // Lỗi: private
}
}
Và bây giờ thử truy cập các thành viên này từ package khác:
package other;
import zoo.Animal;
public class Test {
public static void main(String[] args) {
Animal animal = new Animal();
System.out.println(animal.publicName); // OK
System.out.println(animal.protectedName); // Lỗi: protected
System.out.println(animal.packageName); // Lỗi: package-private
System.out.println(animal.privateName); // Lỗi: private
animal.publicMethod(); // OK
animal.protectedMethod(); // Lỗi: protected
animal.packageMethod(); // Lỗi: package-private
animal.privateMethod(); // Lỗi: private
}
}
Kết luận:
- public — truy cập ở mọi nơi.
- protected — truy cập trong package và trong lớp con (kể cả từ package khác nếu thông qua kế thừa).
- package-private — chỉ trong nội bộ package.
- private — chỉ trong nội bộ lớp.
4. Best practices: cách chọn bộ sửa đổi truy cập
Giảm thiểu phạm vi hiển thị
Càng ít mã nhìn thấy trường hay phương thức của bạn càng tốt. Chỉ mở ra những gì thực sự cần cho mã bên ngoài. Điều này gọi là nguyên tắc đặc quyền tối thiểu (principle of least privilege).
- Các trường gần như luôn phải là private. Ngoại lệ — chỉ dành cho hằng số thực sự (public static final), nhưng sẽ nói kỹ hơn ở các bài sau.
- Các phương thức để public chỉ khi chúng là một phần giao diện bên ngoài của lớp.
- Các phương thức hỗ trợ (logic nội bộ) — private.
- Các phương thức dành cho lớp con — protected.
- Các phương thức dịch vụ nội bộ cho package — package-private.
Vì sao điều này quan trọng?
- Bất kỳ thay đổi nào trong chi tiết hiện thực đều có thể làm hỏng mã của người khác nếu bạn phơi bày các chi tiết đó.
- Lớp trở nên khó kiểm thử và bảo trì hơn.
- Lỗi ngẫu nhiên (ví dụ thay đổi trường không đúng) có thể dẫn đến bug.
Đôi khi người mới nghĩ: “Tại sao phải phức tạp, cứ để tất cả đều public!” Nhưng rồi khi dự án phình to, họ phải viết lại nửa chương trình chỉ vì ai đó đã thay đổi trực tiếp các trường của lớp.
5. Các lỗi thường gặp khi làm việc với bộ sửa đổi truy cập
Lỗi số 1: Để trường public hoặc package-private theo mặc định.
Nếu không chỉ định bộ sửa đổi, trường hay phương thức sẽ truy cập được với tất cả các lớp trong package. Điều này có thể dẫn tới tình huống bất ngờ nếu ai đó bắt đầu thay đổi trực tiếp các trường của bạn.
Lỗi số 2: Cố gắng truy cập thành viên private từ lớp khác.
Trình biên dịch sẽ không cho phép — bạn sẽ nhận lỗi. Và nếu bạn cố lách qua reflection (phản chiếu) — hãy sẵn sàng bước vào thế giới bug và những cú sập bất ngờ.
Lỗi số 3: Quá nhiều public.
Nếu khai báo public tràn lan, lớp của bạn sẽ giống một hộp dây điện mở toang — ai cũng có thể kéo nhầm sợi dây và làm hỏng mọi thứ.
Lỗi số 4: Không dùng protected cho các phương thức chỉ cần cho lớp con.
Nếu phương thức chỉ để mở rộng trong lớp con, hãy khai báo protected, không phải public.
Lỗi số 5: Phạm vi package-private không rõ ràng.
Đôi khi quên chỉ định bộ sửa đổi, và phương thức trở nên truy cập được với toàn bộ package. Điều này có thể gây bất ngờ nếu bạn nghĩ rằng nó là private.
GO TO FULL VERSION