面试题,关于构造方法和重写的问题

piaoranxinyu 2014-06-09 10:45:45
父类:

public class A {

private String str = "A";

public A(){
aa();
}

public void aa(){
System.out.println("class A-aa:" + str);
}

}

子类:

public class B extends A{

private String str = "B";

public B(){
aa();
}

public void aa(){
System.out.println("class B-aa:" + str);
}

public static void main(String[] args) {
new B();
}
}

结果:
class B-aa:null
class B-aa:B
为啥,解释下,父类构造方法也调用子类重写好的方法吗,还有为什么为null?
...全文
595 25 打赏 收藏 举报
写回复
25 条回复
切换为时间正序
当前发帖距今超过3年,不再开放新的回复
发表回复
Yoara 2014-06-11
引用 24 楼 Yoara 的回复:
其实用在字节码层面可以很清晰的看到过程
public learn.javavm.B();
  Code:
   Stack=2, Locals=1, Args_size=1
   0:	aload_0
   1:	invokespecial	#10; //Method learn/javavm/A."<init>":()V
   4:	aload_0
   5:	ldc	#12; //String B
   7:	putfield	#14; //Field str:Ljava/lang/String;
   10:	aload_0
   11:	invokevirtual	#16; //Method aa:()V
   14:	return
  LineNumberTable: 
   line 12: 0
   line 9: 4
   line 13: 10
   line 14: 14

  LocalVariableTable: 
   Start  Length  Slot  Name   Signature
   0      15      0    this       Llearn/javavm/B;
执行顺序是: 1.先在堆中开辟B新对象的内存区域。 2. 1: invokespecial #10; //Method learn/javavm/A."<init>":()V 3. 5: ldc #12; //String B 7: putfield #14; //Field str:Ljava/lang/String; 4. 11: invokevirtual #16; //Method aa:()V 问题是为什么是null呢?其实前面几位答得已经很好了,我在这里补充一些。 首先在2步骤执行前,1步骤开辟了内存区域,但还未到3步骤去初始化str值。 其次为什么父类调用子类的aa()方法呢,其实这就是Java面向对象的关键特性——多态导致的。在对这种虚函数的加载之前,Java虚拟机的执行逻辑是这样的: 1.找到栈顶元素指向的实际对象类型,这里就是B。 2.从B中搜索,找到与方法的描述符和简单名称一致的方法,成功找到则返回执行方法。 3.从B的父类逆流向上逐个搜索,找到则返回执行该方法。 4始终没找到,则抛出AbstractMethodError
补充一下,出这个题当面试题的没有意义(但是一道很好玩的题),虽然靠的是对多态和对初始化过程过程的理解,但这种代码在实际运用中出现的话,会被人骂死的,毕竟现在团队开发强调的是可读性。所以面试,我觉得更应该去找性格和愿景上和团队合得来的人(不是他现在有什么,而是他在团队中能成为什么,并帮助团队成为什么)。
  • 打赏
  • 举报
回复
Yoara 2014-06-11
其实用在字节码层面可以很清晰的看到过程
public learn.javavm.B();
  Code:
   Stack=2, Locals=1, Args_size=1
   0:	aload_0
   1:	invokespecial	#10; //Method learn/javavm/A."<init>":()V
   4:	aload_0
   5:	ldc	#12; //String B
   7:	putfield	#14; //Field str:Ljava/lang/String;
   10:	aload_0
   11:	invokevirtual	#16; //Method aa:()V
   14:	return
  LineNumberTable: 
   line 12: 0
   line 9: 4
   line 13: 10
   line 14: 14

  LocalVariableTable: 
   Start  Length  Slot  Name   Signature
   0      15      0    this       Llearn/javavm/B;
执行顺序是: 1.先在堆中开辟B新对象的内存区域。 2. 1: invokespecial #10; //Method learn/javavm/A."<init>":()V 3. 5: ldc #12; //String B 7: putfield #14; //Field str:Ljava/lang/String; 4. 11: invokevirtual #16; //Method aa:()V 问题是为什么是null呢?其实前面几位答得已经很好了,我在这里补充一些。 首先在2步骤执行前,1步骤开辟了内存区域,但还未到3步骤去初始化str值。 其次为什么父类调用子类的aa()方法呢,其实这就是Java面向对象的关键特性——多态导致的。在对这种虚函数的加载之前,Java虚拟机的执行逻辑是这样的: 1.找到栈顶元素指向的实际对象类型,这里就是B。 2.从B中搜索,找到与方法的描述符和简单名称一致的方法,成功找到则返回执行方法。 3.从B的父类逆流向上逐个搜索,找到则返回执行该方法。 4始终没找到,则抛出AbstractMethodError
  • 打赏
  • 举报
回复
GW786228836 2014-06-11
  • 打赏
  • 举报
回复
Mime_mi 2014-06-10
引用 5 楼 Mime_mi 的回复:
你得弄清楚变量、构造函数的执行顺序。

  当父类和子类有Static时,先初始化Static,再初始化子类的Static,再初始化父类的其他成员变量->父类构造方法->子类其他成员变量->子类的构造方法。
