CodeGym /课程 /JAVA 25 SELF /抽象与层次结构的实现

抽象与层次结构的实现

JAVA 25 SELF
第 19 级 , 课程 2
可用

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
abstract
abstract
-
Circle 已实现 已实现
radius
Rectangle 已实现 已实现
width, height

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();
}

现在任何 LineEllipse 都可以移动,而无需重新实现该方法。

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:混淆抽象类与接口。
当存在公共状态和/或部分实现时使用抽象类;当只需要“承诺”方法而不存储数据、不提供实现时使用接口。非必要不要混用。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION