Java面试基础问题之(二) 您所在的位置:网站首页 四个权限修饰符的区别 Java面试基础问题之(二)

Java面试基础问题之(二)

2024-07-08 16:17| 来源: 网络整理| 查看: 265

一.  一张表

 一般这种问题都是先摆上一张表,如下:

          权限

           类内       

         同包   

         子类

      不  同   包          

       private 

           ✔    

         ❌

         ❌  

          ❌

      default    

           ✔  

         ✔

         ❌

          ❌

    protected    

           ✔    

         ✔  

         ✔ 

          ❌

       public    

           ✔    

          ✔  

         ✔ 

            ✔ 

首先是记忆问题:

1)private 和public:什么都不行 VS  什么都行

不用记,因为一个是最小权限,一个是最大权限,即一个是什么都不行(除了本类以外),一个是什么都行。

2)default:包属性

default是默认的权限修饰符,即如果不写权限修饰符,则一般认为是default。而default又叫作包属性,即同包可访问。

至于为什么,我是这样理解的:default是默认权限修饰符,而一般项目开发都习惯在一个包下开发关联紧密的类,此时default一方面满足了包内类间相互访问的需求,另外一方面又可对外包隐藏本包类的细节(default和private修饰的变量和方法),考虑到default一般不写,其作为包属性既符合编程习惯,又满足保密性需求。

3) protected:继承属性

protected又叫作继承属性,继承继承,顾名思义,即子类可访问。

二.  表中令人迷惑的地方:“同包”,“子类”和“其他包”的交集问题

这张表看似明晰,但是仔细琢磨还是有不明晰的地方:“同包”和“子类”以及“子类”和“其他包”之间并不是没有交集的,如default属性是同包✔,子类❌,那么同包内的子类呢? protected属性是子类✔,其他包❌,那么其他包内的子类呢 ?

记忆:

                                          

可以看到, 同包  子类 不同包  在出现冲突的情况下,以低权限为准,低权限往高权限是“侵入式”的。具体说来,即

1)default属性是同包✔,子类❌,那么同包内的子类呢 ?

default下,同包下的子类也是可以访问的

这是符合常规逻辑的:既然default是同包下可以,为什么同包下的子类不可以呢(当然,这里是先默认了同包下可以访问作为前提,再看子类),从这个角度看,在default属性下的子类❌应该删掉,不写为好,就用“同包可用,不同包不可用”来区分,同包 + 不同包 = 全集,多好。

2)protected属性是子类✔,其他包❌,那么其他包内的子类呢 ?

protected下,其他包的子类也是可访问的

和上面同理,这是符合常规逻辑:既然protected(继承)是子类可以,为什么其他包下的子类不可以呢,如果不可以那么protected和default又有什么区别呢,从哪里体现他的权限比default大呢。从这个角度看,在protected属性下的其他包不填,或者填“非子类❌“,修正后的表格如下:

          权限

           类内       

         同包   

         子类

      不  同   包          

       private 

           ✔    

         ❌

         ❌  

          ❌

      default    

           ✔  

         ✔

         

          ❌

    protected    

           ✔    

         ✔  

         ✔ 

     非子类❌

       public    

           ✔    

          ✔  

         ✔ 

            ✔ 

三.  实验结果

以上都是理论知识(其实也是看其他的理论和自己验证得出的,只是放在前面了。。。),那么实际结果呢

问题1:

1)default属性是同包✔,子类❌,那么同包内的子类呢 ?

default下,同包下的子类也是可以访问的。

验证:

一个包下建两个类:Father, Daughter,其中Daughter继承自Father:

Father类,注意,name是默认属性:

Daughter类:

运行结果:

即default下,同包的子类式也可以访问的。

问题2:

2)protected属性是子类✔,其他包❌,那么其他包内的子类呢 ?

protected下,其他包的子类也是可访问的。

验证:

一个分别在两个不同包下新建两个类:Father, Son,其中Son继承自Father:

Father类,注意,name是protected属性:

Son类:

运行结果:

即protected下,不同包的子类式也可以访问的。

三.  父类成员可不可见/能不能访问的真正含义

可以注意到上面例子中,子类对父类成员的访问是这样达成的: 

有趣的是,经常可见的另外一种访问方式,仍然以上面的protected为例:

分别在两个不同包下新建两个类:Father, Son,其中Son继承自Father:

 

Father类,注意,name是protected属性:

Son类:

有趣的事情发生了,IDEA直接报错了,提示类Father中的name属性是protected的,这样访问是红色的,硬着头皮先运行一 下:

可以看到,编译阶段即报错(IDE自动检错基本都是编译不通过的提示):Father类中的name是protected访问修饰符

问题来了,不是说protected是继承属性吗,无论是不是同一包下,子类都可用,现在不同包却不行?

其实这是弄错了子类访问父类成员时,访问修饰符的”可不可见/能不能访问“的真正含义,先放结论:

