该文为《疯狂java 突破程序员基本功的16课》读书纪要。
注意以下代码:
class Test {
int num1 = num2 + 2;
static int num2 = 10;
}
在java中定义成员变量时,必须采用合法的前向引用,即若num1=num2+2,则必须先定论num2,当前,必须采用合法的前向引用的前提是:num1和num2同时为成员变量或类变量(static修饰),如上述代码中,num1为成员变更,num2为类变量时,则无须遵守这一要求。
有以下代码:
/**
*
*/
package com.gsoft.geloin;
import junit.framework.TestCase;
import org.junit.Test;
/**
* @author Geloin
*
*/
public class MyTest extends TestCase {
@Test
public void testArrays() throws Exception {
new Derived();
}
}
class Base {
private int i = 2;
public Base() {
this.display();
}
public void display() {
System.out.println(i);
}
}
class Derived extends Base {
private int i = 22;
public Derived() {
i = 222;
}
public void display() {
System.out.println(i);
}
}
Derived继承Base,按照JVM规则,调用new Derived()时,系统开始为这个Derived对象分配内存空间,此时,Derived对象有两个i实例变量,也即是说,系统会分配两个i的内存给Derived。与我们的认知不同: 构造器只是负责对Java对象实例变更进行初始化(也就是赋初始值),在执行构造器代码之前,该对象所占用的内存已经被分配下来,这些内存里值都是默认的空值。
当程序调用new Derived()方法时,系统会先为Derived对象分配内存空间,此时系统内存需要为这个Derived对象分配两块内存,它们分别用于存放Derived对象的两个i实例变量,其中一个i属于Base定义的实例变量,另一个i属于Derived对象定义的实例变量,此时,这两个i实例变量的值都是0。
接下来,程序在执行Derived的构造器之前,会首先执行Base的构造器,于是执行以下两行代码:
i = 2;
this.display();
即先将Base类中定义的i初始化为2,再调用this.display()方法,此处有一个关键:this代表谁?
当this在构造器中时,this代表正在初始化的Java对象。此时的情况是:此时的this位于Base构造器内,但目前正在初始化的Java对象是Derived——是Derived()构造器隐式调用了Base()构造器的代码。由此可见,此时的this应该代表Derived对象。
既然this代码Derived对象,那我们再改写Base构造子,将它变更成以下代码:
public Base() {
System.out.println(this.getClass());
System.out.println(this.i);
System.out.println(this.getClass());
this.display();
}
再次运行单元测试代码,发现结果如下所示:
class com.gsoft.geloin.Derived
2
class com.gsoft.geloin.Derived
0
好吧,问题来了,既然this,表示的是Derived,那么为什么打印this.i时,显示的是2呢?
这里涉及到另外一个概念:编辑时和运行时。编译时顾名思义就是正在编译的时候.而所谓的编译,就是编译器帮你把源代码翻译成机器能识别的代码,简单来说,编译时就是简单的作一些翻译工作;所谓运行时就是代码跑起来了.被装载到内存中去了。
需要注意的是:当变量的编辑时类型和运行时类型不同时,通过该变量访问它引用的对象的实例变量时,该实例变量的值由声明该变量的类型决定。但通过该变量调用它引用的对象的实例方法时,该方法行为将由它实际所引用的对象决定。
也就是说,在一个构造子中,调用this.变量名时,无论如何,这个this都是指它所在的类;当调用this.方法时,这个this则表示它实际上的类型。如上代码所示,调用this.i时,取得的是Base类中被初始化的i,而调用this.display()时,调用的则是Derived的display方法。
显然,通过以上代码,我们又要推翻我们以前的认知了——子类可以调用父类的方法,但父类不可能调用子类的方法——但实际上,如果子类重写的父类的方法,且父类的空构造子调用了该被重写的方法时,则父类能够调用子类的方法——如以上代码所描述。