什么是抽象类
接口只声明行为,没有状态。普通类是完整实现的,可以被实例化。抽象类介于两者之间:它可以像普通类一样拥有字段、构造方法和已实现的方法,但也可以让某些方法不实现,并且禁止被直接实例化。用 abstract 关键字来标记它。
它的思路是把子类的所有共同点集中放在一处,同时强制每个子类补全真正不同的部分。
Animal 为所有子类统一定义了一次 getName(),并把 sound() 声明为 abstract:这是一个只有签名、没有方法体的方法,Dog 必须提供它的实现。
不能实例化抽象类
抽象类可能有未完成的方法,直接创建它会得到一个不完整的对象。编译器会拒绝这样做:
Animal a = new Animal("???"); // error: Animal is abstract; cannot be instantiated
你总是实例化一个具体子类,也就是实现了每个抽象方法的子类。然后可以把这个子类实例存到抽象类型的变量中,这正是使用抽象的方式。
抽象方法强制子类做出决定
abstract 方法是子类必须兑现的承诺。如果子类忘了实现某个抽象方法,这个子类本身也会变成抽象类,编译器会告诉你这一点。这是抽象类最主要的作用:保证某种行为一定存在,却不规定它具体做什么。
describe() 只在 Shape 中写了一次,却会调用每个子类自己的 area()。抽象类提供共同的框架,子类提供具体的细节。
共享状态与构造方法
和传统的接口不同,抽象类可以拥有实例字段并定义构造方法。构造方法永远不会单独创建出一个 Animal 或 Shape,它在创建子类时通过 super(...) 运行,用来初始化共享状态。
balance 字段、deposit 和 applyInterest 都集中在一处。只有真正会变化的策略 interestRate() 被保留为抽象方法。子类的构造方法必须调用 super(...) 来初始化继承来的状态。
一个陷阱:同时使用 abstract 和 final
abstract 和 final 是相反的。abstract 方法要求被重写,final 方法禁止被重写。把同一个东西同时标记成这两种,或者写一个 final 抽象类,都会导致编译错误。另外要记住,抽象类可以一个抽象方法都没有:仅仅为了防止被实例化而把类声明为 abstract 是合法的,对于只打算被继承的基类型有时也很有用。
abstract final class Bad { } // error: abstract and final conflict
abstract class Base {
abstract final void f(); // error: an abstract method can't be final
}
抽象类与接口
两者有重叠,选择哪个取决于你需要共享什么:
- 抽象类:用于共享状态和代码、关系紧密的类。
Savings和Checking都继承Account,继承了balance字段和deposit逻辑。一个类只能继承一个类。 - 接口:用于互不相关的类可以共享的某种能力。
Bird和Airplane都可以是Flyable,却不共享任何实现。一个类可以实现多个接口。
常见的模式是把两者结合起来:接口定义契约,抽象类实现样板代码,这样具体子类只需补全自己独有的部分。
下一步:多态
注意,上面每个例子都把子类实例存到抽象类型的变量中,然后调用方法,自动得到子类的行为。这种能力,即一个引用类型对应多种运行时行为,就是多态,它让抽象类和接口真正发挥价值。这就是下一页的内容。
常见问题
Java 抽象类是什么?
抽象类是用 abstract 关键字声明、不能单独实例化的类。它就是用来被继承的。它既可以包含完整实现的方法和字段(供子类共享的状态和代码),也可以包含没有方法体的 abstract 方法,这些方法由每个子类负责实现。
Java 中可以实例化抽象类吗?
不可以。new AbstractType() 会导致编译错误,因为抽象类可能有未实现的(abstract)方法,这样创建出的对象是不完整的。你应该实例化一个实现了所有抽象方法的具体子类,再把它存到抽象类型的变量中。
Java 中抽象类和接口有什么区别?
抽象类可以有实例字段、构造方法和部分实现的逻辑,但一个类只能继承一个抽象类。接口声明行为,没有实例状态,一个类可以实现多个接口。要在关系紧密的子类之间共享状态和代码,就用抽象类;要让互不相关的类具备某种共同能力,就用接口。