第一部分:为什么需要抽象类和抽象方法?(理论解释)
想象一下,在面向对象编程中,我们用“类”来描述现实世界中的事物。但有些事物或概念本身是抽象的、不具体的。你无法在现实世界中找到一个叫做“动物”的东西,你能找到的都是具体的“狗”、“猫”、“鸟”。
“动物”就是一个抽象的概念,而“狗”、“猫”、“鸟”是它的具体实例。然而,“动物”这个概念是有意义的,因为我们可以总结出所有动物都具备的共同特征(比如:有生命、会吃东西)和共同行为(比如:会发出声音)。
Java中的抽象类和抽象方法,就是为了对这种“抽象概念”进行建模而设计的。
1. 抽象类的核心作用
抽象类(abstract class
)就像一个不完整的模板或者蓝图。它的核心作用有两点:
-
抽离共性,实现代码复用:
所有子类都具备的共同属性(成员变量)和共同行为(具体方法),可以直接在抽象父类中实现。这样,子类就不需要重复编写这些代码,直接继承即可。这遵循了不要重复自己(Don’t Repeat Yourself, DRY)的编程原则。 -
定义规范,强制子类实现:
有些行为,父类知道必须要有,但具体怎么做则由每个子类自己决定。例如,我们知道所有“动物”都会“发出声音”,但狗是“汪汪”叫,猫是“喵喵”叫。这种“只定义规范,不提供实现”的行为,就用抽象方法(abstract method
)来表示。任何继承这个抽象类的子类,都必须实现这个抽象方法,否则它自己也必须被声明为抽象类。这相当于一种强制性的约定或合同。
2. 抽象方法的角色
抽象方法(abstract method
)是一个只有方法声明,没有方法体(没有 {}
代码块)的方法。它存在的唯一目的,就是在父类层面规定一个行为契约。
- 它告诉所有子类:“嘿,如果你想成为我这一族的一员,你就必须拥有这个功能,并且要自己去实现它。”
3. 这样设计的优点
-
模板化设计:抽象类提供了一个模板,既包含了子类通用的功能(具体方法),又为子类必须实现的特定功能预留了位置(抽象方法)。这让类的层级结构非常清晰。
-
强制性约束:通过抽象方法,可以确保所有子类都具备某些必要的功能,避免了子类开发者忘记实现关键方法,从而保证了体系的完整性和健壮性。
-
拥抱多态(Polymorphism):这是最核心的优点之一。因为抽象类保证了所有子类都实现了某个方法(比如
makeSound()
),所以我们可以用父类(抽象类)的引用去指向任何一个子类的对象,并安全地调用那个方法。程序在运行时会自动调用该子类自己实现的版本。这让代码更加灵活、可扩展。 -
提高可维护性:如果所有子类共有的一个功能需要修改,我们只需要在抽象父类中修改一次即可,所有子类都会自动继承这个变更,大大提高了代码的可维护性。
第二部分:一个具体的例子:图形(Shape)
让我们用一个经典的“图形”例子来演示抽象类和抽象方法的威力。
场景:我们要创建一个程序来计算不同图形(如圆形、矩形)的面积。
分析:
- “图形”本身是一个抽象概念。你无法画一个“图形”,你只能画一个“圆形”或“矩形”。所以
Shape
类适合做成抽象类。 - 所有图形都有一些共性,比如它们都有一个“名字”。这个可以做成具体方法。
- 所有图形都能计算面积,但计算公式完全不同。圆形是 πr²,矩形是长×宽。所以,“计算面积”这个行为就适合做成抽象方法。
第1步:创建抽象父类 Shape
这个类定义了所有图形的“模板”。
// 定义一个抽象类 Shape
public abstract class Shape {
// 这是一个具体属性,所有子类都将拥有它
protected String name;
// 构造方法,用于初始化通用属性
public Shape(String name) {
this.name = name;
}
// 这是一个具体方法,提供了通用功能的实现,子类可以直接复用
public String getName() {
return this.name;
}
// 这是一个抽象方法,它只定义了规范(必须能计算面积),但没有提供具体实现
// 它强制所有子类必须自己实现这个方法
public abstract double calculateArea();
}
解读:
Shape
类被abstract
修饰,所以你不能new Shape()
。- 它有具体的属性
name
和具体的方法getName()
,这部分代码可以被所有子类复用。 - 它有一个抽象方法
calculateArea()
,这部分是规范,强制子类必须实现。
第2步:创建具体的子类 Circle
和 Rectangle
现在,我们来创建继承 Shape
的具体图形类。
// Circle 类继承了 Shape
public class Circle extends Shape {
private double radius;
public Circle(double radius) {
super("圆形"); // 调用父类的构造方法来设置名字
this.radius = radius;
}
// 必须实现父类中定义的抽象方法 calculateArea()
@Override
public double calculateArea() {
// 提供圆形的面积计算公式
return Math.PI * radius * radius;
}
}
// Rectangle 类继承了 Shape
public class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
super("矩形"); // 调用父类的构造方法
this.width = width;
this.height = height;
}
// 必须实现父类中定义的抽象方法 calculateArea()
@Override
public double calculateArea() {
// 提供矩形的面积计算公式
return width * height;
}
}
解读:
Circle
和Rectangle
都extends Shape
。- 它们都必须通过
@Override
来实现calculateArea()
方法,否则Java编译器会报错。这就是抽象类的强制约束作用。 - 它们都可以直接使用从
Shape
继承来的getName()
方法,这就是代码复用。
第3步:演示多态的威力
现在看看使用这些类有多么方便和灵活。
public class Main {
public static void main(String[] args) {
// 创建具体的图形对象
Shape circle = new Circle(10.0);
Shape rectangle = new Rectangle(5.0, 8.0);
// 使用一个 Shape 类型的数组来统一管理不同的图形
Shape[] shapes = {circle, rectangle};
// 遍历数组,调用每个图形的方法
for (Shape shape : shapes) {
// 调用的是父类 Shape 的具体方法 getName() - 代码复用
System.out.println("图形名称: " + shape.getName());
// 调用的是被子类重写的 calculateArea() - 多态
// 程序在运行时,会自动判断 shape 的真实类型(是Circle还是Rectangle)
// 然后调用对应子类中的 calculateArea() 方法
System.out.println("图形面积: " + shape.calculateArea());
System.out.println("--------------------");
}
}
}
运行结果:
图形名称: 圆形
图形面积: 314.1592653589793
--------------------
图形名称: 矩形
图形面积: 40.0
--------------------
解读:
- 在
main
方法中,我们用父类Shape
的引用(Shape circle = ...
)来指向子类的对象。 - 最关键的是
for
循环:循环中的shape
变量是Shape
类型,但它在运行时可以指向Circle
对象或Rectangle
对象。 - 当我们调用
shape.calculateArea()
时,Java虚拟机(JVM)会执行动态绑定:- 当
shape
指向Circle
对象时,调用的是Circle
类中的calculateArea()
。 - 当
shape
指向Rectangle
对象时,调用的是Rectangle
类中的calculateArea()
。
- 当
- 这就是多态。我们不需要写
if (shape instanceof Circle)
这样的判断,代码非常简洁优雅。这一切之所以能实现,正是因为抽象类Shape
保证了任何它的子类都必定有一个calculateArea()
方法。
总结
设计点 | 优点 | 在例子中的体现 |
---|---|---|
抽象类 | 提供模板:整合了通用部分和待实现部分。 | Shape 类既有 name 属性和 getName() 方法,也定义了 calculateArea() 的规范。 |
具体方法 | 代码复用:减少重复代码,提高可维护性。 | Circle 和 Rectangle 都能直接使用 getName() ,无需重写。 |
抽象方法 | 定义规范:强制子类实现特定功能,保证体系完整。 | Circle 和 Rectangle 都必须实现 calculateArea() ,否则编译不通过。 |
整体设计 | 实现多态:允许用统一的方式处理不同的子类对象,代码灵活、可扩展。 | main 方法中可以用 Shape 数组统一处理 Circle 和 Rectangle ,并调用各自的面积计算方法。 |