这里的”可不可见/能不能访问“是指能在子类中直接访问,即直接通过变量/方法名访问,而不是通过创建父类对象的实例,然后通过实例调用这样”迂回“的方式访问,像之前那样:

 

或者使用super关键字:

 

而不是这样:

等等,有人会疑惑了:你说直接用name 和 用father.name有区别我还能理解,但是super.name 和 father.name不是一样的吗?为什么一个可以,一个又不行呢?

这个问题看似是一个延伸出来的小问题,但其实恰恰涉及到name 和 用father.name的区别,即两种”访问“方式的区别。

要回答这个问题,先想一个貌似无关的问题:

在创建一个子类对象时,是否创建了他的父类对象(这里不聊顺序)?

 

答案是:没有!!

 

很多时候大家的回答是:肯定创建了啊,比如说通过在构造函数中做标记就能看出:

分别在两个不同包下新建两个类:Father, Son,其中Son继承自Father:

父类Father:

这里补充一个小知识:构造方法的默认权限d和类的修饰符一样的(这里是public),如果缩小权限至private,则另外一个包的子类Son无法调用父类构造方法的。像这样:

当然,这里我们给Father的构造方法加上了protected修饰符,Son可以正常访问的。(这个问题在构造方法那节会详细讲)。

点击运行,结果:

回到之前那个问题: 在创建一个子类对象时,是否创建了他的父类对象?

对比这个运行结果,这不是代表先创建了父类对象吗?

抱歉,并没有,这个只代表执行了Father的构造方法,哪里说明创建了父类对象呢,这二者并不相等啊(内心OS:WTF??),实际上,上面执行的父类构造方法是写在子类中构造方法的头一行的: 

又有人要问了:行行行,暂且假设你说的都是真的,只是执行了子类构造方法的中父类构造方法,但是问题又来了:如果没有创建父类对象,那这个构造方法是从哪里来的,这个super又是什么,还有之前的的那个super.name,即: 

没有父类对象,哪来的name呢?

其实这是一种误解,能用super不代表就生成的父类对象,能使用父类的name也不代表非要生成父类对象(WTF???)

可以这么理解,子类继承父类时,即extends,像这样:

其实只是把”父类代码“(为了形象理解先这么说吧),添加在子类中,现在子类代码就成了这样(只是为了形象说明,报错忽略):

注意,是将父类所有的”代码“全都移植过来,不是只一直对于子类可见的部分。即private,default,protected.public全都拿过来。

接下来,在new Son()时,在内存中开辟一片区域,在最前面放置父类变量和方法(PS:实际上类的方法信息在编译时即存储在Class文件中,运行时也是在专门的虚拟机栈,不是new对象之后开辟空间保存的,这里为了说明问题这么抽象地说),后面放置子类变量和方法。然后执行父类的构造方法————用于父类变量的初始化及一些客户端程序员想要进行的工作(如我们的打印操作),再执行子类的构造方法(这里只提到了类的构造方法,实际上类的初始化有着默认的执行顺序,关于父子类的静态变量/代码,非静态变量/代码及构造方法的执行顺序会有其他专题专门介绍,这里不展开)。

所以说并不是想象中的先在堆上开辟一片空间创建父类对象,然后再开辟一片空间创建子类对象,然后默认子类中的super指向父类对象。并不是。

建子类对象时只是建了一个父类”亚对象“(并不是真正的对象),super代指这个亚对象,用于找到这个父类对象的方法和变量,若子类没有覆盖,其实可以直接使用,就如:

而我们看到的:

 

只代表执行了父类构造方法,子类方法中调用super(),使用super+方法/属性的方式(如super.name)只是用于标记寻找子类”代码“中的父类方法和变量,直接使用方法和变量有可能是子类覆盖过的(如子类中也定义了name属性),实际上并没有这个独立对象的存在,为了直观感受,看一下子类和父类的地址:

Father:

 

Son:

 

一个细节:Son的构造方法中没有”显式“调用父类构造方法,这是因为子类会默认调用父类的无参构造方法,且在调用自身默认构造方法之前。所以父类的无参构造方法还是用大于等于protected的修饰符修饰,不然子类会报错(Son在另外一个包)。

运行结果:

 

可以看出,Father和Son的this地址是一样的! 

地址都是 ”sonpackage.Son@4554617c“,都在Son所在的包,的确是像是”将父类代码放在子类代码前面“一样。

搞清楚了这个问题,再回到原来的问题: 

 

上面两个可以,下面的却报错:

 

前面可知,可将父类全部代码放在子类前面考虑问题,而权限修饰符就是限制子类能不能像操作自己方法/属性一样操作父类的方法/属性,这才是父类方法/变量”可不可见/能否访问“真正含义,public,default同包下的子类,protected下的所有子类指的是:子类可以像操作自己的方法/变量一样操作位于“顶部”的父类方法/变量,这样,“visible/invisible”就很形象了:对于一个类来说,它自己的方法/变量肯定是可见的,但是对于父类的方法,它不一定能“看见”。而能否访问:就像一个类中访问自己方法/变量一样,这里的访问指的是直接访问(操作)父类方法/变量,不是创建对象的方式访问。

