美文网首页Java学习笔记
关于Java中的protected,还有一点需要注意

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

作者: leibnist | 来源:发表于2017-08-10 21:35 被阅读782次

对于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 {
  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虚拟机可以接受这样的定义,这就涉及到名字和类型描述符的一些知识了,具体请自行查阅。

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

相关文章

网友评论

    本文标题:关于Java中的protected,还有一点需要注意

    本文链接:https://www.haomeiwen.com/subject/ayjkrxtx.html