CodeGym /课程 /JAVA 25 SELF /多态与抽象的常见问题

多态与抽象的常见问题

JAVA 25 SELF
第 23 级 , 课程 3
可用

1. 多态:它是什么,有什么用

如果你以为多态是 Marvel 变种人世界里的东西,恐怕要让你失望了:在编程里它要平静得多,但同样神奇。多态指的是具有不同实现的对象对同样的方法调用做出不同的响应的能力。

生活中的例子:
你有类 Book 和类 Magazine,它们都继承自抽象类 LibraryItem。你希望对任何图书馆条目都可以调用方法 printInfo(),并让它输出相应信息——对书籍来说是作者和书名,对杂志来说是期号和日期。

示例代码:

abstract class LibraryItem {
    String title;

    LibraryItem(String title) {
        this.title = title;
    }

    abstract void printInfo();
}

class Book extends LibraryItem {
    String author;

    Book(String title, String author) {
        super(title);
        this.author = author;
    }

    @Override
    void printInfo() {
        System.out.println("书籍:" + title + ",作者:" + author);
    }
}

class Magazine extends LibraryItem {
    int issueNumber;

    Magazine(String title, int issueNumber) {
        super(title);
        this.issueNumber = issueNumber;
    }

    @Override
    void printInfo() {
        System.out.println("杂志:" + title + ",期号:" + issueNumber);
    }
}

现在可以创建一个包含不同元素的数组,并为每个元素调用 printInfo()

LibraryItem[] items = {
    new Book("蝇王", "威廉·戈尔丁"),
    new Magazine("科学与生活", 5)
};

for (LibraryItem item : items) {
    item.printInfo();
}
// 输出:
// 书籍:蝇王,作者:威廉·戈尔丁
// 杂志:科学与生活,期号:5

这就是多态的工作方式!

2. 多态中的常见错误

试图调用基类中不存在的方法

最常见的错误之一,是通过基类引用去调用只在子类中声明的方法。

LibraryItem item = new Book("哈利·波特", "J. K. 罗琳");
// item.getAuthor(); // 编译错误!在 LibraryItem 中没有方法 getAuthor()

Java 根据变量的静态类型(LibraryItem),而不是对象的实际类型(Book)来编译代码。因此如果你需要调用书籍特有的方法,就需要进行类型转换:

if (item instanceof Book) {
    Book book = (Book) item;
    // 现在可以调用 book.getAuthor()
}

不做检查就进行类型转换

如果你确信对象是 Book,但实际上并不是,你会在运行时得到 ClassCastException。例如:

LibraryItem item = new Magazine("Forbes", 12);
Book book = (Book) item; // 砰!ClassCastException

正确做法——总是先检查类型:

if (item instanceof Book) {
    Book book = (Book) item;
    // OK
} else {
    System.out.println("这不是书!");
}

没有利用多态的优势

有时开发者把代码写得对具体类型强耦合,而其实可以使用抽象。例如,如果你这样写:

Book[] books = ...;
for (Book book : books) {
    book.printInfo();
}

这只对书籍有效。如果明天又出现杂志、报纸、漫画呢?最好使用数组 LibraryItem[] 并通过基类或接口中的方法来工作。

3. 抽象:为何需要以及如何用得恰当

抽象类与接口

抽象是一种抓住本质、隐藏细节的艺术。在 Java 中,我们有抽象类与接口来实现这一点。

  • 抽象类——不能直接实例化,只能被继承的类。
  • 接口——一种契约:规定类应该做什么,而不是如何去做。

错误 1:创建没有抽象方法的抽象类

如果你的抽象类一个抽象方法都没有,请三思——它真的需要是抽象的吗?也许直接做成普通类更简单?

abstract class UselessAbstract {
    void sayHello() {
        System.out.println("Hello!");
    }
}
// 如果没有抽象方法,最好做成普通类

错误 2:子类未实现必需的方法

如果一个类继承了抽象类或实现了接口,就必须实现所有抽象方法。若忘记实现——编译器会提醒,但有时方法被“为了应付”而实现却什么也不做。这对维护很不友好。