当然,要注意,上面的解释是”父类成员可不可见/能不能访问的真正含义“,即只针对于父类成员加访问修饰符,子类可不可见的含义,这并不能延申到诸如访问本包下非父类,访问外包下非父类情况,e.g.

类的包结构:

Father:

Neighbor:

可以看到,在一个非子类的Neighbor类中直接操作Father类成员——fatherName会直接报错。

其实做这个测试我是拒绝的。。。太蠢了点,因为不是子类的话,直接访问父类(此时应该叫另外一个类)的成员根本不从谈起。不管怎么说,理清思路——标题所说的”可不可见/能不能访问的真正含义“是针对于继承关系,即子类访问父类成员而言的,对于非继承关系,”可不可见/能不能访问“仍然是下面所说的——通过对象实例访问成员。

四. 通过对象实例访问成员

上一节中讨论了子类中父类成员的可见性的真正含义,那么现在看下另外一个问题:如果就是考虑通过对象实例访问变量或方法,又有什么限制呢,和private,default,protected.public有什么关系呢?

为了完整探究,下面的实验中一同实验了非继承关系和继承关系,使用通过对象实例访问成员的情况:

类的包结构:

fatherpackage包下;

Father类:

Daughter类:

Neighbor类:

不用多说,对于非子类,也只能通过这种方式访问。

另外一个包sonpackage下:

Son类:

Stranger类:

同Neighbor类,对于非子类,也只能通过这种方式访问。

1)public下,以f.name方式都能正常访问:

2)把Father类的name权限修饰符改为protected:

结果:

即protected下,以f.name方式是退化为包内可见(原来是本包和外包子类可见),比原来范围缩小了。

 

3)把Father类的name权限修饰符改为default(即什么都不加):

结果:

即default下,以f.name方式是包内可见,和原来范围相同。

4)把Father类的name权限修饰符改为private:

结果:

即private下,其他类都不可见,至于本类可不可见,还要测试一下,因为回看Father代码:

并没有实验在本类中通过f.name方式访问变量,直接下“private下只有本类可见”并不严谨(因为本段是后来编辑,所以前面代码就不修改了),这里改变Father代码:

结果:

这符合常理:类中通过自身类对象访问其属性,是可以的(无论是什么修饰符)。至于public,protected.default为什么不重写Father实验,一方面现在private都可以以实例访问自身变量,其他不用多说;另一方面,public,protected.default下,本包其他类(Daughter,Neighbor)都可以以实例访问自身变量,那本类还用说吗。

另外,有个问题并不像实验,因为现在有点像一直看一个汉字,一直看,反而疑神疑鬼觉得不像的状态了。之前讨论的”父类成员可不可见/能不能访问的真正含义“将范围限制在继承关系中,即子类下,但是会看这张表:

          权限

           类内       

         同包   

         子类

      不  同   包          

       private 

           ✔    

         ❌

         ❌  

          ❌

      default    

           ✔  

         ✔

         

          ❌

    protected    

           ✔    

         ✔  

         ✔ 

     非子类❌

       public    

           ✔    

          ✔  

         ✔ 

            ✔ 

其实四个范围中,除了子类有两种访问方式——直接访问和通过父类实例访问之外,还有个”类内“也是有两种访问方式的——直接访问和通过自身类实例访问,上面已经实验了private下通过自身实例访问成员的可见性——可见,那么private直接访问呢?

废话,肯定可以访问啊,真的是越学越糊涂了。不解释,不实验。继续。

在实验完”普通的“变量类型后,对于static变量,会发现有趣的“现象”(之所以称为现象,原因是只知道表现,不知道为什么。。。):

将Father的name属性改为static后:

结果:

可以发现,protected的返回恢复了——仍表现为同包及子类可见,和直接访问的范围相同。而其他三种权限没有发生变化(这里不再实验)。

至于为什么改成了static后,f.name对于外包的子类就可见了,暂时并不清楚机制,隐隐约约觉得static成员都是属于类的,f.name访问方式和Father.name访问方式是一样的,但是为什么Father.name(static)就可见,而f.name(非static)就不可见呢,未知。

不管如何,现在能总结实验得到的结果:

1) 对于静态变量,通过f.name和直接访问的权限是一样的

2) 对于非静态的变量,f.name的protected权限退化为了包权限(default)

好了,本节到此结束,另注意,访问修饰符的细节不必纠结太多,一方面,在实际开发中属性一般都是private,方法(特别是setter/getter)都是public,而内部工具类方法或者不想暴露的方法直接就是private,很少用protected或者default;另一方面,考虑到Java反射机制的存在,无论是什么修饰符,变量都无秘密可言。

 



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有