抽象与接口

JavaBeginner
立即练习

介绍

抽象和接口是 Java 面向对象编程中的两个基本概念。虽然抽象类和接口的用途不同,但它们有一些共同的特点。本实验将引导你理解和实现这两个概念,帮助你掌握在 Java 程序中何时以及如何有效地使用它们。

理解抽象

抽象是面向对象编程中的核心概念,它专注于隐藏实现细节,仅向用户暴露必要的功能。通过只展示相关内容并隐藏复杂的内部机制,你可以创建对象的简化视图。

在 Java 中,抽象通过以下方式实现:

  • 抽象类
  • 接口

什么是抽象类?

抽象类是一种不能直接实例化的类,它可能包含抽象方法(没有实现的方法)。抽象类作为其他类的蓝图,提供了共同的结构和行为。

抽象类的主要特点:

  • 使用 abstract 关键字声明
  • 可能包含抽象方法和非抽象方法
  • 不能直接实例化
  • 可以有构造函数和实例变量
  • 子类必须实现所有抽象方法,否则自身也必须声明为抽象类

让我们通过打开 WebIDE 编辑器并修改文件 /home/labex/project/abstractTest.java 来探索如何创建抽象类:

// 抽象类示例
abstract class Animal {
    // 抽象方法 - 无实现
    public abstract void makeSound();

    // 具体方法 - 有实现
    public void eat() {
        System.out.println("The animal is eating");
    }
}

// 实现抽象方法的具体子类
class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog barks: Woof! Woof!");
    }
}

// 测试实现的主类
public class abstractTest {
    public static void main(String[] args) {
        // 不能创建 Animal 的实例
        // Animal animal = new Animal(); // 这会导致编译错误

        // 创建 Dog 的实例
        Dog dog = new Dog();
        dog.makeSound(); // 调用实现的抽象方法
        dog.eat();       // 调用继承的具体方法
    }
}

现在让我们运行这段代码来查看结果:

javac /home/labex/project/abstractTest.java
java abstractTest

编译并运行

你应该会看到以下输出:

Dog barks: Woof! Woof!
The animal is eating

这展示了抽象类如何为其子类提供模板。Animal 类定义了子类应该具备的方法(makeSound() 方法),同时也提供了通用功能(eat() 方法)。

抽象类继承

在使用抽象类时,继承起着至关重要的作用。在这一步中,我们将探索涉及抽象类继承的更复杂场景。

让我们修改 /home/labex/project/abstractTest.java 文件,以演示抽象类如何从其他抽象类继承:

// 基础抽象类
abstract class Animal {
    // 实例变量
    protected String name;

    // 构造函数
    public Animal(String name) {
        this.name = name;
    }

    // 抽象方法
    public abstract void makeSound();

    // 具体方法
    public void eat() {
        System.out.println(name + " is eating");
    }
}

// 另一个继承自 Animal 的抽象类
abstract class Bird extends Animal {
    // 调用父类构造函数的构造函数
    public Bird(String name) {
        super(name);
    }

    // 特定于 Bird 的具体方法
    public void fly() {
        System.out.println(name + " is flying");
    }

    // 注意:Bird 没有实现 makeSound(),因此它仍然是抽象的
}

// Bird 的具体子类
class Sparrow extends Bird {
    public Sparrow(String name) {
        super(name);
    }

    // 实现 Animal 中的抽象方法
    @Override
    public void makeSound() {
        System.out.println(name + " chirps: Tweet! Tweet!");
    }
}

// Animal 的具体子类
class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    @Override
    public void makeSound() {
        System.out.println(name + " barks: Woof! Woof!");
    }
}

// 测试实现的主类
public class abstractTest {
    public static void main(String[] args) {
        // 创建具体类的实例
        Dog dog = new Dog("Buddy");
        Sparrow sparrow = new Sparrow("Jack");

        // 测试方法
        dog.makeSound();
        dog.eat();

        sparrow.makeSound();
        sparrow.eat();
        sparrow.fly();
    }
}

让我们运行这个更新后的代码:

javac /home/labex/project/abstractTest.java
java abstractTest

你应该会看到以下输出:

Buddy barks: Woof! Woof!
Buddy is eating
Jack chirps: Tweet! Tweet!
Jack is eating
Jack is flying

这个示例说明了几个重要的概念:

  1. 抽象类可以有构造函数和实例变量
  2. 一个抽象类可以继承另一个抽象类
  3. 当一个抽象类继承另一个抽象类时,它不需要实现抽象方法
  4. 继承链末端的具体类必须实现所有父类的抽象方法

抽象类在以下情况下特别有用:

  • 你想在密切相关的类之间共享代码
  • 你期望子类有许多共同的方法或字段
  • 你需要为某些方法提供默认实现
  • 你想控制某些方法的访问权限

