父类构造方法也调用子类重写好的方法,这里其实是子类在调用父类的构造方法,方法内继续调用父类的aa方法,碰巧aa方法子类重写过了,那就继续调用子类的了。
null 这个跟类的预先加载与依需求加载有关系 。JAVA类装载器在装载类的时候是按需加载的,只有当一个类要使用(使用new 关键字来实例化一个类)的时候,类加载器才会加载这个类并初始化。new B(); 开始执行时,因为A 类是父类,所以A类先加载到内存中并初始化,当A类的构造方法中的aa 继续调回到子类B中的时候,按需加载,先加载aa方法,这时候,B 类的 str = "B"; 尚未初始化,自然就是null了。
楼主可以给A B 两个类打断点,跟一下,可以帮助理解。
类加载

解决方案 »

  1.   

    2楼说的对,我在补充一点就是:java中如果一个类继承了另一个类那么java总是先调用父类的构造函数,创建父类,然后才会调用子类的构造函数创建子类,事实上在子类的构造函数中有个隐藏的调用super(),这个是调用父类的无参数的构造函数创建父类的,这句话必须写在子类的构造函数的第一行,如果没有写JVM在编译成字节码的时候会自动帮你加上的,当然如果父类没有无参数的构造函数,这种默认机制就会报错了,但是放心,这种错误是编译时的,你可以很容易的通过一些IDE(比如eclipse)的智能提示发现错误。
      

  2.   

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

  3.   


    这里所说的str是B中的str变量,不是A中的。
      

  4.   

    class B extends class A,new B()时,先创建class A的实例,class A的构造函数中调用aa(),可是aa被子类class B重写了,所在class A的构造函数中调用的是class B的aa(),此时class B中的str变量尚未初始化,所以输出null;class B初始化完成后,构造放方法中调用自己的aa(),所以输出"B"
      

  5.   

    要弄懂这个建议你看看《Java编程思想》中的类加载那一章,看别人消化过的知识,当时你是懂了但是很快就会忘,还是看看必须看的书吧。
      

  6.   

    重写就调用那个重写的方法?str这个变量呢,被重写了?
      

  7.   

    B没实例化,str这个引用保存在哪了?
      

  8.   

    先普及两个知识:
    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。
    可能我说的有点乱,不知道你看明白没有。
      

  9.   

    B在new B()的第一个字节码已经存在了,同时在这个刚被创建的实例的数据域内也给str这个引用保留了其将来要被填入实际数值的位置,也可以叫这个str这个引用的空间已经开辟了但是还没用起来,这个时候他还是个0.
      

  10.   

      如果把A中的aa()方法改成aaa(),为什么调的是A的方法了。
      

  11.   

      如果把A中的aa()方法改成aaa(),为什么调的是A的方法了。
    如果改成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的地址调用过去。
      

  12.   

      如果把A中的aa()方法改成aaa(),为什么调的是A的方法了。
    如果改成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,跳转过去
      

  13.   

    这个玩意我研究了。
    工作一年了,发现对这些概念很混乱。
    看看写的博文:透析Java本质-类的初始化顺序
    http://blog.csdn.net/xiaohulunb/article/details/26264841
      

  14.   

    Str=“B”,没有初始化,B类中的aa方法已经加载了吗?
      

  15.   

    是的,JVM按需加载。因为B类重写了A类的aa方法,所以在A类调用的时候,会调用到B类的aa方法,所以B类会优先加载aa方法,此时,str 尚未加载,或者说尚未初始化,所以还是null
      

  16.   

    其实用在字节码层面可以很清晰的看到过程
    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