A类构造函数执行aa()这个方法时,调用的是子类B中的aa()方法,但是此时str尚未初始化,所以str为默认值null。
这里所说的str是B中的str变量,不是A中的。
  • 打赏
  • 举报
回复
Mime_mi 2014-06-10
你得弄清楚变量、构造函数的执行顺序。

  当父类和子类有Static时,先初始化Static,再初始化子类的Static,再初始化父类的其他成员变量->父类构造方法->子类其他成员变量->子类的构造方法。
A类构造函数执行aa()这个方法时,调用的是子类B中的aa()方法,但是此时str尚未初始化,所以str为默认值null。
  • 打赏
  • 举报
回复
蜗牛- 2014-06-10
父类构造方法也调用子类重写好的方法,这里其实是子类在调用父类的构造方法,方法内继续调用父类的aa方法,碰巧aa方法子类重写过了,那就继续调用子类的了。 null 这个跟类的预先加载与依需求加载有关系 。JAVA类装载器在装载类的时候是按需加载的,只有当一个类要使用(使用new 关键字来实例化一个类)的时候,类加载器才会加载这个类并初始化。new B(); 开始执行时,因为A 类是父类,所以A类先加载到内存中并初始化,当A类的构造方法中的aa 继续调回到子类B中的时候,按需加载,先加载aa方法,这时候,B 类的 str = "B"; 尚未初始化,自然就是null了。 楼主可以给A B 两个类打断点,跟一下,可以帮助理解。 类加载
  • 打赏
  • 举报
回复
yufengdxw 2014-06-10
基础知识都忘了。。
  • 打赏
  • 举报
回复
蜗牛- 2014-06-10
引用 21 楼 fengerpiao123 的回复:
Str=“B”,没有初始化,B类中的aa方法已经加载了吗?
是的,JVM按需加载。因为B类重写了A类的aa方法,所以在A类调用的时候,会调用到B类的aa方法,所以B类会优先加载aa方法,此时,str 尚未加载,或者说尚未初始化,所以还是null
  • 打赏
  • 举报
回复
fengerpiao123 2014-06-10
引用 2 楼 magi1201 的回复:
父类构造方法也调用子类重写好的方法,这里其实是子类在调用父类的构造方法,方法内继续调用父类的aa方法,碰巧aa方法子类重写过了,那就继续调用子类的了。 null 这个跟类的预先加载与依需求加载有关系 。JAVA类装载器在装载类的时候是按需加载的,只有当一个类要使用(使用new 关键字来实例化一个类)的时候,类加载器才会加载这个类并初始化。new B(); 开始执行时,因为A 类是父类,所以A类先加载到内存中并初始化,当A类的构造方法中的aa 继续调回到子类B中的时候,按需加载,先加载aa方法,这时候,B 类的 str = "B"; 尚未初始化,自然就是null了。 楼主可以给A B 两个类打断点,跟一下,可以帮助理解。 类加载
Str=“B”,没有初始化,B类中的aa方法已经加载了吗?
  • 打赏
  • 举报
回复
2.wa 2014-06-10
引用 19 楼 u011278496 的回复:
这个玩意我研究了。 工作一年了,发现对这些概念很混乱。 看看写的博文: 透析Java本质-类的初始化顺序 http://blog.csdn.net/xiaohulunb/article/details/26264841
花点时间研究下,搞清楚,不能老模模糊糊的╮(╯▽╰)╭
  • 打赏
  • 举报
回复
2.wa 2014-06-10
这个玩意我研究了。 工作一年了,发现对这些概念很混乱。 看看写的博文: 透析Java本质-类的初始化顺序 http://blog.csdn.net/xiaohulunb/article/details/26264841
  • 打赏
  • 举报
回复
Andy韩 2014-06-10
2楼说的对,我在补充一点就是:java中如果一个类继承了另一个类那么java总是先调用父类的构造函数,创建父类,然后才会调用子类的构造函数创建子类,事实上在子类的构造函数中有个隐藏的调用super(),这个是调用父类的无参数的构造函数创建父类的,这句话必须写在子类的构造函数的第一行,如果没有写JVM在编译成字节码的时候会自动帮你加上的,当然如果父类没有无参数的构造函数,这种默认机制就会报错了,但是放心,这种错误是编译时的,你可以很容易的通过一些IDE(比如eclipse)的智能提示发现错误。
  • 打赏
  • 举报