理解接口

接口为 Java 中实现抽象提供了另一种方式。与抽象类不同,接口是完全抽象的,在 Java 8 之前不能包含任何方法实现。

接口定义了一个契约,实现该接口的类必须遵循这个契约,它规定了一个类可以做什么,但不规定应该如何去做。

什么是接口?

接口的主要特点:

  • 使用 interface 关键字声明
  • 在 Java 8 之前,所有方法都隐式地是公共的和抽象的
  • 所有字段都隐式地是公共的、静态的和最终的(常量)
  • 一个类可以实现多个接口
  • 接口可以继承多个接口

让我们通过修改文件 /home/labex/project/interfaceTest.java 来创建一个基本的接口:

// 定义一个接口
interface Animal {
    // 常量(隐式地是公共的、静态的、最终的)
    String CATEGORY = "Living Being";

    // 抽象方法(隐式地是公共的和抽象的)
    void makeSound();
    void move();
}

// 实现接口的类
class Dog implements Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog barks: Woof! Woof!");
    }

    @Override
    public void move() {
        System.out.println("Dog runs on four legs");
    }
}

// 测试接口的主类
public class interfaceTest {
    public static void main(String[] args) {
        // 创建一个 Dog 对象
        Dog dog = new Dog();

        // 调用接口方法
        dog.makeSound();
        dog.move();

        // 访问接口常量
        System.out.println("Category: " + Animal.CATEGORY);
    }
}

让我们运行这段代码,看看接口是如何工作的:

javac /home/labex/project/interfaceTest.java
java interfaceTest

你应该会看到以下输出:

Dog barks: Woof! Woof!
Dog runs on four legs
Category: Living Being

这个示例展示了接口的基本概念。Animal 接口定义了一个契约,Dog 类必须实现这个契约。任何实现 Animal 接口的类都必须为接口中声明的所有方法提供实现。

多接口与接口继承

接口的主要优势之一是能够实现多个接口并创建接口层次结构。与仅支持单继承的抽象类相比,这提供了更大的灵活性。

让我们更新 /home/labex/project/interfaceTest.java 文件来演示这些概念:

// 第一个接口
interface Animal {
    // 常量
    String CATEGORY = "Living Being";

    // 方法
    void makeSound();
    void eat();
}

// 第二个接口
interface Pet {
    // 常量
    String STATUS = "Domesticated";

    // 方法
    void play();
    void cuddle();
}

// 继承自另一个接口的接口
interface Bird extends Animal {
    // 额外的方法
    void fly();
}

// 实现多个接口的类
class Dog implements Animal, Pet {
    @Override
    public void makeSound() {
        System.out.println("Dog barks: Woof! Woof!");
    }

    @Override
    public void eat() {
        System.out.println("Dog eats meat and dog food");
    }

    @Override
    public void play() {
        System.out.println("Dog plays fetch");
    }

    @Override
    public void cuddle() {
        System.out.println("Dog cuddles with its owner");
    }
}

// 实现 Bird 接口的类
class Sparrow implements Bird {
    @Override
    public void makeSound() {
        System.out.println("Sparrow chirps: Tweet! Tweet!");
    }

    @Override
    public void eat() {
        System.out.println("Sparrow eats seeds and insects");
    }

    @Override
    public void fly() {
        System.out.println("Sparrow flies with its wings");
    }
}

// 测试接口的主类
public class interfaceTest {
    public static void main(String[] args) {
        // 创建对象
        Dog dog = new Dog();
        Sparrow sparrow = new Sparrow();

        // 调用不同接口的方法
        System.out.println("--- Dog behaviors ---");
        dog.makeSound();
        dog.eat();
        dog.play();
        dog.cuddle();

        System.out.println("\n--- Sparrow behaviors ---");
        sparrow.makeSound();
        sparrow.eat();
        sparrow.fly();

        // 访问接口常量
        System.out.println("\n--- Interface Constants ---");
        System.out.println("Animal Category: " + Animal.CATEGORY);
        System.out.println("Pet Status: " + Pet.STATUS);
    }
}

让我们运行这个更新后的代码:

javac /home/labex/project/interfaceTest.java
java interfaceTest

你应该会看到以下输出:

--- Dog behaviors ---
Dog barks: Woof! Woof!
Dog eats meat and dog food
Dog plays fetch
Dog cuddles with its owner

--- Sparrow behaviors ---
Sparrow chirps: Tweet! Tweet!
Sparrow eats seeds and insects
Sparrow flies with its wings

--- Interface Constants ---
Animal Category: Living Being
Pet Status: Domesticated

