抽象类和抽象方法
抽象类:为所有由它派生出来的类创建一个公共的接口。要创建这种公共接口的唯一原因就是,各个子类要用它自己的方式来实现这个接口。它定义了一个基本的形式,你可以说这是所有的派生类所共有的。还有一种说法,就是 “抽象的基类(abstract base class 或者简化为抽象类abstract class)”。当你想要通过一个公共的接口来操控一组类的时候,就可以使用抽象类了。通过动态绑定机制,那些符合方法特征的派生类方法将会得到调用。
如果你有一个像Instrument 这样的abstract class,那么这种类的对
象是没什么意思的。也就是说Instrument 只是用来定义接口,而不是
具体实现,因此创建Instrument 对象没有意义,更何况你还可能要禁
止用户这么作。要做到这点,可以让Instrument 的方法打印错误信息,但是这样一来就把问题留到运行时了,因此用户端需要进行详尽的测
试。更好的办法还是在编译时发现这个问题。
Java 提供了一种被称为“抽象方法(abstract method)”的机制来解决
这个问题。这是一种尚未完成的方法;这种方法只有声明,没有正
文。下面就是抽象方法的声明:
abstract void f();
包含抽象方法的类被称为“抽象类(abstract class)”。如果类包含一个或多个抽象方法,那么这个类就必须被定义成abstract 的。(否则编译器就会报错了。)
既然抽象类是尚未完成的类,那么如果有人想要创建抽象类的对象的话,编译器又打算怎么做呢?编译器没法安全地创建抽象类对象,所以它会报错。由此,编译器保证了抽象类的纯粹性,而你也不用担心它会被误用了。
如果你继承了抽象类,而且还打算创建这个新类的对象,那你就必须实现基类所定义的全部抽象方法。如果你不这么做(你确实可以选择不这么做),那么这个继承下来的类就也成了抽象类了,编译器会强制你用abstract 关键词来声明这个类的。创建一个不包含abstract 方法的abstract 类,是完全可以的。这种技巧可以用于“不必创建abstract 的方法,但是又要禁止别人创建这个类的对象”的场合。
构造函数的调用顺序
在创建派生类对象的过程中,基类的构造函数总是先得到调用,这样一级一级的追溯上去,每个基类的构造函数都会被调用。这种做法是很合乎情理的,因为构造函数有一个特殊的任务:它要知道对象是不是被正确地创建了。派生类只能访问它自己的成员,它看不到基类的成员(因为它们通常都是private 的)。只有基类的构造函数才知道怎样初始化它的成员,同时也只有它才有权限进行初始化。因此“把所有的构造函数都调用一遍”就变得非常重要了,否则对象就没法创建了。这就是为什么编译器会强制每个派生类都要调用其基类的构造函数的原因了。如果你不在派生类的构造函数里明确地调用基类的构造函数,那编译器就会悄悄的调用那个默认的构造函数。如果没有默认构造函数,编译器就会报错。(要是类没有构造函数,编译器会自动为你准备一个默认构造函数。)
复杂对象的构造函数的调用顺序是这样的:
1. 调用基类的构造函数。这是一个递归过程,因此会先创建继承体系
的根,然后是下一级派生类,以此类推,直到最后一个继承类的构
造函数。
2. 成员对象按照其声明的顺序进行初始化。
3. 执行继承类的构造函数的正文。
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 {
private static Test monitor = new Test();
public static void main(String[] args) {
new RoundGlyph(5);
monitor.expect(new String[] {
"Glyph() before draw()",
"RoundGlyph.draw(), radius = 0",
"Glyph() after draw()",
"RoundGlyph.RoundGlyph(), radius = 5"
});
}
}
1. 在进行其它工作之前,分配给这个对象的内存会先被初始化为两进制的
零。
2. 正如前面一直在所说的,先调用基类的构造函数。这时会调用被覆写的draw( )方法(是的,在调用RoundGlyph 的构造函数之前调用),这
时它发现,由于受第一步的影响,radius 的值还是零。
3. 数据成员按照它们声明的顺序进行初始化。
4. 调用派生类的构造函数的正文。
一个好的构造函数应该,“用最少的工作量把对象的状态设置好,而且要尽可能地避免去调用方法。”构造函数唯一能安全调用的方法,就是基类的final 方法。