回复
动视飘雪 2014-06-10
引用 17 楼 qwe5628197 的回复:
[quote=引用 16 楼 dongjianzhu 的回复:] [quote=引用 13 楼 dongjianzhu 的回复:] B没实例化,str这个引用保存在哪了?
如果把A中的aa()方法改成aaa(),为什么调的是A的方法了。[/quote] 如果改成aaa,则aaa与aa不在具备同一个token值,在A的构造方法的调用aaa处,此处给出了aaa的token值,这个值会被invokevirtual字节码使用。我举个例子把,A内aaa的token是n,B内aa的token是n+1类B的数据结构内会给出B内的方法token值的一个首值,同时还有一个tokencount指代有几个方法,这个token起始值自然就是n+1,tokencount是1.但是A内的aaa的token是n,tokencount也是1,在A的构造方法内调用aaa时,给出的token是n,这个由编译器决定。在B的类的结构内查找n的时候发现n不在B的n+1的范围内,则invokevirtal字节码会去B的父类内找n,发现n在A的范围内,则在类A的数据结构下取到aaa的地址调用过去。[/quote] 最后一句话更正下是在类A的数据结构下取到aaa的索引,然后再把这个索引转换为具体的方法地址,送给方法跳转函数,改变PC,跳转过去
  • 打赏
  • 举报
回复
动视飘雪 2014-06-10
引用 16 楼 dongjianzhu 的回复:
[quote=引用 13 楼 dongjianzhu 的回复:] B没实例化,str这个引用保存在哪了?
如果把A中的aa()方法改成aaa(),为什么调的是A的方法了。[/quote] 如果改成aaa,则aaa与aa不在具备同一个token值,在A的构造方法的调用aaa处,此处给出了aaa的token值,这个值会被invokevirtual字节码使用。我举个例子把,A内aaa的token是n,B内aa的token是n+1类B的数据结构内会给出B内的方法token值的一个首值,同时还有一个tokencount指代有几个方法,这个token起始值自然就是n+1,tokencount是1.但是A内的aaa的token是n,tokencount也是1,在A的构造方法内调用aaa时,给出的token是n,这个由编译器决定。在B的类的结构内查找n的时候发现n不在B的n+1的范围内,则invokevirtal字节码会去B的父类内找n,发现n在A的范围内,则在类A的数据结构下取到aaa的地址调用过去。
  • 打赏
  • 举报
回复
引用 13 楼 dongjianzhu 的回复:
B没实例化,str这个引用保存在哪了?
如果把A中的aa()方法改成aaa(),为什么调的是A的方法了。
  • 打赏
  • 举报
回复
动视飘雪 2014-06-10
引用 13 楼 dongjianzhu 的回复:
B没实例化,str这个引用保存在哪了?
B在new B()的第一个字节码已经存在了,同时在这个刚被创建的实例的数据域内也给str这个引用保留了其将来要被填入实际数值的位置,也可以叫这个str这个引用的空间已经开辟了但是还没用起来,这个时候他还是个0.
  • 打赏
  • 举报
回复
动视飘雪 2014-06-10
先普及两个知识: 1.在new B()的时候,用的是B的class来创建instance,instance创建需要先调用父类的构造方法,具体到字节码级别是这个样子的,在A的构造方法内的第一条字节码是一个invokespecial的字节码,他是直接调用其父类的构造方法,其父类的构造方法的第一条字节码也是一个invokespecial继续调用这个父类的父类的构造方式。在最基类即Object类内开始执行具体的字节码,Object的字节码完毕后退回到倒数第二层的某个父类的构造方法的字节码环境下执行这个倒数第二的父类的构造方法的内容,其他依次递推。 2.类内的每个方法除了private修饰的方法外均有一个独一无二的token来标识,子类覆写的父类的同名方法具备同一个token值,在使用instance.method的时候,虚拟机会根据instance来找到其对应的类的唯一标号,可以叫它类的ID,根据你源码中的method的名字编译阶段可以知道这个方法的token,然后根据这个token去前面那个instance的所属类的类的结构下去找一个方法ID。在每个类的数据结构下面有一个方法数组,该数组内存储着属于当前类的方法的方法ID,token即这个数据的索引。 。。。。。。。。。。。。。。。。。。。。。。。。。。。分割线。。。。 所以,因为你new B()这句话在解释的时候编译器使用的是B的class,B的实例会在构造方法前被一个叫new的字节码首先创建,在B的instance被创建后其后续的方法无论是构造方法还是构造方法调用的其他方法使用的类的ID都是B的,所以你的A的方法aa无法被调用,至于str为什么是null,因为使用的是B类内的str,那会儿还没初始化呢。数据域与方法均遵循一个token的排列规则,但是你这个str是private的,不参与token排列。只根据new B()的时候的new的字节码的实例的数据域内找到的那个位置就是个null。 可能我说的有点乱,不知道你看明白没有。
  • 打赏
  • 举报
回复
B没实例化,str这个引用保存在哪了?
  • 打赏
  • 举报
回复
你们都是根据结果,推算的执行流程
  • 打赏
  • 举报
回复
重写就调用那个重写的方法?str这个变量呢,被重写了?
  • 打赏
  • 举报
回复
加载更多回复(4)
相关推荐
发帖
Java SE

6.2w+

社区成员

Java 2 Standard Edition
社区管理员
  • Java SE
加入社区
帖子事件
创建了帖子
2014-06-09 10:45
社区公告
暂无公告