介绍
抽象和接口是 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
这个示例说明了几个重要的概念:
- 抽象类可以有构造函数和实例变量
- 一个抽象类可以继承另一个抽象类
- 当一个抽象类继承另一个抽象类时,它不需要实现抽象方法
- 继承链末端的具体类必须实现所有父类的抽象方法
抽象类在以下情况下特别有用:
- 你想在密切相关的类之间共享代码
- 你期望子类有许多共同的方法或字段
- 你需要为某些方法提供默认实现
- 你想控制某些方法的访问权限
理解接口
接口为 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
这个示例展示了几个重要的接口概念:
- 一个类可以实现多个接口(
Dog同时实现了Animal和Pet) - 一个接口可以继承另一个接口(
Bird继承自Animal) - 实现接口的类必须实现该接口及其所有继承接口的所有方法
- 可以使用接口名来访问接口常量
接口在以下情况下特别有用:
- 你想定义一个不包含实现细节的契约
- 你需要实现多继承
- 不相关的类需要实现相同的行为
- 你想为服务提供者指定行为,但不规定具体实现方式
抽象类与接口的对比
既然我们已经探讨了抽象类和接口,那么让我们来比较这两种抽象机制,以了解何时使用它们。
主要区别
让我们创建一个名为 /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 编程之旅时,你会发现合理使用抽象类和接口能够构建出更易于维护、更灵活且更健壮的代码结构。



