1. 抽象类与抽象方法
生活中(以及编程中)有时会想:“嗯,我不清楚具体该怎么做,但我确定这件事必须存在!” 例如,所有动物都应该会发出声音,但发出什么声音取决于具体的动物。为此,Java 引入了抽象类与抽象方法。
抽象类是不能被直接实例化的类(如果 Animal 是抽象的,就不能写 new Animal()),但它可以被继承。这样的类既可以包含普通(已实现)的实例方法,也可以包含抽象方法——即只声明而未实现的方法。
抽象方法没有方法体。它通过关键字 abstract 声明,并且必须由子类实现(除非该子类本身也是抽象类)。
生活中的例子
假设我们有一个动物园应用。我们希望所有动物都有方法 makeSound(),但并不知道它们各自发出的具体声音。那么我们就定义一个抽象类:
public abstract class Animal {
public abstract void makeSound(); // 抽象方法
}
具体的动物按各自的方式实现该方法:
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("汪汪!");
}
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("喵!");
}
}
此时,如果有人尝试创建 new Animal(),编译器会立刻说:“抱歉,抽象的动物在自然界中并不存在!” 这很有用:你可以保证程序中只会存在具有具体行为的具体动物。
2. 通过抽象实现多态
抽象是为一组对象提取共同的对外约定(契约)。抽象类正是用来定义这种共同契约:它规定所有子类必须实现哪些方法。
多态与抽象是搭档:抽象类保证所有子类都拥有所需的方法,而多态允许你通过基类引用来调用这些方法。
示例:搭建一个小型动物园
我们有抽象类 Animal 及其多个子类:
public abstract class Animal {
public abstract void makeSound();
}
public class Cow extends Animal {
@Override
public void makeSound() {
System.out.println("哞——!");
}
}
public class Duck extends Animal {
@Override
public void makeSound() {
System.out.println("嘎嘎!");
}
}
现在我们可以创建一个动物数组:
Animal[] zoo = {
new Dog(),
new Cat(),
new Cow(),
new Duck()
};
for (Animal animal : zoo) {
animal.makeSound(); // 会为每个动物调用“正确的”方法
}
数组中的每个对象都是具体的动物,但在代码中它们的静态类型都是 Animal。借助抽象与多态,我们可以确信每个对象都拥有 makeSound() 方法,并且会得到正确的行为。
3. 使用抽象类来实现多态
再看一个更贴近实际的例子。设想我们在开发一个公司员工管理应用。员工有不同的类型:经理、开发、测试。大家都有一个共同的方法 work(),但实现方式各不相同。
抽象类 Employee
public abstract class Employee {
protected String name;
public Employee(String name) {
this.name = name;
}
public abstract void work();
}
具体子类
public class Manager extends Employee {
public Manager(String name) {
super(name);
}
@Override
public void work() {
System.out.println(name + " 领导团队。");
}
}
public class Developer extends Employee {
public Developer(String name) {
super(name);
}
@Override
public void work() {
System.out.println(name + " 编写代码。");
}
}
public class Tester extends Employee {
public Tester(String name) {
super(name);
}
@Override
public void work() {
System.out.println(name + " 测试应用程序。");
}
}
多态的使用
现在我们可以创建一个员工数组,并对每个员工调用 work() 方法:
Employee[] employees = {
new Manager("Anna"),
new Developer("Ivan"),
new Tester("Maria")
};
for (Employee e : employees) {
e.work();
}
输出结果:
Anna 领导团队。
Ivan 编写代码。
Maria 测试应用程序。
请注意:在循环中我们既不知道(也不想知道!)员工的具体类型。我们只调用 work(),每个对象都会完成自己的工作。
4. 一些有用的细节
实现保证
抽象类会强制所有子类实现所需的方法。如果你忘了在子类中实现某个抽象方法,编译器会立刻提醒你:“你必须实现它!”
通用接口
处理抽象类型的数组或列表(如 Employee[]、List<Animal>)的代码可以做到高度通用。你可以添加新的子类——而无需修改主流程代码。
防止“来路不明”的对象
由于抽象类不能被直接实例化,没人会误创建一个“不明所以”的类型对象,而该对象又没有实现所需的方法。
理论与语法:如何声明抽象类与方法
- 抽象类:在 class 之前加上关键字 abstract。
- 抽象方法:使用关键字 abstract 声明且没有方法体(只有分号)。
- 只要类中至少有一个抽象方法,该类就必须声明为抽象类。
- 继承抽象类的类必须实现其所有抽象方法,否则该类本身也必须是抽象的。
示意
public abstract class Animal {
public abstract void makeSound();
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("喵!");
}
}
5. 使用抽象类的常见错误
错误 1:尝试实例化抽象类。
像 new Animal() 这样的代码无法编译。抽象类就像没有配件的家具装配说明书:在出现具体子类之前,你无法把对象“组装”出来。
错误 2:在子类中忘记实现抽象方法。
如果你声明了抽象方法,却没有在子类中实现它(也没有把子类声明为抽象类),编译器会报错。
错误 3:忽略访问修饰符的约束。
重写方法的访问级别不能比基类更严格。比如,如果抽象方法是 public,那么实现也必须是 public(而不是 protected 或 private)。
错误 4:把抽象方法写成了有方法体的样子。
abstract 方法不能有方法体,否则编译器会翻白眼并说:“请先想清楚——要么是抽象,要么是实现!”
错误 5:把多态指望在静态方法上。
多态只对实例方法有效。静态方法不会被重写——它们是隐藏的,因此调用时的行为取决于变量的静态类型,而不是实际对象。
GO TO FULL VERSION