关于Java中的protected,还有一点需要注意

对于protected访问符,大家也应该是很熟悉的。无非是:

  • 可以在同一个包中的其他类访问
  • 可以在不同包中的子类中访问

对于第一个没什么疑惑,但是对于第二个,往往会忽略掉一点东西。看以下代码

// Person.java
package a;
public class Person {
  protected void run() {
    System.out.println("I am running");
  }
}
// Man.java
package b;
import a.Person;
public class Man extends Person {
  public static void main() {
    Man man = new Man();
    man.run();
    Person person = new Person();
    person.run(); // 这行代码会报错,不能这样访问。
  }
}

在IDE中写入上面的代码时,person.run();无法通过编译,因为Person的run方法是protected,不能这样访问。这样就奇怪了吧,明明是在子类里面,为什么不能这样访问呢?这说明,第一个访问,还有些细节,没有被说明,而一般的资料往往都没有介绍这个细节,大部分人也很少在开始就遇到这样的问题,故而这个细节一直被忽略。

关于这个问题的解释,我一直没有找到很好的解释,在学习JVM的时候,找到了一些可以用来解释。

这里需要对Java虚拟机指令有一定了解,我也不准备详细讲解虚拟机指令,因此直接给出结论吧。

涉及到的指令是invokespecial,Java虚拟机规范是这么描述的:

Invoke instance method; special handling for superclass, private, and instance initialization method invocations.

这说明,在Java代码中,若是调用父类的方法、构造器或者私有方法时,会被翻译为invokespecial字节码。

我们继续看invokespecial的关键描述:

If the resolved method is protected , and it is a member of a superclass of the current class, and the method is not declared in the same run-time package (§5.3) as the current class, then the class of objectref must be either the current class or a subclass of the current class.

意思就是说,若解析的方法(可以简单的认为被调用的方法)是protected修饰的,并且这个方法,是当前类的父类中的方法,而且这个方法没有声明在当前类的同一个运行时包内(可以简单的认为,声明该方法的包,和当前类所在包不是同一个包),那么,调用该方法的对象所对应的类要么是当前类,要么是当前类的子类。

提炼一下,当被调用的方法被protected修饰时,在满足以下两个条件时:

  • 调用的方法是当前类的父类中的方法
  • 当前类和声明该方法的类不在同一包中

必然可以得出以下结论:

  • 调用该方法的对象所对应的类要么是当前类,要么是当前类的子类

我们把上面的代码,和现在的例子进行一下对比就知道了:

  • 当前类是Man,而run方法是Person中的方法,因此满足被调用的方法是当前类的父类
  • Man声明在b包中,Person声明在a包中,故满足不在同一包内

因此,两个条件均满足,所以,调用该方法的对象所对应的类要么是当前类Man,要么是当前类的子类。而Person是当前类的父类,于是不能够进行调用。可以看到,代码前面使用man.run()是没有问题的,因为man对应着当前类。

那么我们进行一下类型强转呢?

例如如下代码(Person代码不变):

// Man.java
package b;
import a.Person;
public class Man {
  public static void main() {
    Man man = new Man();
    man.run();
    Person person = new Person();
    ((Man)person).run(); // 这行代码能通过编译,但是运行时会抛出java.lang.ClassCastException
  }
}

在IDE下,进行强转之后,代码并不会报错,正常编译,而运行之后,还是抛出异常了:java.lang.ClassCastException这个异常主要是在类型转换的时候发生,原因在于,person是一个Person对象(主要是指动态类型是Person对象),当强制转化为Person的子类时,编译可以通过,但是运行时无法通过的,原因在于,无论怎么强制转化,person的动态类型始终没有变化(即,在运行时,强制类型转化,并没有对堆上的数据进行改变,改变的只是变量person的引用类型,例如上面的强转,将person从指向Person,变成了指向Man)。讲到这里貌似有点偏题了。。。

前面讲到的是protected修饰的方法,那么变量呢?其实也是一样,对于非静态的变量(静态变量不会被继承,并没有啥可说的),在进行读写时,涉及到的指令是getfieldputfield。同样,我在Java虚拟机规范的getfield中也找到了如下描述:

If the field is protected , and it is a member of a superclass of the current class, and the field is not declared in the same run-time package (§5.3) as the current class, then the class of objectref must be either the current class or a subclass of the current class.

在putfield中也有同样的描述,只不过还有其他的描述,这说明,protected修饰的变量和protected修饰的方法在访问的规则上是一样的。

小结

要完全看懂本文,还是需要对Java虚拟机有一定的了解,因为我本身也没有找到很好的资料来解释前面的问题(毕竟很多书都是忽略这些东西的,作为基础书籍,确实不应该在这方面太细致),幸而在Java虚拟机规范中找到了相应的说明,故以此来进行解释,说起来这种解释也不一定合适。毕竟,在编译阶段,应当用Java语法规则来解释,而不是Java虚拟机规范,因为这两者本身并不相同,甚至对某些东西,这两个还会出现不一致的情况,最典型的就是,我们在同一代码块不能定义相同的变量名,即不能在同一代码块定义int a; float a;,而这是Java语法的规定,而Java虚拟机并没有这样规定,关于为什么Java虚拟机可以接受这样的定义,这就涉及到名字和类型描述符的一些知识了,具体请自行查阅。

以上内容都是在学习的时候一些自己的思考,可能不是很严谨,甚至还有可能是错误的,还请读者能够有思考的能力,若发现其中不当之处,也请不吝赐教,不胜感激。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 135,850评论 19 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 32,514评论 18 399
  • 五、Java 虚拟机 一、什么是Java虚拟机Java虚拟机是一个想象中的机器,在实际的计算机上通过软件模拟来实现...
    壹点零阅读 4,073评论 0 0
  • 今天七号,长假马上就要结束了。 安静的假期里,回头一看,做了好多事儿…… > 1.新媒体运营在线课程学习,累计时间...
    殷春燕阅读 1,643评论 0 0
  • RACCommand RACCommand 属性与方法
    李潇南阅读 3,523评论 0 0

友情链接更多精彩内容