第七章 多态
继承允许将对象视为它自己本身的类型或其基类型来加以处理。这种能力很重要,因为它允许将多种类型视为同一类型来处理,而同一份代码也就可以毫无差别地运行在这些不同类型之上了。
一个对象变量可以指向多种实际类型的现象被称为多态。而在运行时自动选择正确的方法进行调用的现象被称为“动态绑定”。
这种把对某个对象的引用视为对其基类型的引用的做法被称为“向上转型”。
动态绑定:
1)编译器检查对象的声明类型和方法名
2)编译器检查方法调用中提供的参数类型
3)若方法是private、final或static的,或者是一个构造器,则编译器能准确判断应调用哪个方法,称为静态绑定;而对于其他方法,要调用哪个方法只有根据隐式参数的实际类型来决定,并在运行时使用动态绑定。
4)当程序运行并且使用动态绑定调用方法时,虚拟机必须调用同隐式参数所指对象的实际类型相匹配的方法版本。
将某个方法声明为final有两个作用:防止别人覆盖该方法;可以有效关闭“动态绑定”,告诉编译器不需要对其进行动态绑定,可以生成更有效的代码。但是,最好根据设计来决定是否使用final,而不是出于试图提高性能的目的。
不能有这样的两个方法,它们具有相同的名字和参数类型,但是返回值却不同。这样会使得编译器不知该调用哪个方法。
class Haa{
private void f(){
System.out.println("private");
}
}
public class Ha extends Haa{
public void f(){
System.out.println("public");
}
public static void main(String[] args){
Ha aHa=new Ha();
aHa.f();
}}//private方法被自动认为是final方法,而且对导出类是屏蔽的。Ha中的f方法是一
//个全新的方法。而且基类中的f方法在Ha中不可见,所以也不能被重载
仅有声明而没有方法体的是抽象方法。包含一个或多个抽象方法的类是抽象类,不需要所有的方法都是抽象的。如果从一个抽象类继承,并创建新类的对象,就要为新类中的所有抽象方法提供方法定义。
抽象类可以有具体方法,接口不可以。
构造器的工作顺序,如下
class Meal {
Meal() { System.out.println("Meal()"); }
}
class Bread {
Bread() { System.out.println("Bread()"); }
}
class Cheese {
Cheese() { System.out.println("Cheese()"); }
}
class Lettuce {
Lettuce() { System.out.println("Lettuce()"); }
}
class Lunch extends Meal {
Lunch() { System.out.println("Lunch()"); }
}
class PortableLunch extends Lunch {
PortableLunch() { System.out.println("PortableLunch()");}
}
public class Sandwich extends PortableLunch {
private Bread b = new Bread();
private Cheese c = new Cheese();
private Lettuce l = new Lettuce();
public Sandwich() {
System.out.println("Sandwich()");
}
public static void main(String[] args) {
new Sandwich();
}
} ///:~ 输出为
Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
Sandwich()
从这个例子中可以看出,在复杂对象调用构造器时,要遵照的顺序为:
1) 调用基类构造器(并执行基类中的初始化语句)
2) 按声明顺序调用成员的初始化方法
3) 调用导出类构造器的主体
但是,按照我的观察,如果把上述程序改为:
class a{
a(){System.out.println("Hello");}}
class Meal {
a aa=new a();
Meal() { System.out.println("Meal()"); }
}
class Bread {
Bread() { System.out.println("Bread()"); }
}
class Cheese {
Cheese() { System.out.println("Cheese()"); }
}
class Lettuce {
Lettuce() { System.out.println("Lettuce()"); }
}
class Lunch extends Meal {
Lunch() { System.out.println("Lunch()"); }
}
class PortableLunch extends Lunch {
PortableLunch() { System.out.println("PortableLunch()");}
}
public class Sandwich extends PortableLunch {
private Bread b = new Bread();
private Cheese c = new Cheese();
private Lettuce l = new Lettuce();
public Sandwich() {
System.out.println("Sandwich()");
}
public static void main(String[] args) {
new Sandwich();
}
} ///:~ 输出为
Hello
Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
Sandwich()
红色的代码是新添加的。由此我们可以看出,在调用基类构造器时,是将基类中的初始化语句也执行了的。所以在上面的总结中添加了括号中的红色说明。
一般来说,通过继承和组合来创建新类时,永远不必担心对象的清理问题。如果确实需要清理的问题,那么必须为新类创建dispose()方法,或者在导出类中覆盖dispose()方法。当覆盖基类的dispose()方法时,务必一定要调用基类版本的dispose()方法。
对于子对象和字段,销毁的顺序应该和初始化(子对象)和声明(字段)的顺序相反。并且,先对导出类清理,后对基类清理。
看看下面这个例子:
abstract class Glyph {
abstract void draw();
Glyph() {
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph {
private int radius = 1;
RoundGlyph(int r) {
radius = r;
System.out.println(
"RoundGlyph.RoundGlyph(), radius = " + radius);
}
void draw() {
System.out.println(
"RoundGlyph.draw(), radius = " + radius);
}
}
public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
} ///:~ 输出为
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
逻辑上看似可以,其实结果却不是我们想要的。所以,初始化的顺序为:
1)在其他任何事务发生之前,将分配给对象的存储空间初始化成二进制的零
2)调用基类构造器(并执行基类中的初始化语句)
3)按声明顺序调用成员的初始化方法
4) 调用导出类构造器的主体
当不能十分确定应该使用继承还是组合时,更好的方式是首先选择组合。一条通用的准则是:用继承表达行为间的差异,并用字段表示状态上的变化。
在Java中,所有类型转换都会得到检查。这一点对于向下转型尤其重要。