如果需要在子类方法中调用父类被覆盖的实例方法,则可使用super限定来调用父类被覆盖的实例方法。
public class Bird
{
//Bird类的fly方法
public void fly()
{
System.out.println("我在天空中飞翔...");
}
}
下面定义一个Ostrich类,这个类扩展了Bird方法,重写了Bird类的fly方法。
public class Ostrich extends Bird
{
//重写了Bird类的fly()方法
public void fly()
{
System.out.println(“我只能在地上奔跑…”);
}
public static void main(String[] args)
{
//创建Ostrich对象
Ostrich os = new Ostrich();
//执行Ostrich对象的fly()方法
os.fly();
}
}
为上面的Ostrich类添加一个方法,在这个方法中调用Bird类被覆盖的fly方法。
public void callOverridedMethod()
{
//在子类方法中通过super显式调用父类被覆盖的实例方法
super.fly();
}
借助callOverrideMethod()方法的帮助,就可以让Ostrich对象既可以调用字节重写的fly()方法,也可以调用Bird类中被覆盖的fly()方法(调用callOverridedMethod()方法即可)。
super是Java提供的一个关键字,super用于限定该对象调用它从父类继承得到的实例变量或方法。正如this不能出现在static修饰的方法中一样,super也不能出现在static修饰的方法中。static是属于类的,该方法的调用者可能是一个类,而不是对象,因而super限定也就失去了意义。
一、访问父类被覆盖的实例变量
如果在构造器中使用super,则super用于限定该构造器初始化的是该对象从父类继承得到的实例变量,而不是该类自己定义的实例变量。
如果子类定义了和父类同名的实例变量,则会发生子类实例隐藏父类实例变量的情形。在这种情况下,子类里定义的方法直接访问该实例变量默认会访问到子类中定义的实例变量,无法访问到父类中被隐藏的实例变量。在子类定义的实例方法中可以通过super来访问父类中被隐藏的实例变量。
如下代码所示。
class BaseClass
{
public int a = 5;
}
public class SubClass extends BaseClass
{
public int a = 7;
public void accessOwner()
{
System.out.println(a);
}
public void accessBase()
{
//通过super来限定访问从父类继承得到的a实例变量
System.out.println(super.a);
}
public static void main(String[] args)
{
SubClass sc = new SubClass();
sc.accessOwner();//输出7
sc.accessBase();//输出5
}
}
上面程序的BaseClass和SubClass中都定义了名为a的实例变量,则SubClass的a实例变量将会隐藏 BaseClass的a实例变量。当系统创建了SubClass对象时,实际上会为SubClass对象分配两块内存,一块用于存储在SubClass类中定义的a实例变量,一块用于存储从BaseClass类继承得到的a实例变量。
如果子类里没有包含和父类同名的成员变量,那么在子类实例方法中访问该成员变量时,则无需显式使用super或父类名作为调用者。如果在某个方法中访问名a的成员变量,但没有显式指定调用者,则系统查找a的顺序为:
(1)查找该方法中是否有名为a的局部变量
(2)查找当前类是否包含名为a的成员变量。
(3)查找a的直接父类中是否包含a的成员变量,依次上溯a的所有父类直到java.lang.Object类,如果最终没有找到名为a的成员变量,则系统出现编译错误。
如果被覆盖的是类变量,在子类的方法则可以通过父类名作为调用者来访问被覆盖的类变量。
提示:
当程序创建一个子类对象时,系统不仅会为该类中定义的实例变量分配内存,也会为它从父类继承到所有实例变量分配内存,即使子类定义了与父类中同名的实例变量。也就是说,当系统创建一个Java对象时,如果Java的两个父类(一个直接父类A,一个简洁父类B)。假设A中定义了2个实例变量,B类中定义了3个实例变量,当前类中定义了2个实例变量,那么这个Java对象将会保存2+3+2 = 8个实例变量。
class Parent
{
public String tag = "Parent";//①
}
class Derived extends Parent
{
//定义一个私有的tag实例变量来隐藏父类的tag实例变量
private String tag = "Derived";//②
}
public class HideTest
{
public static void main(String[] args)
{
Derived d = new Dervied();
//程序不可访问d的私有变量tag,所以下面语句将引起编译错误
//System.out.println(d.tag);//③
//将d变量显式地向上转型为Parent后,既可访问tag实例变量
//程序将输出:Parent
System.out.println(((Parent)d).tag);//④
}
}
上面程序①行粗体字代码为父类Parent定义了一个tag实例变量,②行粗体字代码为其子类定义了一个private的tag实例变量,子类中定义的这个实例变量将会隐藏父类中定义的tag实例变量。
程序的入口main()方法中先创建了一个Derived对象。这个Derived对象将会保存两个tag实例变量,一个是在Parent类中定义的tag实例变量,一个是在Derived类中定义的tag实例变量。此时程序中包括一个d变量,它引用一个Derived对象。
接着程序将Derived对象赋给d变量,挡在③行粗体字·代码处试图通过d来访问tag实例变量时,程序将提示访问权限不允许。这是因为访问哪个实例变量由声明该变量的类型决定,所以系统将会试图访问②行粗体字代码处定义tag实例变量;程序在④行粗体字代码处先将d变量强制向上转型为Parent类型,再通过它来访问tag实例变量时允许的,因为此时系统将会访问在①行粗体字代码处定义的tag实例变量,也就是输出“Parent”。
二、调用父类构造器
子类不会获得父类的构造器,但子类构造器可以调用父类构造器的初始化代码,类似于前面介绍的一个构造器调用另一个重载的构造器。
在一个构造器中调用另一个重载的构造器使用this调用来完成,在子类构造器中调用父类的构造器使用super调用来完成。
看下面的程序定义了Base类和Sub类,其中Sub类是Base类的子类,程序在Sub类的构造器中调用Base构造器的初始化代码。
class Base
{
public double size;
public String name;
public Base(double size , String name)
{
this.size = size;
this.name = name;
}
}
public class Sub extends Base
{
public String color;
public Sub(double size,String name,String color)
{
//通过super调用父类构造器的初始化过程
super(size , name);
this.color = color;
}
public static void main(String[] args)
{
Sub s = new Sub(5.6,"测试对象","红色");
//输出Sub对象三个实例变量
System.out.println(s.size + "--" + s.name + "--" + s.color);
}
}
从上面程序中不能看出,使用super调用和使用this调用也很像,区别在于super调用的是其父类的构造器,而this调用的是同一个类中重载的构造器。
因此,super调用父类构造器也必须出现在子类构造器执行体的第一行,所以this调用和super调用不会同时出现。
不管是否使用super调用来执行父类构造器的初始化代码,子类构造器总会调用父类构造器一次。子类构造器调用父类构造器分如下几种情况。
①子类构造器执行体的第一行使用super显式调用父类构造器,系统将根据super调用里传入的实参列表调用父类对于的构造器。
②子类构造器执行体的第一行代码使用this调用本类中 重载 的构造器,系统将根据this调用里传入的实参列表调用本类中的另一个构造器。执行本类中另一个构造器即会调用父类构造器。
③子类构造器执行体中既没有super调用,也没有this调用,系统会在执行子类构造器之前,隐式调用父类无参数的构造器。
不管哪种情况,当子类构造器来初始化子类对象时,父类构造器总是会在子类构造器之前执行;不仅如此,执行父类构造器时,系统会再次上溯其父类构造器……依次类推,创建任何Java对象,最先执行的总是java.lang.Object类的构造器。
下面程序定义了三个类,它们之间有严格的继承关系,通过这种继承关系让读者看到构造器之间的调用关系。
class Creature
{
public Creature()
{
System.out.println("Creature无参数的构造器");
}
}
Class Animal extends Creature
{
public Animal(String name)
{
System.out.println("Animal带一个参数的构造器," + "该动物的name为" + name);
}
public Animal(String name , int age)
{
//使用this调用同一个重载的构造器
this(name);
System.out.println("Animal带两个参数的构造器," + "其age为" + age );
}
public class Wolf extends Animal
{
public Wolf()
{
//显式调用父类有两个参数的构造器
super("灰太狼" , 3);
System.out.println("Wolf无参数的构造器");
}
public static void main(String[] args)
{
new Wolf();
}
}
}
上面程序main方法只创建了一个Wolf对象,但系统在底层完成了复杂的操作。
运行结果:
Creature无参数的构造器
Animal带一个参数的构造器,该动物的name为灰太狼
Animal带两个参数的构造器,其age为2
Wolf无参数的构造器
从上面运行结果来看,创建任何对象总是从该类所在继承树最顶端构造器开始执行,然后向下执行,最后才执行本类的构造器。如果某个父类通过this调用了重载的构造器,就会依次执行父类的多个构造器。
但是为什么我们创建Java对象时从未感觉到java.lang.Object类的构造器被调用过?
答:因为,自定义的类从未显示调用过java.lang.Object类的构造器,即使显式调用,java.lang.Object类也只有一个默认的构造器可被调用。当系统执行java.lang.Object类的默认构造器时,该构造器的执行体并未输出任何内容,所以你感觉不到调用过java.lang.Object类的构造器。