1. 构建层次:从抽象到细节
实现抽象与层次结构,是一种将代码从通用规则组织到具体细节的方式。我们先描述所有对象必须具备的能力(抽象),再细化每个具体类如何实现这些能力。
在编程中如同在生活里,一切从问题开始。比如:“圆和矩形有什么共同点?”答案:它们都是图形。图形有什么共同点?通常它们有面积,并且可以被绘制。
在 Java 中,这通过 abstract 类来表达:
public abstract class Shape {
public abstract double area();
public abstract void draw();
}
这里的意思是:
- 任何图形都必须能计算自己的面积(area())。
- 任何图形都必须能被绘制(draw())。
- 至于具体怎么做——暂且不关心。
现在创建具体图形:
public class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public void draw() {
System.out.println("绘制半径为 " + radius + " 的圆");
}
}
public class Rectangle extends Shape {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
@Override
public void draw() {
System.out.println("绘制矩形 " + width + "x" + height);
}
}
我们做了什么?
- 把共性上移到抽象类。
- 在子类中细化具体行为。
示意:
. Shape
/ \
Circle Rectangle
表格:各处实现了什么
| 类 | area() | draw() | 自有字段 |
|---|---|---|---|
| Shape | |
|
- |
| Circle | 已实现 | 已实现 | |
| Rectangle | 已实现 | 已实现 | |
2. 为什么这很方便?(以及它为何有效)
统一接口,用同一种方式处理不同对象
假设你有一个图形集合:
Shape[] shapes = {
new Circle(5),
new Rectangle(3, 4),
new Circle(2.5)
};
你可以以相同方式遍历它们,不必关心具体类型:
for (Shape shape : shapes) {
shape.draw();
System.out.println("面积:" + shape.area());
}
让 JVM 自己去搞清楚谁是圆、谁是矩形!这就是多态(我们已经提过,下一部分还会更详细地说明)。
易于扩展
想要添加三角形?直接写(新类型——旧代码不改):Triangle extends Shape。
public class Triangle extends Shape {
private double base, height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
@Override
public double area() {
return 0.5 * base * height;
}
@Override
public void draw() {
System.out.println("绘制三角形:底边 " + base + ",高 " + height);
}
}
其余代码(例如遍历图形列表)无需修改。
避免代码重复
如果所有图形都有一个共同属性(例如颜色),就可以把它提到抽象类中:
public abstract class Shape {
private String color = "黑色";
public String getColor() { return color; }
public void setColor(String color) { this.color = color; }
public abstract double area();
public abstract void draw();
}
现在任何子类——无论是圆还是三角形——都会“继承”到颜色。
3. 实践:开发一个迷你图形编辑器
让我们把一切整合起来。设想你在做一个简单的图形编辑器。
抽象类 Figure
public abstract class Figure {
private String color = "black";
public String getColor() { return color; }
public void setColor(String color) { this.color = color; }
public abstract void draw();
public abstract void resize(double factor);
}
具体图形
public class Line extends Figure {
private double length;
public Line(double length) {
this.length = length;
}
@Override
public void draw() {
System.out.println("绘制长度为 " + length + " 的线条,颜色为 " + getColor());
}
@Override
public void resize(double factor) {
length *= factor;
System.out.println("线条的新长度:" + length);
}
}
public class Ellipse extends Figure {
private double a, b;
public Ellipse(double a, double b) {
this.a = a;
this.b = b;
}
@Override
public void draw() {
System.out.println("绘制椭圆,长短轴分别为 " + a + " 和 " + b + ",颜色为 " + getColor());
}
@Override
public void resize(double factor) {
a *= factor;
b *= factor;
System.out.println("椭圆的新轴:" + a + "," + b);
}
}
想添加一个新工具,例如 Polygon?只需创建一个新类——编辑器的其余代码都通过抽象的 Figure 工作。
在代码中的使用
Figure[] figures = {
new Line(10),
new Ellipse(5, 3)
};
for (Figure figure : figures) {
figure.setColor("red");
figure.draw();
figure.resize(1.5);
}
输出:
绘制长度为 10.0 的线条,颜色为 red
线条的新长度:15.0
绘制椭圆,长短轴分别为 5.0 和 3.0,颜色为 red
椭圆的新轴:7.5,4.5
层次结构可视化
. Figure
/ \
Line Ellipse
4. 如何避免重复:公共字段和方法
有时所有子类不仅有共同方法,还有共同字段(例如中心坐标)。抽象类是放置它们的理想位置:
public abstract class Figure {
private double x, y; // 中心坐标
public Figure(double x, double y) {
this.x = x;
this.y = y;
}
public void moveTo(double newX, double newY) {
x = newX;
y = newY;
System.out.println("图形已移动到点 (" + x + ", " + y + ")");
}
public abstract void draw();
}
现在任何 Line 或 Ellipse 都可以移动,而无需重新实现该方法。
5. 另一个示例:支付系统
抽象不仅仅是图形!设想你在编写一个支付处理系统。
抽象类 Payment
public abstract class Payment {
public abstract void process();
}
具体实现
public class CreditCardPayment extends Payment {
@Override
public void process() {
System.out.println("处理信用卡支付");
}
}
public class PaypalPayment extends Payment {
@Override
public void process() {
System.out.println("通过 PayPal 处理支付");
}
}
使用
Payment[] payments = {
new CreditCardPayment(),
new PaypalPayment()
};
for (Payment payment : payments) {
payment.process();
}
输出:
处理信用卡支付
通过 PayPal 处理支付
6. 这种方法的优点
- 统一接口:可以用同一种方式操作不同对象。
- 可扩展性:添加新类型对象无需重写旧代码。
- 最少重复:共性上移到基础抽象类。
- 灵活性:可以使用抽象类型的集合,而不关心细节。
7. 现实中的例子:交通
抽象不仅存在于教材中。比如你在设计交通管理系统:
public abstract class Transport {
public abstract void move();
public abstract void fuelUp();
}
具体的交通工具实现细节:
public class Car extends Transport {
@Override
public void move() {
System.out.println("汽车在道路上行驶");
}
@Override
public void fuelUp() {
System.out.println("加注汽油");
}
}
public class Bicycle extends Transport {
@Override
public void move() {
System.out.println("自行车通过踩踏前进");
}
@Override
public void fuelUp() {
System.out.println("自行车不需要燃料,只需要给骑手提供三明治!");
}
}
8. 有用的示意:如何构建抽象层次
[抽象类]
|
[具体子类]
|
[更具体的子类] (如需)
- 通用的都放在上面!
- 特有的都放在下面!
9. 实现抽象与层次结构时的常见错误
错误 № 1:在子类中重复代码。
如果你发现每个子类都在写相同的字段或方法——这意味着它们应该被提到抽象类中。如果能减少重复,不要害怕把抽象“做宽”。
错误 № 2:违背“自上而下(先共性后特性)”的原则。
有时新手会从细节开始构建层次,忽视共性。结果出现诸如 RedCircleWithShadow 这样的奇怪类,难以融入整体结构。务必先提炼抽象,再落实细节。
错误 № 3:层次过深。
如果你的继承链超过 3–4 层,想一想:是否该用组合或接口来替代继承?
错误 № 4:强迫实现不相关的方法。
如果抽象类包含过多对某些子类并不相关的抽象方法,也许应当重新审视结构。比如,并不是所有交通工具都需要 fuelUp() 方法(自行车就不需要)。
错误 № 5:混淆抽象类与接口。
当存在公共状态和/或部分实现时使用抽象类;当只需要“承诺”方法而不存储数据、不提供实现时使用接口。非必要不要混用。
GO TO FULL VERSION