No history yet

高级面向对象特性

高级抽象:抽象类与接口

我们已经知道,继承允许一个类获取另一个类的属性和方法。但有时,我们需要定义一个不完整的“模板”类,它只规定了其子类必须具备哪些功能,而不关心具体如何实现。这就是抽象发挥作用的地方。

抽象类就像一个半成品蓝图。它既可以包含已经实现的方法,也可以包含只有声明没有方法体的“抽象方法”。任何继承抽象类的子类,都必须实现所有这些抽象方法,否则它自己也必须被声明为抽象类。

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 类同时具备 FlyableSingable 的能力,通过继承就很难实现。

因此,有一个重要的设计原则:“组合优于继承”。

这个原则建议,类应该通过包含其他类的实例来获得功能(组合),而不是通过继承它们(继承)。这是一种“黑盒复用”,因为类只与它所包含对象的公共接口交互,而不关心其内部实现。

让我们来看一个例子。假设我们有一个 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方法

这是如何工作的呢?这背后是“动态绑定”或“后期绑定”在起作用。

  1. 编译时:编译器检查 myCar 的类型是 Vehicle。它会确认 Vehicle 类(或其父类)中确实有一个名为 move() 的方法。如果找不到,编译就会失败。编译器此时只关心方法签名是否存在,不关心具体实现。

  2. 运行时:当代码实际执行 myCar.move() 时,Java虚拟机(JVM)会查看 myCar 变量当前实际引用的对象是什么。它发现这个对象是一个 Car 实例。

  3. 方法调用:于是,JVM会去 Car 类中查找 move() 方法的实现并执行它。这个过程发生在程序运行时,因此被称为动态绑定。

这种机制让我们的系统具有极高的可扩展性。我们可以随时添加新的 Vehicle 子类(比如 Motorcycle),只要它正确实现了 move() 方法,处理 Vehicle 的现有代码无需任何修改就能与新类协同工作。

Quiz Questions 1/5

在Java中,“组合优于继承”这一设计原则主要是为了解决继承带来的哪个问题?

Quiz Questions 2/5

Java 8在接口中引入“默认方法”(default methods)主要是为了解决什么问题?