杰瑞科技汇

接口、抽象类、实现类,如何选择与关联?

核心思想:蓝图与建筑

  • 接口:就像一份“功能需求说明书”,它只规定了“能做什么”(有哪些功能),但不规定“怎么做”(具体实现),一份说明书要求“产品必须能飞”和“产品必须能发声”,但没说怎么飞,怎么发声。
  • 抽象类:就像一份“半成品设计方案”,它既有一些已经实现好的、通用的部分(产品都有名字和重量”),也包含一些需要子类去具体实现的部分(如何飞行”),它提供了一个基础模板。
  • 实现类:就是最终的、具体的建筑,它根据“功能需求说明书”(接口)和“半成品设计方案”(抽象类),建造出真正可以使用的、能飞能叫的无人机或者飞机。

接口

接口是 Java 中一种引用类型,它像一份契约,定义了一组方法的规范,但不提供具体实现。

接口、抽象类、实现类,如何选择与关联?-图1
(图片来源网络,侵删)

主要特点:

  1. 纯抽象:在 Java 8 之前,接口里的方法默认是 public abstract 的,不能有方法体,从 Java 8 开始,接口可以包含 default 方法和 static 方法(可以有方法体)。
  2. 常量:接口中可以定义常量,默认是 public static final 的。
  3. 多实现:一个类可以实现多个接口,这弥补了 Java 单继承的不足,是实现多态的重要方式。
  4. 解耦:通过接口,调用方(客户端)只关心接口,而不关心具体的实现类,这使得代码的耦合度降低,更易于扩展和维护。

代码示例:

// 1. 定义一个接口 - 功能需求说明书
public interface Flyable {
    // 抽象方法,规定所有能飞的东西都必须实现这个方法
    void fly();
    // Java 8+ 可以有默认方法,提供默认实现
    default void takeOff() {
        System.out.println("准备起飞...");
    }
}
// 2. 定义另一个接口
public interface Singable {
    void sing();
}
// 3. 定义一个实现类 - 飞机,它同时实现了两个接口
public class Airplane implements Flyable, Singable {
    @Override
    public void fly() {
        System.out.println("飞机通过引擎和机翼高速飞行。");
    }
    @Override
    public void sing() {
        System.out.println("飞机发出轰鸣的歌声。");
    }
}
// 4. 另一个实现类 - 鸟,也实现了这两个接口
public class Bird implements Flyable, Singable {
    @Override
    public void fly() {
        System.out.println("鸟通过扇动翅膀在天空翱翔。");
    }
    @Override
    public void sing() {
        System.out.println("鸟儿唱着悦耳的歌。");
    }
}
// 5. 使用
public class Main {
    public static void main(String[] args) {
        // 使用接口类型引用对象,体现了多态
        Flyable airplane = new Airplane();
        Flyable bird = new Bird();
        airplane.fly();       // 输出: 飞机通过引擎和机翼高速飞行。
        airplane.takeOff();   // 输出: 准备起飞... (使用了默认方法)
        bird.fly();           // 输出: 鸟通过扇动翅膀在天空翱翔。
        bird.takeOff();       // 输出: 准备起飞... (使用了默认方法)
        // 如果想调用 sing() 方法,需要使用 Singable 类型
        Singable singingBird = new Bird();
        singingBird.sing();   // 输出: 鸟儿唱着悦耳的歌。
    }
}

抽象类

抽象类是不能被实例化的类,它作为基类,为子类提供一个共同的模板。

主要特点:

  1. 不能实例化:你不能直接创建 new AbstractClass(),它必须被继承。
  2. 包含抽象方法:至少包含一个 abstract 方法(没有方法体的方法),子类必须实现所有父类的抽象方法,否则子类也必须被声明为抽象类。
  3. 包含具体方法:可以包含有具体实现的方法(非抽象方法),子类可以直接继承使用这些方法,也可以重写它们。
  4. 有构造方法:抽象类可以有构造方法,用于在创建子类实例时初始化抽象类的成员变量。
  5. 单继承:一个类只能继承一个抽象类(因为 Java 只支持单继承)。

代码示例:

// 1. 定义一个抽象类 - 半成品设计方案
public abstract class Animal {
    // 成员变量
    protected String name;
    // 构造方法,用于初始化子类共有的属性
    public Animal(String name) {
        this.name = name;
    }
    // 抽象方法,所有动物都必须有吃东西的行为,但方式不同
    public abstract void eat();
    // 具体方法,所有动物都会睡觉,实现方式相同
    public void sleep() {
        System.out.println(name + " 正在睡觉。");
    }
}
// 2. 定义一个实现类 - 具体的动物
public class Dog extends Animal {
    public Dog(String name) {
        super(name); // 调用父类的构造方法
    }
    @Override
    public void eat() {
        System.out.println(name + " 正在啃骨头。");
    }
}
// 3. 另一个实现类
public class Cat extends Animal {
    public Cat(String name) {
        super(name); // 调用父类的构造方法
    }
    @Override
    public void eat() {
        System.out.println(name + " 正在吃鱼。");
    }
}
// 4. 使用
public class Main {
    public static void main(String[] args) {
        // Animal animal = new Animal("Tom"); // 编译错误!不能实例化抽象类
        Dog dog = new Dog("旺财");
        Cat cat = new Cat("咪咪");
        dog.eat();    // 输出: 旺财 正在啃骨头。
        dog.sleep();  // 输出: 旺财 正在睡觉。
        cat.eat();    // 输出: 咪咪 正在吃鱼。
        cat.sleep();  // 输出: 咪咪 正在睡觉。
    }
}

实现类

