人们对 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:对同一包的类可见、子类可继承。

  • 无修饰符:对同一包的类可见。


  对类的访问控制与对类成员的访问控制效果类似,但因为类种类的复杂性,这里不再概括,只是给出相应的对修饰符的使用限制。

类的类型\修饰符类型无修饰符publicprivateprotected
普通的类(非内部类的类)××
成员内部类、静态内部类
局部内部类、匿名内部类×××

平常所说的 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.”开头的用户自定义类。不过话说回来,不能依赖访问控制修饰符来杜绝所有的不正当访问的问题,因为可以还通过反射等手段来绕过这些权限限制。访问控制修饰符只是反映了类作者对使用该类的一种建议,还要依靠使用者自己遵循契约精神,才能有效防止滥用。

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值