人们对 Java 中的 protected 与继承有什么误解?
在 Java 中,访问控制修饰符 protected 给许多人带来了混淆,因为它不仅涉及继承,还涉及包。不过,protected 也有自身在设计上的问题,向来为世人所诟病。在详解这个关键字之前,首先来理清什么是调用。
调用方取决于调用方法所属对象还是调用方法所处位置
调用,这是一个很基础的词汇,很多人都以为自己弄清了,可事实是他们有时候才如此。请看下面这段代码。假设下面的函数 main 位于一个无关紧要的类中,方法 nextInt 的调用方是谁?
/**
* 假设此函数 main 位于一个无关紧要的类中
*/
public static void main(String[] args) {
var generator = new Random();
var randomNum = generator.nextInt(100); // 方法 nextInt 的调用方是谁?
System.out.println(randomNum);
}
很多人会不假思索地认为方法 nextInt 的调用方是 generator。他们有一个理所应当但经不起推敲的理由:generator.nextInt(100)
。因为 nextInt 是 generator 的方法,所以 nextInt 的调用方是 generator。
实际上,调用方应该按调用函数在被调用时所处的位置来算。此处它位于函数 main 中,因此可以说调用方为函数 main,或者更古板一些,说成方法 nextInt 被调用处的那段代码。为什么说调用方不是对象 generator 呢?因为 generator 只是找到函数 nextInt 的一种途径,应该说,generator 才有直接访问函数 nextInt 的权限。这有点类似于快递员。你是从快递员那里拿到快递的,但寄快递这个行为不是快递员发起的。反过来也很好理解。因为调用方法 nextInt 总是要通过对象 generator 的,总不能认为在任何地方调用方法 nextInt,调用方都是 generator 吧!这样的话,调用方一词也失去了意义。
记住,非关键字的对象一定不会成为调用方,调用方由调用方法所处位置决定。
Java 的访问控制修饰符
Java 的访问控制多达 4 种。其中,对类有 4 种,对类成员有 4 种。但要记住,首先,访问控制是对调用方的限制,关于什么是调用方,前面已有说明。其次,访问控制永远是对外部来说的。不管使用什么访问控制修饰符,对内都是不存在访问限制的。或者说,不管使用什么访问控制修饰符,对内永远都是 public。不仅如此,调用方只要和被调用方所属对象属于同一个类,则调用方拥有该对象关于该类的任何成员的访问权限。
对类成员的访问控制:
-
private:只对以下的类可见:
-
本类
-
本类的直接或间接的内部类
-
本类的直接或间接的外部类
-
-
public:对任何类都可见。
-
protected:对同一包的类可见、子类可继承。
-
无修饰符:对同一包的类可见。
对类的访问控制与对类成员的访问控制效果类似,但因为类种类的复杂性,这里不再概括,只是给出相应的对修饰符的使用限制。
类的类型\修饰符类型 | 无修饰符 | public | private | protected |
---|---|---|---|---|
普通的类(非内部类的类) | √ | √ | × | × |
成员内部类、静态内部类 | √ | √ | √ | √ |
局部内部类、匿名内部类 | √ | × | × | × |
平常所说的 protected 的子类可见实际指的是什么
为了便于说明,下面构造了三个类,这三个类均位于不同的源文件中。其中,类 Lover 继承了 Friend,但不与其在同一个包中。类 Wife 与 Friend 位于同一个包中,但它们之间没有继承关系。
package home;
public class Friend {
private String changeMe() {
return "Well, next time.";
}
protected String nickname() {
return "dog";
}
protected String getMoney() {
return "Here you are.";
}
public static void main(String[] args) {
var myFriend = new Friend();
System.out.println(myFriend.changeMe()); // Well, next time.
System.out.println(myFriend.nickname()); // dog
System.out.println(myFriend.getMoney()); // Here you are.
}
}
package hotel;
import home.Friend;
public class Lover extends Friend {
protected String nickname() {
return "little dog";
}
public void testUs() {
var myFriend = new Friend();
System.out.println(myFriend.changeMe()); // compile error!
System.out.println(myFriend.nickname()); // compile error! notice!
System.out.println(myFriend.getMoney()); // compile error! notice!
var myLover = new Lover();
System.out.println(myLover.changeMe()); // compile error!
System.out.println(myLover.nickname()); // little dog
System.out.println(myLover.getMoney()); // Here you are.
System.out.println(((Friend)myLover).nickname()); // compile error! notice!
System.out.println(((Friend)myLover).getMoney()); // compile error! notice!
}
public static void main(String[] args) {
var lover = new Lover();
lover.testUs();
}
}
package home;
public class Wife {
public static void main(String[] args) {
var husband = new Friend();
System.out.println(husband.changeMe()); // compile error!
System.out.println(husband.nickname()); // dog
System.out.println(husband.getMoney()); // Here you are.
}
}
可以看出,虽然类 Lover 继承了 Friend,但它却不能使用一个类型为 Friend 的对象的 protected 方法,但与类 Friend 位于同一包的 Wife 却可以。另外一个有意思的现象是,将一个 Lover 对象向下转换(downcasting)成 Friend 对象,居然变得连 Lover 对象的重写方法也不能访问了。可以说,这些一个设计上的缺陷。此外,人们对 Java 中的 protected 也有些误解。实际上,protected 并未对子类提供任何额外的访问权限!protected 与 private 关于子类的区别只是,protected 将方法“交给”了子类。这就是说,相对 private,protected 的作用只是,让子类隐式定义了一个一模一样的方法,仅此而已。而之所以看起来,子类可以访问基类的方法,只是前面所提到的“调用方只要和被调用方所属对象属于同一个类,则调用方拥有该对象关于该类的任何成员的访问权限
”的具体体现而已。protected 让基类的方法变成了子类的方法,然后子类自己可以访问自己的方法,就这么简单!换句话说,继承实际上并不能让子类访问基类的方法,只是用来减少代码的重复。这就是为什么无法使用 Object 对象的方法 clone 对任意对象进行复制(这里指的是将任意对象先向下转换成 Object,然后再调用方法 clone 来复制)。另外,接口中不能声明 protected 方法的原因也是如此(面向接口编程时,使用接口引用变量无法调用该接口的保护方法)。
不过,对包来说并不是如此,可以看出,类 Friend 与 Wife 位于同一个包中,但类 Friend 与其没有继承关系,也没有定义在同一个源文件中,但类 Friend 却可以访问 Wife 对象的方法。这有什么问题呢?设想这样的一种情况,有一个希望禁止继承的类,它有一个 protected 方法。该类的作者希望该类的 protected 方法对外不公开,但该类的作者却不能将其改为 private,原因是该方法在该类的基类就已经被定义为 protected,而子类方法的被访问权限不能低于超类方法,因此该作者选择对类使用 final 来阻止通过继承来访问和重写。不过,由于 protected 对包的可见性,该作者恐怕不能完全如愿。如果定义一个有与该类所属包相同包名的类,就可以轻松访问该类的 protected 方法,这相当于给黑客留了一个后门。因此,从 JDK 1.2
开始,类加载器开始禁止加载包名以“java.
”开头的用户自定义类。不过话说回来,不能依赖访问控制修饰符来杜绝所有的不正当访问的问题,因为可以还通过反射等手段来绕过这些权限限制。访问控制修饰符只是反映了类作者对使用该类的一种建议,还要依靠使用者自己遵循契约精神,才能有效防止滥用。