在 Java 的面向对象设计中,封装(Encapsulation) 是三大基石之一。而访问修饰符(Access Modifiers)正是实现封装的“守门员”。它们严格控制着类、方法、字段和构造器的可见范围,决定了谁可以访问你的代码,谁只能望而却步。
理解 public、protected、private 以及默认(default/package-private)的区别,不仅是面试必考题,更是设计健壮、安全、易维护 API 的核心技能。今天,我们将通过多维对比、场景分析和模块化新特性,彻底掌握这四个修饰符。
一、核心区别一览表
这是最直观的对比,展示了不同修饰符在四种作用域下的可见性:
| 修饰符 | 同类 (Within Class) | 同包 (Within Package) | 子类 (Subclass) | 全局/其他包 (Outside Package) | 核心语义 |
|---|---|---|---|---|---|
private |
✅ | ❌ | ❌ | ❌ | 绝对私有:仅限本类内部使用 |
| (默认) | ✅ | ✅ | ❌ | ❌ | 包级私有:仅限同包内兄弟类使用 |
protected |
✅ | ✅ | ✅ | ❌ | 受保护:同包 + 跨包子类可见 |
public |
✅ | ✅ | ✅ | ✅ | 完全公开:全世界皆可访问 |
注意:
- 默认修饰符:即不写任何关键字,也称为 package-private。
- 类的修饰符:顶层类(Top-level Class)只能用
public或 默认,不能用private或protected(内部类除外)。- 子类的特殊性:
protected对子类可见,但子类只能通过继承的方式访问(即操作自己的实例或父类引用指向的子类实例),不能直接访问父类实例的 protected 成员(除非在同包下)。
二、深度解析:四大修饰符详解
1. private:最严格的封锁
- 范围:仅在声明它的类内部可见。
- 用途:
- 隐藏内部实现细节(如辅助方法、内部状态变量)。
- 强制通过公共方法(Getter/Setter)访问数据,以便进行参数校验或逻辑控制。
- 防止子类意外覆盖关键逻辑。
- 示例:
public class BankAccount { private double balance; // 外部无法直接修改余额 private void validateAmount(double amount) { // 内部辅助方法 if (amount < 0) throw new IllegalArgumentException(); } public void deposit(double amount) { validateAmount(amount); // 类内部可以调用 balance += amount; } }
2. 默认修饰符(Default / Package-Private):小团体的秘密
- 范围:在同一个包(Package) 内的所有类可见。跨包不可见,即使是子类也不行。
- 用途:
- 构建包内部的协作网络。一组紧密相关的类可以共享实现细节,而不暴露给外部世界。
- 测试类与被测类放在同包下,测试类可以访问默认修饰符的方法进行白盒测试。
- 作为“半公开”API,供框架内部组件使用,但不希望被最终用户直接调用。
- 示例:
// 文件:com/example/util/Helper.java package com.example.util; class Helper { // 默认修饰符,其他包无法看到这个类 void doInternalWork() { ... } // 仅 com.example.util 包内可见 }
3. protected:为继承而生的桥梁
- 范围:同包内所有类 + 不同包的子类。
- 用途:
- 设计基类(Base Class)时,允许子类扩展或重写某些方法,但不希望这些方法被无关的外部类调用。
- 模板方法模式(Template Method Pattern)中,钩子方法(Hook Methods)通常设为
protected。
- 陷阱提示:
- 如果子类在不同包中,它只能访问父类对象的
protected成员,不能访问父类引用的其他实例的protected成员。
// 包 A public class Parent { protected void show() {} } // 包 B public class Child extends Parent { public void test(Parent p, Child c) { this.show(); // ✅ 合法:访问自己的继承成员 c.show(); // ✅ 合法:访问子类实例的继承成员 // p.show(); // ❌ 编译错误!p 是 Parent 类型,且不在同包,无法访问 } } - 如果子类在不同包中,它只能访问父类对象的
4. public:开放的接口
- 范围:对所有类可见,无论包结构如何。
- 用途:
- 定义对外的 API 接口、入口方法(如
main)、常量。 - 工具类方法、枚举值。
- 定义对外的 API 接口、入口方法(如
- 原则:尽量缩小
public的范围。过多的public成员会增加维护成本,破坏封装性。
三、类的访问修饰符特殊规则
对于顶层类(非内部类),Java 只允许两种修饰符:
public:类名必须与文件名完全一致。该类可以被任何包导入和使用。- 默认(无修饰符):类名可以与文件名不一致(但通常建议一致)。该类只能被同一个包内的其他类访问。常用于隐藏实现类,只暴露接口。
// 文件:MyService.java
public interface MyService { void run(); } // 公开接口
// 文件:MyServiceImpl.java (可以是默认修饰符)
class MyServiceImpl implements MyService { // 外部包看不到这个实现类
public void run() { ... }
}
// 工厂类(在同一个包内)
public class ServiceFactory {
public static MyService create() {
return new MyServiceImpl(); // 包内可以实例化
}
}
优势:外部用户只能通过 ServiceFactory 获取 MyService 接口,无法依赖具体的 MyServiceImpl 实现,实现了完美的解耦。
四、常见面试题与误区澄清
Q1: protected 和默认修饰符的区别是什么?
答:关键在于跨包的子类。
- 默认:子类如果在不同包,不可见。
- protected:子类即使在不同包,也可见(通过继承)。
- 在同包内,两者效果相同。
Q2: 为什么构造函数有时设为 private?
答:
- 单例模式:防止外部
new对象,强制通过getInstance()获取唯一实例。 - 工具类:如
Math,Collections,类中全是静态方法,不需要实例化,私有构造器防止被实例化。 - 不可实例化的基类:只希望被继承,不希望直接创建基类对象。
Q3: 接口(Interface)中的成员默认是什么修饰符?
答:
- 字段:默认且必须是
public static final。 - 方法:
- Java 8 之前:默认且必须是
public abstract。 - Java 8+:
default方法默认是public;static方法默认是public。 - 注意:接口中不能使用
private(Java 9 之前),Java 9 开始接口支持private方法用于复用 default 方法的代码,但对外仍不可见。
- Java 8 之前:默认且必须是
Q4: 子类能访问父类的 private 成员吗?
答:绝对不能。private 是类的边界,子类继承的是父类的非私有成员。如果子类需要访问,父类必须提供 protected 或 public 的 Getter 方法。
五、2026年视角:模块化系统(Jigsaw)的影响
自 Java 9 引入模块系统(Project Jigsaw)以来,访问控制多了一层模块级限制。
- 传统机制:
public类只要类路径(Classpath)上有,就能被访问。 - 模块机制:即使类是
public的,如果它所在的包没有被模块描述符(module-info.java)中的exports语句导出,其他模块依然无法通过反射或直接代码访问该类。
// module-info.java
module com.myapp.core {
exports com.myapp.api; // 只有这个包里的 public 类能被外部模块访问
// com.myapp.internal 包即使有 public 类,外部模块也无法访问
}
最佳实践:
在现代 Java 开发中,应结合包访问控制和模块导出控制:
- 将内部实现类设为默认修饰符(包级私有)。
- 即使某些类是
public的,如果不想让外部模块依赖,不要在module-info.java中导出其所在包。 - 优先使用
private和默认修饰符,最小化 API 表面面积。
六、总结与选型指南
| 场景 | 推荐修饰符 | 理由 |
|---|---|---|
| 内部状态变量 | private |
严格封装,防止外部随意篡改 |
| 仅供同类使用的辅助方法 | private |
隐藏实现细节 |
| 同一包内协作的工具类/实现类 | 默认 | 限制在包内,避免污染全局命名空间 |
| 基类中供子类重写的方法 | protected |
允许扩展,但对外部隐藏 |
| 对外提供的 API 接口/服务 | public |
必要的开放性 |
| 单例模式的构造器 | private |
控制实例创建 |
黄金法则:
Always start with the most restrictive access level possible.
(始终从最严格的访问级别开始。)
先设为 private,如果同包需要再改为默认,如果子类需要再改为 protected,最后万不得已才设为 public。这能最大程度降低耦合,提高代码的安全性和可维护性。
掌握访问修饰符,就是掌握了 Java 代码的“边界感”。清晰的边界是构建大型、复杂系统的基石。