Java 进阶与实战应用
高级面向对象特性
高级抽象:抽象类与接口
我们已经知道,继承允许一个类获取另一个类的属性和方法。但有时,我们需要定义一个不完整的“模板”类,它只规定了其子类必须具备哪些功能,而不关心具体如何实现。这就是抽象发挥作用的地方。
抽象类就像一个半成品蓝图。它既可以包含已经实现的方法,也可以包含只有声明没有方法体的“抽象方法”。任何继承抽象类的子类,都必须实现所有这些抽象方法,否则它自己也必须被声明为抽象类。
abstract class Vehicle {
// 这是一个已实现的方法
public void startEngine() {
System.out.println("Engine starting...");
}
// 这是一个抽象方法,没有方法体
// 它规定了所有车辆都必须能移动,但具体怎么动由子类决定
public abstract void move();
}
class Car extends Vehicle {
@Override
public void move() {
System.out.println("Car is driving on four wheels.");
}
}
另一方面,接口则是纯粹的“契约”或“能力规范”。它只定义了一组相关的行为,任何类只要实现了这个接口,就承诺自己具备这些行为。在Java 8之前,接口只能包含抽象方法和常量。它强制实现类提供所有方法的具体实现,从而实现了行为和实现的分离。
简单来说:抽象类回答了“它是什么”的问题(例如,一辆车是一种交通工具),而接口回答了“它能做什么”的问题(例如,它能飞、能游泳)。
| 特性 | 抽象类 (abstract class) | 接口 (interface) |
|---|---|---|
| 多重继承 | 不支持。一个类只能继承一个父类。 | 支持。一个类可以实现多个接口。 |
| 构造函数 | 有。用于初始化状态,由子类调用。 | 没有。接口不能被实例化。 |
| 成员变量 | 可以包含任何类型的成员变量。 | 只能包含 public static final 常量。 |
| 方法实现 | 可以包含抽象方法和已实现的方法。 | Java 8前只能有抽象方法,之后可包含默认和静态方法。 |
| 核心思想 | 强烈的 “is-a” 关系,共享代码。 | 定义一种 “can-do” 能力,规定行为契约。 |
接口的进化:Java 8及以后
在Java 8之前,接口有一个很大的局限性:一旦发布,就很难修改。如果你想给一个广泛使用的接口(比如Java集合库里的 List 接口)添加一个新方法,那么所有实现这个接口的类都将编译失败,因为它们没有实现这个新方法。这是一个巨大的维护噩梦。
为了解决这个问题,Java 8引入了“默认方法”。
默认方法
noun
在接口中使用 default 关键字修饰的方法。它提供了方法的默认实现,实现该接口的类可以直接使用这个默认实现,而无需自己提供代码。当然,如果类需要,也可以重写这个默认方法。
interface Loggable {
// 这是一个抽象方法,所有实现类都必须提供它
void log(String message);
// 这是一个默认方法,提供了默认实现
default void logInfo(String message) {
log("INFO: " + message);
}
}
class ConsoleLogger implements Loggable {
@Override
public void log(String message) {
System.out.println(message);
}
// 无需实现 logInfo,可以直接使用默认版本
}
// 使用
ConsoleLogger logger = new ConsoleLogger();
logger.logInfo("System ready."); // 输出: INFO: System ready.
除了默认方法,Java 8还允许在接口中定义静态方法。这些方法和类中的静态方法类似,它们属于接口本身,而不是接口的任何实现对象。这通常用于提供一些辅助或工具性质的功能。
interface Connection {
String getStatus();
// 静态的工厂方法
static Connection create() {
// 内部可以有一些复杂的创建逻辑
return new DefaultConnection();
}
}
// 使用
Connection myConnection = Connection.create();
组合优于继承
继承是强大的,但它也带来了问题。它创建了一种非常紧密的耦合关系。子类不仅继承了父类的功能,还继承了它的内部实现。如果父类的实现发生变化,可能会意外地破坏子类的功能。这种关系被称为“白盒复用”,因为子类必须了解父类的内部工作方式。
此外,Java只支持单继承,这限制了代码复用的灵活性。如果你想让一个 Bird 类同时具备 Flyable 和 Singable 的能力,通过继承就很难实现。
因此,有一个重要的设计原则:“组合优于继承”。
这个原则建议,类应该通过包含其他类的实例来获得功能(组合),而不是通过继承它们(继承)。这是一种“黑盒复用”,因为类只与它所包含对象的公共接口交互,而不关心其内部实现。
让我们来看一个例子。假设我们有一个 Duck 类,有些鸭子会飞,有些会叫。用继承来实现会很笨拙。
如果我们使用组合,可以把“飞”和“叫”的行为定义为独立的接口,并创建它们的具体实现类。然后,Duck 类只需持有这些行为接口的实例,并在需要时委托它们执行任务。
// 行为接口
interface FlyBehavior { void fly(); }
interface QuackBehavior { void quack(); }
// 行为的实现
class FlyWithWings implements FlyBehavior {
public void fly() { System.out.println("I'm flying!"); }
}
class CannotFly implements FlyBehavior {
public void fly() { System.out.println("I can't fly."); }
}
class Duck {
// Duck类包含行为的实例,而不是继承它们
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public Duck(FlyBehavior fb, QuackBehavior qb) {
this.flyBehavior = fb;
this.quackBehavior = qb;
}
// 委托给行为对象
public void performFly() {
flyBehavior.fly();
}
public void performQuack() {
quackBehavior.quack();
}
}
// 创建一个会飞的鸭子
Duck mallard = new Duck(new FlyWithWings(), /* QuackBehavior impl */);
mallard.performFly(); // 输出: I'm flying!
通过组合,我们可以动态地改变对象的行为,系统也变得更加灵活和可维护。
多态的运行时机制
多态是OOP的另一大支柱,它允许我们用父类型的引用指向子类型的对象。这样做的好处是,我们可以编写通用的代码来处理一系列相关的对象,而无需关心它们的具体类型。
考虑这个例子:
Vehicle myCar = new Car();
Vehicle myBike = new Bicycle();
// 尽管myCar和myBike的类型都是Vehicle
// 但实际调用的move()方法却不同
myCar.move(); // 调用Car类的move方法
myBike.move(); // 调用Bicycle类的move方法
这是如何工作的呢?这背后是“动态绑定”或“后期绑定”在起作用。
-
编译时:编译器检查
myCar的类型是Vehicle。它会确认Vehicle类(或其父类)中确实有一个名为move()的方法。如果找不到,编译就会失败。编译器此时只关心方法签名是否存在,不关心具体实现。 -
运行时:当代码实际执行
myCar.move()时,Java虚拟机(JVM)会查看myCar变量当前实际引用的对象是什么。它发现这个对象是一个Car实例。 -
方法调用:于是,JVM会去
Car类中查找move()方法的实现并执行它。这个过程发生在程序运行时,因此被称为动态绑定。
这种机制让我们的系统具有极高的可扩展性。我们可以随时添加新的 Vehicle 子类(比如 Motorcycle),只要它正确实现了 move() 方法,处理 Vehicle 的现有代码无需任何修改就能与新类协同工作。
在Java中,“组合优于继承”这一设计原则主要是为了解决继承带来的哪个问题?
Java 8在接口中引入“默认方法”(default methods)主要是为了解决什么问题?