1. 引言
在 Java 中,访问修饰符就像房子里的锁系统。它们决定了谁、从哪里可以“进入”你的房间(或者访问你类的字段/方法)。如果所有的门都开着——任何人都可以进来改点什么。如果全都锁上——没人能破坏什么,但你自己某个时候也可能被困住。
提醒一下,在 Java 中有四个主要的访问级别:
| 修饰符 | 类内部可见 | 包内可见 | 子类可见 | 其他包可见 |
|---|---|---|---|---|
|
✔ | |||
| (package) | ✔ | ✔ | ||
|
✔ | ✔ | ✔ | (通过继承) |
|
✔ | ✔ | ✔ | ✔ |
(package)——指未显式声明修饰符。这样的类成员只在同一个包内可见。
2. 访问修饰符的常见错误
错误 1:使用默认修饰符的字段和方法(package-private)
最常见的新手错误——忘记写访问修饰符。结果字段或方法在整个包内都可见,而你本来并不打算这样。这可能导致同一包中与您的类无关的其他类修改你的对象内部状态。
// 错误:字段 name 未受保护!
class User {
String name; // package-private!
}
于是,同一包中的任何类都可以这样写:
User user = new User();
user.name = "John"; // 没有限制!
错误 2:破坏封装——将字段设为 public
第二常见的错误——把类的字段声明为 public。学习或写小示例时这样做看似方便,但在真实项目中几乎总是不妥。你会失去对于谁在如何修改你数据的控制。
public class Account {
public double balance; // 危险!
}
于是任何代码都可以这样做:
Account acc = new Account();
acc.balance = -1000000; // 现在谁该负责?
错误 3:缺少 getter 和 setter
有时开发者把字段设为 private,却忘了添加相应的访问方法。结果在一些本应合理的场景下也无法获取或修改它们的值。
public class Product {
private String name;
// 既没有 getName(),也没有 setName()
}
错误 4:从其他类访问 private 成员
如果你把字段或方法声明为 private,就不能从其他类访问它,即使在同一个包中也是如此。新手常常纳闷为什么“看不到”这个字段。
public class User {
private String password;
}
public class UserService {
public void resetPassword(User user) {
// user.password = "123"; // 编译错误!
}
}
错误 5:对 protected 的误解
很多人认为 protected 意味着“凡是有继承的地方都能看到”。但在 Java 中,包外对 protected 成员的访问只能通过继承,且仅限子类。这一细节很容易被忽略。
package animals;
public class Animal {
protected void sleep() {}
}
package zoo;
import animals.Animal;
public class Dog extends Animal {
public void test() {
sleep(); // OK——子类
}
}
public class NotADog {
public void test() {
Animal a = new Animal();
// a.sleep(); // 错误:不是子类!
}
}
3. 正确做法:最佳实践
规则 1:默认将字段设为 private
这是封装的核心原则。字段应对除本类以外的所有人隐藏。若需要提供访问——使用 getter/setter。
public class Book {
private String title;
private int pages;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
规则 2:只公开必要的方法
若方法需要对外可见——将其设为 public。若只在包内需要——保持为 package-private。若仅供子类使用——使用 protected。
规则 3:尽量缩小可见范围
可见范围越小,发生意外错误和“未经邀请的访问”的概率越低。没有必要就不要把方法和字段设为 public。
规则 4:通过 getter 和 setter 控制访问
这允许在读取/写入字段时添加额外逻辑,例如校验。
public class Account {
private double balance;
public void setBalance(double balance) {
if (balance < 0) {
throw new IllegalArgumentException("余额不能为负数!");
}
this.balance = balance;
}
public double getBalance() {
return balance;
}
}
规则 5:不要暴露内部实现
如果你的字段是数组或列表,不要通过 getter 直接返回它——要返回副本,或只提供必要的方法。
public class Team {
private List<String> members = new ArrayList<>();
// 正确做法:
public List<String> getMembers() {
return new ArrayList<>(members); // 返回副本
}
}
4. 实战示例
假设我们有一个类 LibraryUser,用于描述图书馆用户。
错误实现示例
public class LibraryUser {
public String name;
public int borrowedBooks;
}
这样任何代码都能对对象为所欲为:
LibraryUser user = new LibraryUser();
user.name = null;
user.borrowedBooks = -10; // 逻辑?这算什么逻辑?
带封装的正确实现示例
public class LibraryUser {
private String name;
private int borrowedBooks;
public LibraryUser(String name) {
this.name = name;
this.borrowedBooks = 0;
}
public String getName() {
return name;
}
public int getBorrowedBooks() {
return borrowedBooks;
}
public void borrowBook() {
borrowedBooks++;
}
public void returnBook() {
if (borrowedBooks > 0) {
borrowedBooks--;
}
}
}
现在外部代码不能直接修改借书数量或用户名。一切只能通过类的方法来控制。
5. 实现中的特点与细节
有时把字段设为 public 看起来比写一堆 getter/setter 更省事。但这是个陷阱!公开字段就像把家门敞开:是的,方便,但并不安全。
另一个细节——并不总要为所有字段都提供 getter 和 setter。如果对象创建后该字段不应再改变,只提供 getter,并把字段设为 final:
public class Passport {
private final String number;
public Passport(String number) {
this.number = number;
}
public String getNumber() {
return number;
}
}
还要记住:如果类被声明为 public,文件名必须与类名相同!这并不完全是关于访问修饰符的问题,但却是新手非常常见的错误。
6. 使用访问修饰符时的常见错误
错误 1:忘了给字段或方法写访问修饰符。 结果该字段或方法在整个包内都可见,即使你并不想这样。即使 IDE 不报错,也要显式写出修饰符。
错误 2:把所有字段都声明为 public。 这会破坏封装,让你的代码变得脆弱且不可预测。示例中为“图省事”而养成的习惯不应出现在生产代码中。
错误 3:试图从其他类访问 private 字段。 Java 不会允许这么做——编译器会保护你。但如果你还想通过反射“绕过”它——请想一想,为什么会产生这样的需求。
错误 4:以为只要有继承,protected 成员到处都能访问。 实际上,在包外只能由子类访问,并且只能通过 this 或子类对象来访问。
错误 5:通过 getter 返回内部集合。 如果返回内部数组或列表的引用,外部代码就能修改它,从而破坏类的不变量。
错误 6:通过 setter 设值却没有校验。 如果不验证输入值,对象可能进入不正确的状态(例如出现负余额)。
错误 7:方法的可见范围过宽。 有时把方法设为 public,但其实它们只在包内或类内才需要。这会暴露不必要的 API,并增加维护成本。
GO TO FULL VERSION