如何理解继承成员变量和方法的区别?

  继承是面向对象的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方法拿到对象的实例变量值。
  

如果父类构造器调用了被子类重写的方法,且通过子类构造器来创建子类对象,调用了这个父类构造器,就会导致子类的重写方法在子类构造器的所有代码之前被执行,从而导致子类的重写方法得不到想要的结果。

总结

  继承成员变量和继承方法之间存在这样的差别:当变量的编译时类型和运行时类型不同时,通过该变量访问它引用的对象的实例变量时,该值由编译时类型决定。而访问它引用的对象的实例方法时,该方法的行为将由运行时类型所决定

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值