这个示例展示了几个重要的接口概念:

  1. 一个类可以实现多个接口(Dog 同时实现了 AnimalPet
  2. 一个接口可以继承另一个接口(Bird 继承自 Animal
  3. 实现接口的类必须实现该接口及其所有继承接口的所有方法
  4. 可以使用接口名来访问接口常量

接口在以下情况下特别有用:

  • 你想定义一个不包含实现细节的契约
  • 你需要实现多继承
  • 不相关的类需要实现相同的行为
  • 你想为服务提供者指定行为,但不规定具体实现方式

抽象类与接口的对比

既然我们已经探讨了抽象类和接口,那么让我们来比较这两种抽象机制,以了解何时使用它们。

主要区别

让我们创建一个名为 /home/labex/project/ComparisonExample.java 的文件来说明这些区别:

// 抽象类示例
abstract class Vehicle {
    // 实例变量
    protected String brand;

    // 构造函数
    public Vehicle(String brand) {
        this.brand = brand;
    }

    // 抽象方法
    public abstract void start();

    // 具体方法
    public void stop() {
        System.out.println(brand + " vehicle stops");
    }
}

// 接口示例
interface ElectricPowered {
    // 常量
    String POWER_SOURCE = "Electricity";

    // 抽象方法
    void charge();
    void displayBatteryStatus();
}

// 使用抽象类和接口的类
class ElectricCar extends Vehicle implements ElectricPowered {
    private int batteryLevel;

    public ElectricCar(String brand, int batteryLevel) {
        super(brand);
        this.batteryLevel = batteryLevel;
    }

    // 实现 Vehicle 中的抽象方法
    @Override
    public void start() {
        System.out.println(brand + " electric car starts silently");
    }

    // 实现 ElectricPowered 接口中的方法
    @Override
    public void charge() {
        batteryLevel = 100;
        System.out.println(brand + " electric car is charging. Battery now at 100%");
    }

    @Override
    public void displayBatteryStatus() {
        System.out.println(brand + " battery level: " + batteryLevel + "%");
    }
}

public class ComparisonExample {
    public static void main(String[] args) {
        ElectricCar tesla = new ElectricCar("Tesla", 50);

        // 调用抽象类 Vehicle 中的方法
        tesla.start();
        tesla.stop();

        // 调用接口 ElectricPowered 中的方法
        tesla.displayBatteryStatus();
        tesla.charge();
        tesla.displayBatteryStatus();

        // 访问接口中的常量
        System.out.println("Power source: " + ElectricPowered.POWER_SOURCE);
    }
}

让我们运行这段代码:

javac /home/labex/project/ComparisonExample.java
java ComparisonExample

你应该会看到以下输出:

Tesla electric car starts silently
Tesla vehicle stops
Tesla battery level: 50%
Tesla electric car is charging. Battery now at 100%
Tesla battery level: 100%
Power source: Electricity

何时使用它们?

以下是一个对比表格,帮助你决定何时使用抽象类,何时使用接口:

特性 抽象类 接口
方法 可以有抽象方法和具体方法 所有方法都是抽象的(Java 8 之前)
变量 可以有实例变量 只能有常量(公共静态常量)
构造函数 可以有构造函数 不能有构造函数
继承 支持单继承 一个类可以实现多个接口
访问修饰符 方法可以有任何访问修饰符 方法隐式为公共的
用途 “是一个”关系(继承) “能做”能力(行为)

在以下情况下使用抽象类:

  • 你想在密切相关的类之间共享代码
  • 你需要为某些方法提供默认实现
  • 你想要非公共成员(字段、方法)
  • 你需要构造函数或实例字段
  • 你要为一组子类定义一个模板

在以下情况下使用接口:

  • 你期望不相关的类实现你的接口
  • 你想指定行为但不指定实现
  • 你需要多继承
  • 你想为服务定义一个契约

在很多情况下,一个好的设计可能会同时涉及抽象类和接口,就像我们的 ElectricCar 示例一样。

总结

在本次实验中,你探索了 Java 中两种强大的抽象机制:抽象类和接口。以下是关键要点:

  • 抽象类:

    • 不能直接实例化
    • 可以同时包含抽象方法和具体方法
    • 支持构造函数和实例变量
    • 遵循单继承模型
    • 非常适合“是一个”关系和共享实现
  • 接口:

    • 定义实现类必须遵循的契约
    • 所有字段都是常量(公共、静态、最终)
    • 方法隐式为公共且抽象(Java 8 之前)
    • 通过实现支持多继承
    • 非常适合“能做”能力和松耦合

抽象类和接口都是在 Java 中实现抽象的重要工具,而抽象是面向对象编程的基本原则之一。在它们之间做出选择取决于你的设计需求,每种机制在特定场景下都有其独特的优势。

当你继续 Java 编程之旅时,你会发现合理使用抽象类和接口能够构建出更易于维护、更灵活且更健壮的代码结构。