class Magazine extends LibraryItem {
    Magazine(String title, int issueNumber) {
        super(title);
        // ...
    }

    @Override
    void printInfo() {
        // 空的!不好!
    }
}

错误 3:过深或混乱的抽象层级

当类彼此继承达到五到十层时,就会变得非常难以理解。最好做成“扁平”的层级,让一切清晰。

反例:

LibraryItem
  |
  BookItem
    |
    PrintedBook
      |
      IllustratedBook
        |
        ChildrenIllustratedBook

很复杂,不是吗?最好控制在两到三层。

4. 实践:在教学应用中运用多态与抽象

让我们完善你的图书馆教学应用。之前你只有书籍。现在添加杂志,并为出版物实现一个通用接口。

先声明一个抽象类:

abstract class LibraryItem {
    protected String title;

    public LibraryItem(String title) {
        this.title = title;
    }

    public abstract void printInfo();
}

添加子类:

class Book extends LibraryItem {
    private String author;

    public Book(String title, String author) {
        super(title);
        this.author = author;
    }

    @Override
    public void printInfo() {
        System.out.println("书籍:" + title + ",作者:" + author);
    }
}

class Magazine extends LibraryItem {
    private int issueNumber;

    public Magazine(String title, int issueNumber) {
        super(title);
        this.issueNumber = issueNumber;
    }

    @Override
    public void printInfo() {
        System.out.println("杂志:" + title + ",期号:" + issueNumber);
    }
}

使用多态:

LibraryItem[] items = {
    new Book("代码整洁之道", "罗伯特·马丁"),
    new Magazine("Java World", 3)
};

for (LibraryItem item : items) {
    item.printInfo();
}

为电子出版物添加接口

假设有些出版物可以在线阅读。我们引入一个接口:

interface ReadableOnline {
    void openOnline();
}

class EBook extends Book implements ReadableOnline {
    private String url;

    public EBook(String title, String author, String url) {
        super(title, author);
        this.url = url;
    }

    @Override
    public void openOnline() {
        System.out.println("正在打开该电子书,地址:" + url);
    }
}

现在可以通过接口来操作电子书:

ReadableOnline ebook = new EBook("Java 入门", "巴里·伯德", "https://example.com/java");
ebook.openOnline();

5. 如何避免多态与抽象中的问题:最佳实践

  • 使用接口和抽象类描述行为,而不是状态。
    例如,接口 Printable 很适合描述“可打印”这一能力,但在接口里放一个 String title 字段就是个坏主意。
  • 在进行类型转换之前,用 instanceof 检查类型。
    尤其当对象可能有多种实现时。这能避免 ClassCastException
  • 尽量保持“扁平”、清晰的继承层级。
    层级越简单——越容易维护与扩展代码。
  • 避免“无意义”的抽象。
    如果类不包含抽象方法,也不打算被继承——就不要把它做成抽象类。
  • 重写方法时始终使用注解 @Override
    这有助于编译器捕获签名错误。

6. 使用多态与抽象时的典型错误

错误 1:未检查就进行类型转换

有时想“走捷径”直接强转。这也许能跑,但也可能导致程序突然崩溃。务必使用 instanceof

if (item instanceof Book) {
    Book book = (Book) item;
    // ...
}

错误 2:通过基类引用调用子类方法

LibraryItem item = new Book("Java", "作者");
item.getAuthor(); // 编译错误:在 LibraryItem 中没有这样的方法!

解决办法——要么进行类型转换,要么将所需方法添加到基类(如果这在语义上合理)。

错误 3:不完整地实现接口或抽象类

如果忘了实现接口的所有方法——编译器不会让项目通过。但如果只是写了什么也不做的“空方法”,则会导致意料之外的行为。

错误 4:过深的继承层级

如果你的继承层级超过三层——请考虑能否简化架构。

错误 5:违反单一职责原则

如果一个抽象承载了太多职责,它会变得难以维护。最好将其拆分为多个接口或类。

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