Java进阶---对象与内存控制(二)
2、父类构造器
无论是创建什么Java对象,程序总会依次调用该类的父类的非静态初始化块、父类构造器执行初始化,然后调用该类的非静态初始化块、构造器执行初始化。
1)、显式调用和隐式调用
如果大家记得显示初始化和隐式初始化的概念,那么显示调用和隐式调用就很好理解其概念了。一个对象的初始化过程如下:
l 当创建Java对象时,系统会在调用该类的非静态初始化块之前,先调用父类的非静态初始化块(这个调用是隐式执行的),然后调用父类的一个户哦多个构造器执行初始化(这个调用既可以是通过super进行显式调用,也可以是隐式调用)。
l 当所有父类的非静态初始化块、一个或多个构造器一次调用完成之后,系统会调用本类的非静态初始化块、构造器执行初始化。
通过对下面实例的执行过程的讲解,大家将更好地理解Java对象的初始化过程:
class Persion{
{
System.out.println("Persoin的非静态初始化块");
}
public Persion() {
System.out.println("Persoin的无参数的构造器");
}
public Persion(String name){
this();//调用Persion的无参重载构造器
System.out.println("Persion带有name参数的构造器,name参数:" + name);
}
}
class Man extendsPersion{
{
System.out.println("Man的非静态初始化块");
}
public Man(String name) {
super(name);
}
public Man(String name , int age) {
this(name);
System.out.println("Man带2个参数的构造器,其age:" + age);
}
}
class YangManextends Man{
{
System.out.println("YangMan的非静态初始化块");
}
public YangMan() {
super("小武" , 21);
System.out.println("YangMan的无参构造器");
}
public YangMan(int weight){
this();
System.out.println("YangMan的带weight参数的构造器,weight参数:" + weight);
}
}
public classtest01 {
public static void main(String[] args) {
new YangMan(55);
}
}
运行结果如图:
运行过程为:
A. newYangMan(55),跳转到YangMan类,在执行非静态代码块前跳转到Man类,在执行非静态代码块前跳转到Persion类;
B. Persion的非静态初始化代码块执行,输出图中第一行。因为YangMan显式调用了Man的双参构造器(super(“小武”, 21);),Man的双参构造器又调用了自身的另一个重载构造器(this(name);),Man的单参构造器又显式调用了Persion的单参构造器(super(name);),所以Persion会先调用自身的单参构造器执行初始化;
C. Persion的单参构造器调用了自身的无参构造器,所以输出图中的第二行,然后输出图中的第三行;
D. 之后初始化Man类,先对非静态代码块初始化,输出第四行;调用双参构造器,在其中调用了自身的单参构造器,输出第五行;输出第六行;
E. 之后初始化YangMan类,先对非静态代码块初始化,输出第七行;调用单参构造器,在其中调用了自身的无参构造器,输出第八行,输出第九行;
F. 在test01的主函数中返回YangMan实例;
下面关于调用父类的哪个构造器执行初始化,做一个分类讨论:
l 子类构造器执行体的第一行代码使用super显式调用父类构造器,系统将根据super调用里传入的实参列表来确定调用父类的哪个构造器;
l 子类构造器执行体的第一行代码使用this()显式调用本类中重载的构造器,系统将根据this里传入的实参列表来确定本类的另一个构造器(执行本类中另一个构造器时即进入第一种情况);
l 子类构造器执行体重既没有super调用,也没有this调用,系统将会在执行子类构造器之前,显式调用父类无参数的构造器
注意:super调用用于显示调用父类的构造器,this调用用于显式调用本类中另一个重载的构造器。Super调用和this调用都只能在构造器中使用,而且super调用和this调用都必须作为构造器的第一行代码,因此构造器中的super调用和this调用最多只能使用其中之一,而且最多只能调用一次。
2)、访问子类对象的实例变量
大家都知道子类的方法可以访问其父类的实例变量,但父类的方法不能访问子类的实例变量,因为父类根本无从知道它将被哪个子类继承,它的子类将会增加怎样的成员变量
下面介绍的这一情况却反其道而行,父类访问了子类的实例变量。先看代码,再看结果,最后看分析:
class Persion{
private String name = "小武";
public Persion() {
this.display();
}
public void display(){
System.out.println(name);
}
}
class Man extendsPersion{
private String name = "灵灵";
public Man() {
name = "小武灵灵";
}
public void display(){
System.out.println(name);
}
}
public classtest01 {
public static void main(String[] args) {
new Man();
}
}
看完了代码,相信大家心里都有一个此程序的输出结果,是什么呢?正确答案应该是null……下面我们从内存分配的角度来分析程序的执行过程:
A. newMan();执行,系统为Man中的name分配内存空间;由于Man继承了Persion,所以执行Man的构造器之前程序首先初始化Persion对象;
B. 程序为Persion对象的name分配内存空间,为了区分,我们将name称之为Persion.name;程序调用Persion类的构造器执行初始化,首先将Persion.name赋值为”小武”,再执行this.display();但是此处有一个疑问,this代表的是Persion还是Man?
C. 正确答案应该是Man。大家要记住这样一句话:当this在构造器中时,this代表正在初始化的Java对象。所以初始化过程中大家始终要记住初始化的是谁。在此程序中,Man构造器隐式调用了Persion构造器的代码,虽然this位于Persion构造器内,但这些代码实际是放在Man构造器内执行的,所以初始化对象应该是Man,此时我们可以将Persion称之为Man.Persion;
D. 为了证明C的正确性,大家可以在this.display()前加一句System.out.print(this.name),查看一下输出结果。输出结果为“小武null”。大家肯定又看糊涂了,既然this指的是Man,那为什么会输出“小武”呢?因为Man.Persion 指的是在Man构造器中的Persion对象,即Persoin被Man包裹在内,所以这里的this最终是位于Persion中的,它的编译时类型是Persion,而运行时类型是Man。举个例子,这里的Man是鸡蛋,Persion是蛋清,this是蛋黄,蛋黄最终会将鸡蛋进化成鸡,所以蛋黄(this)也代表了鸡蛋(Man),但蛋黄(this)却位于蛋清(Persion)中,虽然最终会进化为鸡蛋(编译时类型为Persion),但是需要吸收蛋清的养分(运行时类型是Man)。
E. 继续执行Persion中的构造器this.display();代码,既然this指的是Man,则调用Man中的display()方法,而此时Man中的name,即Man.name还是系统默认值null,所以最终结果会输出null。
总结,当变量的编译时类型和运行时类型不同时,通过该变量访问它所引用的对象的实例变量时,该实例变量的值由声明该变量的类型决定;当通过该变量调用它引用的对象的实例方法时,该方法行为将由它所引用的对象来决定。
如果这一段话不好理解,我可以这样改:还有这样一个规则:变量前都存在着变量所在对象的索引,被子类重写的方法前只存在重写该方法的子类的索引。例如有这样一个继承关系:A→B→C→D→......→N,其中A为顶层父类;在A对象中有变量a和被B,C,D...N重写的方法b(),则a实际上为A.a,而b()实际上为N.b()。
下面通过对程序进行改写,并给出输出结果,来帮助大家更好地理解上述概念:
class Persion{
private String name = "小武";
public Persion() {
System.out.println("Persion中的代码:这里输出的是this.name,即Persion.name:" + this.name);
System.out.println("Persion中的代码:下面执行this.display(),即Man.display()");
this.display();
}
public void display(){
System.out.println("这是Persion中的display()方法:" + name);
}
}
class Man extendsPersion{
private String name = "灵灵";
public Man() {
name = "小武灵灵";
}
public void display() {
System.out.println("这是Man中的display()方法:" + name);
}
}
public classtest01 {
public static void main(String[] args) {
new Man();
}
}
输出结果为:
3)、调用被子类重写的方法
大家都知道子类可以调用父类的方法,但父类不能调用子类的方法,因为父类根本无从知道它将被哪个子类继承,它的子类将会增加怎样的方法。
下面介绍一种特殊情况,当子类重写了父类的方法之后,父类表面上只是调用属于自己的、被子类重写的方法,但随着执行context的改变,将会变成父类实际调用子类的方法。下面来看一个例子:
class Persion{
private String name;
public Persion() {
this.name = getName();
}
public String getName(){
return "小武";
}
public String toString(){
returnname;
}
}
public classtest01 extends Persion{
private String name;
private int age;
public test01(String name , int age) {
this.name = name;
this.age = age;
}
@Override
public String getName() {
return "名字:" + name +",年龄:" + age;
}
public static void main(String[] args) {
System.out.println(newtest01("灵灵", 21));
}
}
现公布输出结果:名字:null,年龄:0
再来看执行过程:通过之前讲的父类调用子类的实例变量,相信大家会从内存分配的角度分析这一程序的执行过程,如果大家对父类调用子类的实例变量一节看的仔细,会发现我已经把结论告诉了大家,即当变量的编译时类型和运行时类型不同时,并通过该变量调用它引用的对象的实例方法时,该方法行为将由它所引用的对象来决定。
那么,我们很快就找出了以上程序的关键代码:this.name = getName(); 这行代码属于Persion,但test01继承了Persion,我们在初始化test01时调用了Persion的无参重载构造函数,这里的getName()前省略的this.,根据上一节讲的内容,大家肯定知道this指代的是test01,且getName()应该为test01.getName(),所以结论很快得出。
总结,如果父类构造器调用了被子类重写的方法,且通过子类构造器来创建子类对象时,调用(不管是隐式调用还是显示调用)了这个父类构造器,就会导致子类的重写方法在子类构造器的所有代码之前被执行,从而导致了子类的重写方法访问不到子类的实例变量值的情形。所以为了避免这种结果,应该避免在父类的构造其中调用被子类重写过的方法。