1. 引言

在本教程中,我们将深入探讨Java中两个重要概念:内部类和子类。这两种方式在Java中定义类,但它们在用法上存在显著差异。理解它们的区别和适用场景,能帮助我们写出更优雅的面向对象代码。

2. Java中的子类

面向对象编程的核心原则之一是继承。继承允许一个类(子类)继承另一个类(父类)的属性和行为。继承和子类的使用促进了代码重用,并使类能够按层次结构组织。

子类与其父类定义了"is-a"关系,即子类的对象也是其父类的对象。这支持了多态的概念,并允许我们通过共同的父类来操作不同子类的实例,从而实现更通用的编码。

定义和使用子类还可以创建高度特化的类,这些类可以扩展和覆盖父类的特定功能。这符合SOLID原则中的开闭原则。

3. Java中的内部类

内部类是Java中嵌套类的一种形式,它定义在另一个宿主类的边界内。

Java中有多种内部类,例如嵌套内部类、静态内部类、方法局部内部类和匿名内部类。尽管这些内部类彼此略有不同,但内部类的核心思想保持不变。这种安排促进了更紧密的封装,并提高了可读性,因为内部类在外部类之外没有用途。因此,这种方法提供了一种改进的类分组方式。

内部类始终与外部类位于同一个文件中。除非我们定义了静态内部类,否则我们不能在不使用外部类实例的情况下实例化内部类。

4. 子类的必要性

本节我们将通过一个通知系统的例子来展示子类的特性。通知模块的核心组件是一个Notifier类,其目的是发送通知:

public class Notifier {
    void notify(Message e) {
        // 通过邮件发送消息的实现细节
    }
}

Message类封装了要发送的消息内容。

这个Notifier类太通用,没有定义发送不同类型通知的方式。如果系统只能发送邮件,这个类工作正常。但是,如果我们想扩展系统的能力以支持其他通信渠道,如短信或电话,它就不够用了。

其中一种方法是在这个类中定义多个方法,每个方法负责通过特定渠道发送通知:

public class Notifier {
    void notifyViaEmail(Message e) {
        // 通过邮件发送消息的实现细节
    }

    void notifyViaText(Message e) {
        // 通过短信发送消息的实现细节
    }

    void notifyViaCall(Message e) {
        // 拨打电话的实现细节
    }
}

❌ 这种方法有很多缺点:

  • Notifier类承担了太多职责,并且了解太多不同渠道的细节
  • 每增加一个新渠道,类的规模就会成倍增加
  • 这种设计没有考虑某些通信渠道本身的不同实现需求,例如RCS、SMS/MMS

为了解决这些问题,我们借助面向对象编程中的继承范式进行重构。

我们将Notifier指定为系统中的基类或父类,并将其设为抽象类:

public abstract class Notifier {
    abstract void notify(Message e);
}

通过这个改变,Notifier类不再知道通知逻辑或渠道。它依赖其子类来定义行为。我们所有的通信渠道都与Notifier表现出"is-a"关系,例如EmailNotifier是一个Notifier:

public class EmailNotifier extends Notifier {
    @Override
    void notify(Message e) {
        // 提供邮件特定的实现
    }
}

public class TextMessageNotifier extends Notifier {
    @Override
    void notify(Message e) {
        // 提供短信特定的实现
    }
}

public class CallNotifier extends Notifier {
    @Override
    void notify(Message e) {
        // 提供电话特定的实现
    }
}

通过这种方法,我们可以根据需要将系统扩展到任意多的特定渠道的实现。我们可以根据需要定义特定子类实现的实例并使用它:

void notifyMessages() {
    // 发送短信
    Message textMessage = new Message();
    Notifier textNotifier = new TextMessageNotifier();

    textNotifier.notify(textMessage);

    // 发送邮件
    Message emailMessage = new Message();
    Notifier emailNotifier = new EmailNotifier();

    emailNotifier.notify(emailMessage);
}

我们在Java JDK中可以找到大量继承和子类的使用。以Java的Collection API为例。在Collections API中有一个AbstractList类,它被LinkedList、ArrayList和Vector等具体实现所扩展,这些类都是它的子类。

5. 内部类的必要性

内部类有助于将重要的代码结构本地化,同时仍然以类的形式封装它们。 在我们之前的例子中,不同的通知器使用不同的底层通知逻辑,这些逻辑可能彼此差异很大。

例如,邮件通知器可能需要SMTP服务器的信息和其他发送邮件的逻辑。另一方面,短信通知器则需要一个发送短信的电话号码。在所有这些通知器中,共享的代码很少。它们也只在各自的上下文中才有用。

我们的EmailNotifier实现需要SMTP服务器的信息和访问权限来发送邮件。我们可以将与连接和发送邮件相关的模板代码编写为一个内部类:

class EmailNotifier extends Notifier {
    @Override
    void notify(Message e) {
        // notify方法的实现
    }

    // 用于邮件连接的内部类
    static class EmailConnector {
        private String emailHost;
        private int emailPort;
        // Getter和Setter

        private void connect() {
            // 连接到smtp服务器
        }
    }
}

然后,我们可以在外部类的notify()方法中使用内部类来发送邮件:

@Override
void notify(Message e) {
    // 连接到邮件连接器并发送邮件
    EmailConnector emailConnector = new EmailConnector();
    emailConnector.connect();
    // 发送邮件
}

在Java JDK中,内部类的使用可以在List接口的LinkedList实现中找到。 Node类被创建为静态内部类,因为它在LinkedList之外的使用没有意义且不必要。HashMap类的设计也采用了类似的方法,它使用Entry作为内部类。

6. 子类与内部类的区别

我们可以总结Java中子类和内部类的区别如下:

  • 内部类总是与外部类位于同一个文件中,而子类可以是独立的文件
  • 内部类通常不能访问外部类的成员变量或方法(除非是非静态内部类),而子类可以访问其父类的成员
  • 我们不能直接实例化内部类(除非是静态内部类),而子类可以直接实例化
  • 创建内部类主要是用作小的辅助类,而子类则用于覆盖父类的功能

7. 结论

在本文中,我们讨论了子类和内部类,以及它们在编写模块化面向对象代码中的作用。我们还研究了它们之间的区别以及何时选择使用其中一种。

完整实现可以在GitHub上找到:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-lang-oop-inheritance


原始标题:Inner Classes vs. Subclasses in Java