继承是面向对象的3大特征之一,也是Java语言的重要特性。而父、子继承关系则是Java编程中需要重点注意的地方。下面,我们将谈谈很少注意到的继承的一个重要细节——继承成员变量和方法其实是有所不同的!
子类方法可以访问父类的实例变量,因为子类继承了父类。但父类方法不能访问子类的实例变量,这是因为父类根本不知道它将被哪个子类继承。
但是,在极端情况下,可能出现父类访问子类变量的情况。下面是一个实例:
class Base
{
private int i=2;
public Base()
{
this.display();
}
public void display()
{
System.out.println(i);
}
}
class Sup extends Base
{
private int i=22;
public Sup()
{
i=222; //②
}
public void display()
{
System.out.println(i);
}
}
public class Test
{
public static void main(String[] args)
{
new Sup(); //①
}
}
猜猜上面程序的输出结果是什么?22?222?2?。运行程序,输出结果为0。很奇怪吧?
下面,我们来详细介绍一下程序的运行过程。
为了解释这个程序,首先需要澄清一个概念:Java对象是由构造器创建的吗?相信你的答案会是:是的。
但实际情况是:构造器只是负责对Java对象实例变量执行初始化(赋初始值),在执行构造器代码之前,该对象所占内存已经被分配下来了,这些内存中默认值都是空值。
1、程序执行到①代码处时,系统会为Sup对象分配内存空间(两块内存,其中一个属于Base类定义的i实例变量,一个属于Sup类定义的i实例变量,值都是0)。
2、在理解接下来的内容之前,我们要先对一个概念理解清楚(若不能理解则先记住,会对后面的理解有帮助)。即:当变量的编译时类型和运行时类型不同时,通过该变量访问它引用的对象的实例变量时,该值由编译时类型决定。而访问它引用的对象的实例方法时,该方法的行为将由运行时类型所决定。
接下来,我们再来讲解。在执行Sup构造器前会执行Base构造器。经过编译器处理后,该构造器包含如下两行代码。
i=2;
this.display();
这时的this代表谁?
回答这个问题之前,先进行简单的修改。将Base类构造器改为如下形式
class Base
{
System.out.println(this.i);
this.dispaly();
}
现在,构造器包含如下三行代码:
i=2;
System.out.println(this.i);
this.display();
再次运行程序,将看到输出为2、0。此时的this又代表着谁?
在《疯狂Java讲义》中我们知道:当this在构造器中时,this代表正在初始化的Java对象。 此时的this位于Base构造器内,但这些代码实际却放在Sup构造器内执行(因为Sup构造器隐式调用了Base构造器)。所以此时的this应该是Sup对象。
既然this代表Sup对象,那么为什么this.i输出为2呢?这是因为,这个this虽然代表Sup对象,但却位于Base构造器中,它的编译类型为Base,而它实际引用一个Sup对象。(相信看到这里的你已经懵逼到不行了,其实,在没继续往下看之前我也是这样的,嘿嘿。)为了证实这一点,我们继续改写一下程序:
为Sup类增加一个way()方法,然后再改写Base构造器
public Base()
{
System.out.println(this.i);
this.display();
//输出this实际的类型,将看到输出Sup
System.out.println(this.getClass());
//因为this的编译类型依旧为base,所以不能调用way()方法
//this.way();
}
其实,就像前面说的。当变量的编译时类型和运行时类型不同时,通过该变量访问它引用的对象的实例变量时,该值由编译时类型决定。而访问它引用的对象的实例方法时,该方法的行为将由运行时类型所决定。因此,this.i为2,而this.dislay()时,输出的i是Sup对象的i,即0。
在访问权限允许情况下,子类可以调用父类方法,这是因为子类继承父类。但父类不能调用子类的方法,因为父类不知道会被哪个子类继承。
但有一种特殊情况,当子类重写了父类方法后,父类表面上只是调用属于自己的方法,实际上,随着context改变,将会变成父类实际调用子类的方法。
下面是一个父类调用到了子类方法的实例
class Animal
{
private String desc;
public Animal()
{
this.desc=getDesc(); //②
}
public String getDesc()
{
return "Animal";
}
public String toString()
{
return desc;
}
}
public class Wolf extends Animal
{
private String name;
private double weight;
public Wolf(String name,double weight)
{
this,name=name; //③
this.weight=weight;
}
//重写父类的getDesc()方法
@Override
public String getDesc()
{
return "Wolf[name="+name+",weight="+weight+"]";
}
public static void main(String[] args)
{
System.out.println(new Wolf("战狼",32)); //①
}
}
1、程序从①代码处调用Wolf类构造器初始化Wolf对象
2、执行Wolf构造器(即③)之前,系统会隐式执行其父类无参数构造器(即②)。执行②时不再是调用父类的getDesc方法,而是调用子类的getDesc方法。因此输出结果为Wolf[name=null,weight=0.0]。
3、当执行完②代码后,程序才会对name,weight两个实例变量赋值。
由上面分析可以看出,这种输出的原因在于,②代码处调用的getDesc方法被子类重写了一遍。所以导致赋值步骤位于输出之后。
我们应该修改代码如下:
class Animal
{
public String getDesc()
{
return "Animal";
}
public String toString()
{
retrurn getDesc();
}
经过改写的Animal不再提供构造器(系统提供无参构造器)。这就保证了对Wolf对象实例变量的赋值语句在getDesc()方法前被执行,使得getDesc方法拿到对象的实例变量值。
如果父类构造器调用了被子类重写的方法,且通过子类构造器来创建子类对象,调用了这个父类构造器,就会导致子类的重写方法在子类构造器的所有代码之前被执行,从而导致子类的重写方法得不到想要的结果。
继承成员变量和继承方法之间存在这样的差别:当变量的编译时类型和运行时类型不同时,通过该变量访问它引用的对象的实例变量时,该值由编译时类型决定。而访问它引用的对象的实例方法时,该方法的行为将由运行时类型所决定!