3、父类与子类实例的内存控制
继承是面向对象的三大特征之一,也是Java语言的重要特性,而父子继承关系是Java编程中需要重点注意的地方。
3、1 继承成员变量和继承方法的区别
class Base {
int count = 2;
public void display() {
System.out.println(this.count);
}
}
class Derived extends Base {
int count = 20;
@Override
public void display() {
System.out.println(this.count);
}
}
public class FieldAndMethodTest {
public static void main(String[] args) {
Base b = new Base(); //声明并创建Base对象 ①
System.out.println(b.count);
b.display();
Derived d = new Derived(); //声明并创建Derived对象 ②
System.out.println(d.count);
d.display();
Base bd = new Derived(); //声明一个Base变量,并将Derived对象赋给该变量 ③
System.out.println(bd.count);
bd.display();
Base db1 = d; //让db1变量指向原d变量所指的Derived对象 ④
System.out.println(db1.count);
}
}
输出结果为:
2
2
20
20
2
20
2
在Base类和Derived类中,分别定义了一个count实例变量和display()方法。在①行代码处,声明了一个Base变量b,并将一个Base对象赋给该变量,在②行代码处,声明了一个Derived变量d,并将一个Derived对象赋给该变量。所以前4行结果输出的结果都是变量对应的结果。在③行代码处,声明了一个Base变量bd,却将Derived对象赋给该变量,那么直接通过db访问count实例变量,输出的将是声明时类型的对象的count实例变量,也就是Base类的count实例变量;如果通过db来调用display()方法,该方法表现的是运行时类型对象的行为,也就是Derived对象的行为。在④行代码处,将变量d赋值给db1变量,这意味着db1和d变量同时指向同一个Java对象,但是分别访问它们的实例变量时却输出不同的值,这就是表明:db1、d变量所指向的Java对象包含两块内存,分别是存放count值为2和20的实例变量。
当通过变量调用方法时,方法的行为总是表现出它们实际类型的行为,但如果通过这些变量访问它们所指对象的实例变量,这些实例变量的值总是表现出声明这些变量所用类型的行为。这也就是Java继承在成员变量和方法时的区别所在。
如果子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统也就不可能把父类的方法移动到子类中。对于实例变量则不存在这种现象,即使子类中定义了与父类完全同名的实例变量,这个实例变量也依然不可能覆盖父类中定义的实例变量。
正是因为成员变量和继承方法之间存在的差别,所以对于一个引用类型的变量而言,当通过该变量访问它所引用的实例变量时,该实例变量的值取决于声明该变量时的类型;当通过该变量来调用它所引用的对象的方法时,该方法行为取决于它所实际引用的对象类型。
3、2 内存中子类的实例
class Base {
int count = 2;
}
class Mid extends Base {
int count = 20;
}
public class Sub extends Mid {
int count = 200;
public static void main(String[] args) {
Sub s = new Sub(); //声明并创建Sub对象
Mid sm = s; //将Sub对象上转型赋值给Mid类型变量
Base sb = s; //将Sub对象上转型赋值给Base类型变量
System.out.println(s.count);
System.out.println(sm.count);
System.out.println(sb.count);
}
}
输出结果为:
200
20
2
程序输出的结果表示,s,sm和sb三个变量所引用的Java对象拥有三个count实例变量,也就是在内存中需要三块内存存储它们。其内存中的存储示意图如下所示。
但是实际上Sub类实际上只定义了一个count实例变量,另外两个count实例变量时Mid、Base所定义的。
class Fruit {
String color = "未定义";
public Fruit getThis() { //该方法用于返回调用该方法的实例
return this;
}
public void info() {
System.out.println("Fruit方法");
}
}
public class Apple extends Fruit {
String color = "红色";
@Override
public void info() {
System.out.println("Apple方法");
}
public void SuperInfo() {
super.info();
}
public Fruit getSuper() {
return super.getThis();
}
public static void main(String[] args) {
Apple a = new Apple();
Fruit f = a.getSuper();
System.out.println("a和f所引用的对象是否相同:" + (a == f));
System.out.println("访问a所引用对象的color实例对象:" + a.color);
System.out.println("访问f所引用对象的color实例对象:" + f.color);
a.info();
f.info();
a.SuperInfo();
}
}
输出结果为:
a和f所引用的对象是否相同:true
访问a所引用对象的color实例对象:红色
访问f所引用对象的color实例对象:未定义
Apple方法
Apple方法
Fruit方法
Fruit类定义的getThis()方法和Apple类定义的getSuper()方法,试图达到的效果是:当一个Apple对象调用getSuper()方法时,该方法返回该Apple对象的所谓的“默认父类对象”。
提示
:Java程序允许某个通过return this;返回调用该方法的Java对象,但是不允许直接使用return super;,甚至不允许直接把super当成一个引用变量使用。super关键字本身并没有引用任何对象,甚至不能被当做一个真正的引用变量使用。
从输出的结果可以看出,通过Apple对象的getSuper()方法返回的依然是Apple对象本身,只是声明类型是Fruit。因此通过f访问color实例变量时,该实例变量的值由Fruit类决定,但是调用info()方法时,该方法的行为有f变量实际所引用的Java对象(Apple类)决定。
当程序创建一个子类对象时,系统不仅会为该类中定义的实例变量分配内存,也会为其父类中定义的所有实例变量分配内存,即使子类定义了与父类同名的实例变量。如果在子类中定义了与父类中已有变量同名的变量,那么子类中定义的变量会隐藏父类中定义的变量。
注意:不是完全覆盖
,因此在创建子类对象时,依然会为父类中定义的、被隐藏的变量分配内存空间。
为了在子类方法中访问父类中定义的、被隐藏的实例变量,或者为了在子类中调用父类中定义的、被覆盖的方法,可以使用super.作为限定来修饰这些事例变量和事例方法。
3、3 子类和父类的类变量
由于类变量本质上属于类本身,因此通常不会涉及到实例变量那样复杂的状况,但是由于Java允许通过对象来访问类变量,因此也可以使用super.作为限定来访问父类中定义的类变量。