父类构造方法也调用子类重写好的方法,这里其实是子类在调用父类的构造方法,方法内继续调用父类的aa方法,碰巧aa方法子类重写过了,那就继续调用子类的了。
null 这个跟类的预先加载与依需求加载有关系 。JAVA类装载器在装载类的时候是按需加载的,只有当一个类要使用(使用new 关键字来实例化一个类)的时候,类加载器才会加载这个类并初始化。new B(); 开始执行时,因为A 类是父类,所以A类先加载到内存中并初始化,当A类的构造方法中的aa 继续调回到子类B中的时候,按需加载,先加载aa方法,这时候,B 类的 str = "B"; 尚未初始化,自然就是null了。
楼主可以给A B 两个类打断点,跟一下,可以帮助理解。
类加载
null 这个跟类的预先加载与依需求加载有关系 。JAVA类装载器在装载类的时候是按需加载的,只有当一个类要使用(使用new 关键字来实例化一个类)的时候,类加载器才会加载这个类并初始化。new B(); 开始执行时,因为A 类是父类,所以A类先加载到内存中并初始化,当A类的构造方法中的aa 继续调回到子类B中的时候,按需加载,先加载aa方法,这时候,B 类的 str = "B"; 尚未初始化,自然就是null了。
楼主可以给A B 两个类打断点,跟一下,可以帮助理解。
类加载
这里所说的str是B中的str变量,不是A中的。
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。
可能我说的有点乱,不知道你看明白没有。
如果改成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的地址调用过去。
如果改成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的地址调用过去。
最后一句话更正下是在类A的数据结构下取到aaa的索引,然后再把这个索引转换为具体的方法地址,送给方法跳转函数,改变PC,跳转过去
工作一年了,发现对这些概念很混乱。
看看写的博文:透析Java本质-类的初始化顺序
http://blog.csdn.net/xiaohulunb/article/details/26264841
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