实现类是具体、完整的类,它通过 implements 关键字实现接口,或通过 extends 关键字继承抽象类,并提供所有未实现的方法的具体逻辑。

主要特点:

  1. 具体化:它是一个完整的、可被实例化的类。
  2. 实现接口:使用 implements 关键字,必须实现接口中的所有抽象方法(除非自身是抽象类)。
  3. 继承抽象类:使用 extends 关键字,必须实现父类中的所有抽象方法(除非自身是抽象类)。
  4. 可以扩展:可以在实现类中添加自己的新属性和方法。

在上面的 Airplane, Bird, Dog, Cat 的例子中,它们都是实现类


核心区别与选择:何时使用?

这是一个非常重要的问题,用错了会导致设计上的问题。

接口、抽象类、实现类,如何选择与关联?-图2
(图片来源网络,侵删)
特性 接口 抽象类
继承/实现 implements (实现),一个类可实现多个接口 extends (继承),一个类只能继承一个抽象类
方法 Java 8+ 可包含 default, static 方法和抽象方法 可包含抽象方法和具体方法
变量 只能是 public static final 常量 可以是各种类型的成员变量
构造方法 没有 有,用于子类调用
目的 定义能力/规范,回答“能做什么?” 定义本质/模板,回答“是什么?”
灵活性 高,解耦,支持多实现 低,耦合度高,但结构更清晰

选择指南

什么时候应该使用接口?

  1. 定义规范/契约:当你想定义一个类应该具备哪些功能时,而不关心它是什么。List, Set, Map 都定义了集合的通用操作。
  2. 实现多态:当一个类需要具备多种不同的能力时,一个 Duck 类既需要 Flyable,也需要 Swimmable
  3. 解耦:当你希望调用方只依赖于接口,而不是具体的实现时,这样未来可以轻松替换实现类而不影响调用方代码。

什么时候应该使用抽象类?

  1. 代码复用:当你想让多个子类共享一部分相同的具体代码时。Animal 抽象类中的 sleep() 方法可以直接被所有子类继承。
  2. 定义模板:当你想创建一个基类,为所有子类提供一个通用的结构和骨架时,子类在此基础上进行扩展和修改。
  3. “是一个”的关系:当子类和父类之间存在非常强的“是一个...”的继承关系时。Dog 是一个 Animal

经典场景:接口与抽象类结合使用

这是最强大、最灵活的设计模式。

场景:我们想设计一个 Bird(鸟)。

  1. 定义本质(抽象类):所有鸟都是“动物”,有名字,会睡觉,我们可以用 Animal 抽象类来定义这个本质。
  2. 定义能力(接口):但不是所有鸟都会飞(比如鸵鸟),也不是所有鸟都会唱歌,我们可以用 FlyableSingable 接口来定义这些可选的能力。

代码实现:

// 抽象类:定义鸟的本质
public abstract class Bird extends Animal { // Bird 本质上是一种 Animal
    public Bird(String name) {
        super(name);
    }
    // 鸟有一个共有的行为,但不是所有动物都有
    public void layEggs() {
        System.out.println(name + " 下蛋了。");
    }
}
// 接口:定义飞的能力
interface Flyable { void fly(); }
// 接口:定义唱歌的能力
interface Singable { void sing(); }
// 实现类1:麻雀,会飞,会唱歌
public class Sparrow extends Bird implements Flyable, Singable {
    public Sparrow(String name) {
        super(name);
    }
    @Override
    public void eat() {
        System.out.println(name + " 在吃虫子。");
    }
    @Override
    public void fly() {
        System.out.println(name + " 在天空自由飞翔。");
    }
    @Override
    public void sing() {
        System.out.println(name + " 在叽叽喳喳地唱歌。");
    }
}
// 实现类2:鸵鸟,不会飞,但会唱歌(或者别的)
public class Ostrich extends Bird { // 鸵鸟不会飞,所以不实现 Flyable
    public Ostrich(String name) {
        super(name);
    }
    @Override
    public void eat() {
        System.out.println(name + " 在吃草。");
    }
    // 鸵鸟有自己的叫声,可以理解为一种特殊的“唱歌”
    public void makeSound() {
        System.out.println(name + " 发出低沉的叫声。");
    }
}
// 使用
public class Main {
    public static void main(String[] args) {
        Sparrow sparrow = new Sparrow("小麻雀");
        Ostrich ostrich = new Ostrich("鸵鸟先生");
        sparrow.eat();    // 继承自 Animal
        sparrow.sleep();  // 继承自 Animal
        sparrow.layEggs(); // 继承自 Bird
        sparrow.fly();    // 实现 Flyable
        sparrow.sing();   // 实现 Singable
        System.out.println("-----");
        ostrich.eat();    // 继承自 Animal
        ostrich.sleep();  // 继承自 Animal
        ostrich.layEggs(); // 继承自 Bird
        // ostrich.fly(); // 编译错误!Ostrich 没有 fly() 方法
        ostrich.makeSound(); // 自己的方法
    }
}
  • 接口:定义“能做什么”,是行为的契约,一个类可以实现多个接口,非常灵活。
  • 抽象类:定义“是什么”,是事物的模板,提供代码复用,一个类只能继承一个抽象类。
  • 实现类:是最终的、具体的类,它“能做什么”“是什么”都通过实现接口和继承抽象类来体现。

理解这三者的区别和联系,是迈向高级 Java 开发者的关键一步,蓝图与建筑”的比喻,能帮助你更好地在实际项目中做出正确的选择。

接口、抽象类、实现类,如何选择与关联?-图3
(图片来源网络,侵删)
分享:
扫描分享到社交APP
上一篇
下一篇