1. 概述
在 Java 语言中,字段、构造方法、普通方法以及类都可以使用访问修饰符(access modifier)来控制其可见性。本文将重点剖析 protected
这个访问修饰符的实际作用和使用场景,帮你彻底搞懂它的访问规则,避免踩坑。
2. protected 关键字的作用
与 private
仅限本类访问不同,protected
提供了更宽松的访问权限:✅ 同一个包内的类 或 ✅ 子类(即使跨包) 都可以访问被 protected
修饰的成员。
换句话说,protected
是一种介于包级私有(package-private)和 public
之间的折中方案,常用于设计类继承体系时暴露“受保护的内部实现”,既不让外部随意调用,又方便子类扩展。
⚠️ 注意:很多人误以为“子类都能访问 protected
成员”,但实际规则更精细,后面会重点说明。
3. 声明 protected 成员
我们先定义一个 FirstClass
,包含 protected
的字段、构造方法和方法:
public class FirstClass {
protected String name;
protected FirstClass(String name) {
this.name = name;
}
protected String getName() {
return name;
}
}
通过 protected
,我们允许:
- 同一个包下的类访问这些成员
- 所有子类(无论是否同包)访问这些成员
接下来我们通过几个典型场景验证访问行为。
4. protected 成员的访问规则
4.1 同包内访问 ✅
创建一个 GenericClass
,与 FirstClass
在同一包下:
public class GenericClass {
public static void main(String[] args) {
FirstClass first = new FirstClass("random name");
System.out.println("FirstClass name is " + first.getName());
first.name = "new name";
}
}
✅ 编译通过,运行正常。因为 GenericClass
和 FirstClass
在同一个包中,可以自由访问其 protected
成员。
4.2 跨包非子类访问 ❌
现在尝试从另一个包中的普通类访问:
public class SecondGenericClass {
public static void main(String[] args) {
FirstClass first = new FirstClass("random name");
System.out.println("FirstClass name is "+ first.getName());
first.name = "new name";
}
}
❌ 编译失败,报错如下:
The constructor FirstClass(String) is not visible
The method getName() from the type FirstClass is not visible
The field FirstClass.name is not visible
原因很清晰:SecondGenericClass
既不在 FirstClass
的包中,也不是其子类,因此无法访问任何 protected
成员。
4.3 跨包子类访问 ✅
重点来了!创建一个跨包的子类 SecondClass
:
public class SecondClass extends FirstClass {
public SecondClass(String name) {
super(name);
System.out.println("SecondClass name is " + this.getName());
this.name = "new name";
}
}
✅ 编译通过。尽管 SecondClass
和 FirstClass
不在同一个包中,但由于它是子类,因此可以访问父类的 protected
构造方法、字段和方法。
这是 protected
最典型的应用场景:支持跨包继承时的受控访问。
5. protected 内部类
除了字段和方法,protected
还可以用于修饰内部类。这是一个容易被忽略但很关键的点。
我们给 FirstClass
添加一个 protected
的静态内部类:
package com.baeldung.core.modifiers;
public class FirstClass {
// ...
protected static class InnerClass {
}
}
protected static class
表示这个内部类只能被:
- 同包中的类访问
- 子类访问(但注意:仅当满足访问构造器的条件时)
我们来验证几种情况。
5.1 同包内实例化 ✅
修改 GenericClass
:
public class GenericClass {
public static void main(String[] args) {
// ...
FirstClass.InnerClass innerClass = new FirstClass.InnerClass();
}
}
✅ 成功实例化。同包无限制。
5.2 跨包非子类实例化 ❌
在 SecondGenericClass
中尝试:
public class SecondGenericClass {
public static void main(String[] args) {
// ...
FirstClass.InnerClass innerClass = new FirstClass.InnerClass();
}
}
❌ 编译报错:
The type FirstClass.InnerClass is not visible
符合预期,跨包且非子类,无法访问。
5.3 跨包子类实例化 ❌(意外情况!)
重点来了!在 SecondClass
中尝试实例化 InnerClass
:
public class SecondClass extends FirstClass {
public SecondClass(String name) {
// ...
FirstClass.InnerClass innerClass = new FirstClass.InnerClass();
}
}
❌ 竟然也报错!
The constructor FirstClass.InnerClass() is not visible
为什么子类也无法访问?
这个问题非常经典,很多人在这里踩坑。原因如下:
protected static class InnerClass
的默认构造器是protected
的(隐式继承访问级别)SecondClass
是FirstClass
的子类,但不是InnerClass
的子类SecondClass
声明在FirstClass
的包外
因此,尽管 SecondClass
是外层类的子类,但它无法访问另一个包中 protected
类的 protected
构造器。
如何解决?
✅ 解决方案:显式声明一个 public
构造器:
protected static class InnerClass {
public InnerClass() {
}
}
这样,即使跨包,子类也能成功实例化:
FirstClass.InnerClass innerClass = new FirstClass.InnerClass(); // ✅ now works
⚠️ 小结:protected
内部类 + 跨包子类访问 = 构造器权限陷阱。建议显式定义 public
构造器以避免问题。
6. 总结
protected
是 Java 中用于控制继承和包访问的关键修饰符,核心规则如下:
场景 | 是否可访问 |
---|---|
同包类 | ✅ |
跨包非子类 | ❌ |
跨包子类 | ✅(成员) |
跨包子类实例化 protected 内部类 | ❌(除非构造器为 public) |
关键点:
protected
成员可被子类跨包访问 ✅protected
内部类的默认构造器也是protected
,跨包子类无法直接实例化 ❌- 若需子类跨包创建内部类实例,应显式提供
public
构造器 ✅
合理使用 protected
,可以在封装性和可扩展性之间取得良好平衡。
示例代码已上传至 GitHub:https://github.com/baeldung/java-tutorials/tree/master/core-java-lang-syntax-2