访问修饰符
引语:小崔写完 Employee 类后,发现测试类访问不到
salary字段,急得直挠头。架构师白歌走过来:"别慌,不是 Bug——是private在保护数据。来,我给你画一张'访问权限地图',四种修饰符各自的领地一清二楚。"
四种访问修饰符概述
Java 提供四种访问修饰符,从严格到宽松依次为:
| 修饰符 | 中文名称 | 同类 | 同包子类 | 同包非子类 | 不同包子类 | 不同包非子类 |
|---|---|---|---|---|---|---|
| private | 私有 | ✅ | ❌ | ❌ | ❌ | ❌ |
| default(无修饰符) | 包级私有 | ✅ | ✅ | ✅ | ❌ | ❌ |
| protected | 受保护 | ✅ | ✅ | ✅ | ✅ | ❌ |
| public | 公开 | ✅ | ✅ | ✅ | ✅ | ✅ |
记忆口诀:private 仅自己,default 同包亲,protected 子可及,public 天下行。
private:仅本类可见
定义
private 是最严格的访问级别。被 private 修饰的成员只能在本类内部访问,对外完全透明。
适用场景
- 类的内部状态字段(如
salary、password) - 仅供本类使用的辅助方法(如校验逻辑、内部计算)
- 不希望被子类继承或覆盖的方法
示例
package com.feixiang.hr;
public class SalaryCalculator {
private double baseSalary; // 基本工资,外部不可见
private double bonusRate = 0.1; // 奖金系数
// ✅ 本类内部可以访问 private 字段
public double calculateMonthlySalary() {
return baseSalary * (1 + bonusRate); // 同级访问 OK
}
// ✅ 本类内部可以调用的私有辅助方法
private double getTaxDeduction() {
return baseSalary * 0.05; // 假设 5% 税
}
public double getNetSalary() {
return calculateMonthlySalary() - getTaxDeduction();
}
}
// ======== 另一个类 ========
class HRAssistant {
public void checkSalary(SalaryCalculator calc) {
// ❌ 编译错误:bonusRate 在 SalaryCalculator 中是 private 访问控制
// System.out.println(calc.bonusRate);
// ❌ 编译错误:getTaxDeduction() 在 SalaryCalculator 中是 private 访问控制
// calc.getTaxDeduction();
// ✅ 只能通过 public 方法访问
System.out.println("实发工资:" + calc.getNetSalary());
}
}
default(包级私有):无修饰符
定义
当一个成员没有任何访问修饰符时,它具有包级私有(Package-Private)访问权限——只能被同一包内的类访问。
重要:default 不是关键字!不写任何修饰符就是 default 访问级别。很多人误写
default void method(),这是编译错误。
适用场景
- 包内部使用的工具类和方法
- 不希望对外暴露但同包类需要的功能
- 包的实现细节(包是天然的内聚单元)
示例
// 文件:com/feixiang/hr/InternalAuditor.java
package com.feixiang.hr;
/**
* 内部审计工具 —— 包级私有,仅供 hr 包内部使用
*/
class InternalAuditor { // 注意:类本身也是 default 级别
// default 方法 —— 同包可调用
void auditSalary(Employee emp) {
System.out.println("【审计】正在审查 " + emp.getName() + " 的薪资记录...");
// 同包内可以访问其他类的 default 成员
}
}
// 文件:com/feixiang/hr/HRManager.java
package com.feixiang.hr;
public class HRManager {
public void runAudit(Employee emp) {
InternalAuditor auditor = new InternalAuditor(); // ✅ 同包,可访问
auditor.auditSalary(emp); // ✅ 同包,可调用
}
}
// 文件:com/feixiang/tech/TechManager.java
package com.feixiang.tech;
public class TechManager {
public void tryAudit() {
// ❌ 编译错误:InternalAuditor 在 com.feixiang.hr 中不是公共的;
// 无法从外部包访问
// InternalAuditor auditor = new InternalAuditor();
}
}
protected:同包 + 子类可见
定义
protected 的访问权限是 default 权限的超集:除了同包可见外,还允许不同包的子类通过继承访问。
核心特性
| 特性 | 说明 |
|---|---|
| 同包访问 | 与 default 一致,同包内所有类均可访问 |
| 跨包子类访问 | 不同包的子类可以继承并访问 protected 成员 |
| 跨包非子类 | 不可访问(这是与 public 的关键差异) |
| 静态 protected | 静态成员也可以用 protected,同样遵循上述规则 |
示例:protected 的跨包继承访问
// ====== 父类:com.feixiang.hr.Employee ======
package com.feixiang.hr;
public class Employee {
protected String empName; // protected:子类可访问
protected double salary; // protected:子类可访问
private String idCard; // private:子类也不可访问
protected double calculateBonus() {
return salary * 0.15;
}
public Employee(String empName, double salary) {
this.empName = empName;
this.salary = salary;
}
}
// ====== 子类:com.feixiang.tech.TechEmployee ======
package com.feixiang.tech;
import com.feixiang.hr.Employee;
/**
* 技术部员工 —— 不同包的子类
*/
public class TechEmployee extends Employee {
private String techStack; // 技术栈
public TechEmployee(String empName, double salary, String techStack) {
super(empName, salary);
this.techStack = techStack;
}
public void printInfo() {
// ✅ 跨包子类:可以访问父类的 protected 字段
System.out.println("姓名:" + empName); // protected 继承
System.out.println("薪资:" + salary); // protected 继承
// ❌ 编译错误:idCard 在 Employee 中是 private 访问控制
// System.out.println("身份证:" + idCard);
// ✅ 跨包子类:可以调用父类的 protected 方法
double bonus = calculateBonus();
System.out.println("奖金:" + bonus);
}
}
// ====== 不同包的非子类 ======
package com.feixiang.finance;
import com.feixiang.hr.Employee;
public class FinanceAuditor {
public void audit(Employee emp) {
// ❌ 编译错误:empName 在 Employee 中是 protected 访问控制
// System.out.println(emp.empName);
// ❌ 编译错误:calculateBonus() 在 Employee 中是 protected 访问控制
// emp.calculateBonus();
// ✅ 只能通过 public 接口访问
System.out.println("审计完成");
}
}
protected 细节:子类中不能通过父类引用访问
这是面试高频陷阱题!子类继承 protected 成员后,可以在子类代码中通过子类引用访问它,但不能通过父类引用访问另一个对象的 protected 成员。
package com.feixiang.tech;
import com.feixiang.hr.Employee;
public class TechEmployee extends Employee {
public TechEmployee(String empName, double salary, String techStack) {
super(empName, salary);
}
// ✅ 通过子类引用(this)访问
public void methodA() {
System.out.println(this.empName); // OK —— this 是 TechEmployee 类型
}
// ✅ 通过另一个同包子类引用访问
public void methodB(TechEmployee other) {
System.out.println(other.empName); // OK —— 同类型
}
// ❌ 通过父类引用访问另一个对象的 protected 成员
public void methodC(Employee other) {
// 编译错误:empName 在 Employee 中是 protected 访问控制
// System.out.println(other.empName);
}
}
原理:Java 语言规范规定,protected 成员的跨包访问必须通过子类类型或其子类型的引用进行。这样设计的目的是防止恶意子类通过继承来"窃取"其他对象的 protected 数据。
public:完全公开
定义
public 是最宽松的访问级别。被 public 修饰的成员可以被任何类在任何地方访问。
适用场景
- 类的公开 API(如 getter/setter)
- 工具类中的静态方法
- 常量(
public static final)
package com.feixiang.hr;
public class CompanyConstant {
public static final String COMPANY_NAME = "飞翔科技";
public static final String COMPANY_ADDRESS = "北京市朝阳区飞翔大厦";
public static final int MAX_EMPLOYEES = 5000;
}
访问修饰符选择原则
最佳实践:
- 字段永远 private——这是封装的底线
- 方法优先 default——只在需要的时候扩大访问级别
- protected 慎用——它让子类与父类实现细节耦合
- public 即承诺——public API 一旦发布就不能轻易修改,因为可能有无数外部调用方
完整示例:飞翔科技多模块访问控制
场景
飞翔科技有三个包:hr(人力资源)、tech(技术部)、finance(财务部)。不同部门对员工数据有不同的访问权限:
- HR 内部:可以查看和修改所有信息
- 技术部经理(继承自 HR 的员工类):可以查看基本信息,不能看薪资明细
- 财务部:只能通过公开报表接口查看汇总数据
代码
// ====== 文件1:com/feixiang/hr/Employee.java ======
package com.feixiang.hr;
public class Employee {
private String idCard; // 身份证号:仅本类
String phoneNumber; // 电话号码:包内可见(default)
protected String empName; // 姓名:子类可见
protected double salary; // 薪资:子类可见
public String empId; // 工号:完全公开
public Employee(String empId, String empName, double salary, String idCard, String phoneNumber) {
this.empId = empId;
this.empName = empName;
this.salary = salary;
this.idCard = idCard;
this.phoneNumber = phoneNumber;
}
// private 字段通过 getter 受控暴露
public String getIdCardMasked() {
// 脱敏:只显示前3后4
if (idCard == null || idCard.length() < 7) return "***";
return idCard.substring(0, 3) + "****" + idCard.substring(idCard.length() - 4);
}
public String getEmpName() { return empName; }
public double getSalary() { return salary; }
}
// ====== 文件2:com/feixiang/hr/HRService.java(同包) ======
package com.feixiang.hr;
public class HRService {
public void printFullInfo(Employee emp) {
// ✅ 同包:可访问 private 以外的所有成员
System.out.println("工号:" + emp.empId); // public
System.out.println("姓名:" + emp.empName); // protected
System.out.println("薪资:" + emp.salary); // protected
System.out.println("电话:" + emp.phoneNumber); // default —— 同包可访问
// ❌ 编译错误:idCard 在 Employee 中是 private 访问控制
// System.out.println("身份证:" + emp.idCard);
// ✅ 通过 getter 获取脱敏数据
System.out.println("身份证:" + emp.getIdCardMasked());
}
}
// ====== 文件3:com/feixiang/tech/TechManager.java(不同包的子类) ======
package com.feixiang.tech;
import com.feixiang.hr.Employee;
public class TechManager extends Employee {
private String techField;
public TechManager(String empId, String empName, double salary,
String idCard, String phoneNumber, String techField) {
super(empId, empName, salary, idCard, phoneNumber);
this.techField = techField;
}
public void printTechInfo() {
// ✅ 跨包子类:可访问 public 和 protected
System.out.println("工号:" + empId); // public
System.out.println("姓名:" + empName); // protected
System.out.println("技术方向:" + techField);
// ❌ 编译错误:phoneNumber 在 Employee 中不是 public 的;无法从外部包访问
// System.out.println("电话:" + phoneNumber); // default —— 跨包不可见
// ❌ 编译错误:idCard 在 Employee 中是 private 访问控制
// System.out.println("身份证:" + idCard);
}
}
// ====== 文件4:com/feixiang/finance/FinanceReport.java(不同包的非子类) ======
package com.feixiang.finance;
import com.feixiang.hr.Employee;
public class FinanceReport {
public void printReport(Employee emp) {
// ✅ 跨包非子类:只能访问 public
System.out.println("工号:" + emp.empId); // public
System.out.println("姓名:" + emp.getEmpName()); // public getter
// ❌ 以下全部编译错误
// System.out.println(emp.empName); // protected —— 跨包非子类不可见
// System.out.println(emp.salary); // protected —— 跨包非子类不可见
// System.out.println(emp.phoneNumber); // default —— 跨包不可见
// ✅ 通过 public getter 受控访问
System.out.println("薪资(脱敏):" + "***");
System.out.println("身份证(脱敏):" + emp.getIdCardMasked());
}
}
测试代码
package com.feixiang;
import com.feixiang.hr.Employee;
import com.feixiang.hr.HRService;
import com.feixiang.tech.TechManager;
import com.feixiang.finance.FinanceReport;
public class AccessControlTest {
public static void main(String[] args) {
// 创建员工
Employee emp = new Employee("FX-1001", "白歌", 35000.00,
"110101199001011234", "13800138000");
TechManager techMgr = new TechManager("FX-2001", "小崔", 28000.00,
"110101199502021234", "13900139000",
"Java 后端");
System.out.println("========== HR 视角(同包)==========");
HRService hr = new HRService();
hr.printFullInfo(emp);
System.out.println("\n========== 技术经理视角(跨包子类)==========");
techMgr.printTechInfo();
System.out.println("\n========== 财务报表视角(跨包非子类)==========");
FinanceReport report = new FinanceReport();
report.printReport(emp);
}
}
运行输出
========== HR 视角(同包)==========
工号:FX-1001
姓名:白歌
薪资:35000.0
电话:13800138000
身份证:110****1234
========== 技术经理视角(跨包子类)==========
工号:FX-2001
姓名:小崔
技术方向:Java 后端
========== 财务报表视角(跨包非子类)==========
工号:FX-1001
姓名:白歌
薪资(脱敏):***
身份证(脱敏):110****1234
易错场景
反例一:接口中的方法不加 public
// ❌ 错误认知:接口方法默认 public,不需要写
interface EmployeeService {
void hire(Employee emp); // 虽然默认为 public,但不写容易造成误解
}
// 实现类可能误写成 default 级别
class EmployeeServiceImpl implements EmployeeService {
// ❌ 编译错误:EmployeeServiceImpl 中的 hire(Employee) 无法实现
// EmployeeService 中的 hire(Employee);正在尝试分配更弱的访问权限;
// 以前为 public
void hire(Employee emp) { } // 接口方法隐式为 public,实现类不能缩小权限
}
纠正:
// ✅ 明确写出 public,语义清晰
interface EmployeeService {
public void hire(Employee emp);
}
class EmployeeServiceImpl implements EmployeeService {
@Override
public void hire(Employee emp) { // 必须写 public
}
}
原理:Java 要求方法重写时,子类方法的访问权限不能低于父类方法。接口方法默认为
public,所以实现类必须用public。
反例二:误以为 protected == public
package com.feixiang.hr;
public class Employee {
protected String empName;
}
package com.feixiang.finance;
import com.feixiang.hr.Employee;
public class Auditor {
public void check(Employee emp) {
// ❌ 编译错误!Auditor 不是 Employee 的子类
// 且不同包,不能访问 protected
// System.out.println(emp.empName);
}
}
纠正:不同包的非子类要访问,只能用 public。
面试考点
Q1:Java 中有哪几种访问修饰符?分别的权限范围是什么?
Java 有四种访问修饰符:①
private——仅本类可见;② default(无修饰符)——同包可见;③protected——同包 + 不同包子类可见;④public——所有类可见。权限从小到大:
private < default < protected < public。
Q2:protected 和 default 有什么区别?
default(包级私有):只能被同包内的类访问,不同包的子类无法访问。protected:同包内所有类 + 不同包的子类都可以访问。protected 比 default 多了一个"跨包子类访问"的能力。这是两者的唯一区别。
Q3:为什么子类中不能通过父类引用来访问另一个对象的 protected 成员?
这是 Java 语言规范(JLS 6.6.2)的保护机制。protected 设计的初衷是让子类继承并使用父类的成员,而非让子类成为"万能钥匙"去访问任意对象的 protected 数据。如果允许通过父类引用访问,一个恶意的子类就可以读取/修改同类型其它对象的 protected 数据,破坏封装。
Q4:接口中的方法可以不用 public 修饰吗?
可以省略,因为接口中的方法隐式为 public。但显式写出
public是更好的实践——它让代码意图更明确,避免实现类误写成 default 级别导致编译错误。从 Java 9 开始,接口还支持private方法(用于默认方法间的复用)。
Q5:类的访问修饰符有哪些?
顶级类(非内部类)只有两种访问级别:
public和 default。public类可以被任何包访问;default 类只能被同包访问。private和protected不能用于顶级类。