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