MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java如何选择接口与抽象类

2023-07-195.4k 阅读

Java 中接口与抽象类的基础概念

在深入探讨如何选择接口与抽象类之前,我们先来回顾一下它们的基础概念。

抽象类

抽象类是一种不能被实例化的类,它通常包含抽象方法(没有方法体的方法)和具体方法。抽象类使用 abstract 关键字来修饰。例如:

abstract class Animal {
    // 抽象方法
    abstract void makeSound();
    // 具体方法
    void eat() {
        System.out.println("Animal is eating.");
    }
}

在上述代码中,Animal 类被声明为抽象类,它包含了一个抽象方法 makeSound 和一个具体方法 eat。任何继承自 Animal 的子类都必须实现 makeSound 方法,除非子类也被声明为抽象类。

接口

接口是一种特殊的抽象类型,它只包含抽象方法(从 Java 8 开始,接口也可以包含默认方法和静态方法)。接口使用 interface 关键字来定义。例如:

interface Flyable {
    void fly();
}

在这个例子中,Flyable 接口定义了一个抽象方法 fly。任何实现 Flyable 接口的类都必须提供 fly 方法的具体实现。

接口与抽象类的特性对比

为了更好地选择接口与抽象类,我们需要详细对比它们的特性。

定义与结构

  • 抽象类:抽象类可以包含抽象方法和具体方法,同时也可以有成员变量、构造函数和静态成员。例如:
abstract class Shape {
    protected double area;
    // 构造函数
    public Shape() {
        this.area = 0;
    }
    abstract double calculateArea();
    static void printInfo() {
        System.out.println("This is a shape.");
    }
}

在上述代码中,Shape 抽象类有一个成员变量 area,一个构造函数,一个抽象方法 calculateArea 和一个静态方法 printInfo

  • 接口:接口在 Java 8 之前只能包含抽象方法,Java 8 引入了默认方法和静态方法。接口中的成员变量默认是 public static final 的,即常量。例如:
interface Drawable {
    double PI = 3.14159;
    void draw();
    // 默认方法
    default void drawWithColor(String color) {
        System.out.println("Drawing with color: " + color);
    }
    // 静态方法
    static void printDrawableInfo() {
        System.out.println("This is a drawable object.");
    }
}

在这个 Drawable 接口中,有一个常量 PI,一个抽象方法 draw,一个默认方法 drawWithColor 和一个静态方法 printDrawableInfo

继承与实现

  • 抽象类:Java 中类只能继承一个抽象类。例如:
class Circle extends Shape {
    private double radius;
    public Circle(double radius) {
        this.radius = radius;
    }
    @Override
    double calculateArea() {
        return PI * radius * radius;
    }
}

这里 Circle 类继承自 Shape 抽象类,并实现了 calculateArea 抽象方法。

  • 接口:一个类可以实现多个接口。例如:
class Plane implements Flyable, Drawable {
    @Override
    public void fly() {
        System.out.println("Plane is flying.");
    }
    @Override
    public void draw() {
        System.out.println("Drawing a plane.");
    }
}

Plane 类实现了 FlyableDrawable 两个接口,分别实现了它们的抽象方法。

多态性

  • 抽象类:通过继承抽象类,子类可以重写抽象类的方法来实现多态。例如:
Shape circle = new Circle(5);
circle.calculateArea();

这里 circle 被声明为 Shape 类型,但实际指向 Circle 类的实例,调用 calculateArea 方法时会执行 Circle 类中重写的方法,体现了多态性。

  • 接口:实现接口的类通过实现接口方法来体现多态。例如:
Flyable plane = new Plane();
plane.fly();

plane 被声明为 Flyable 类型,实际指向 Plane 类的实例,调用 fly 方法时执行 Plane 类中实现的方法,展示了多态性。

何时选择抽象类

在某些特定场景下,抽象类是更合适的选择。

当存在共同实现逻辑时

如果多个子类之间有一些共同的实现逻辑,抽象类可以将这些逻辑封装在具体方法中,避免在子类中重复实现。例如,在图形绘制的场景中,不同的图形(如圆形、矩形)可能有不同的绘制方法,但它们可能都需要一些共同的初始化操作,如设置画笔颜色等。

abstract class GraphicObject {
    protected String color;
    public GraphicObject(String color) {
        this.color = color;
    }
    // 共同的初始化逻辑
    void initialize() {
        System.out.println("Initializing graphic object with color: " + color);
    }
    abstract void draw();
}
class Rectangle extends GraphicObject {
    private int width;
    private int height;
    public Rectangle(String color, int width, int height) {
        super(color);
        this.width = width;
        this.height = height;
    }
    @Override
    void draw() {
        System.out.println("Drawing a rectangle with color " + color + ", width " + width + ", height " + height);
    }
}
class Circle extends GraphicObject {
    private double radius;
    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }
    @Override
    void draw() {
        System.out.println("Drawing a circle with color " + color + " and radius " + radius);
    }
}

在上述代码中,GraphicObject 抽象类的 initialize 方法封装了共同的初始化逻辑,RectangleCircle 子类继承自 GraphicObject 并复用了这个逻辑。

当需要继承层次结构时

如果希望构建一个继承层次结构,抽象类是理想的选择。例如,在一个游戏开发中,有不同类型的角色,如战士、法师、刺客等,它们都继承自一个抽象的角色类。

abstract class Character {
    protected String name;
    protected int health;
    public Character(String name, int health) {
        this.name = name;
        this.health = health;
    }
    abstract void attack();
    void move() {
        System.out.println(name + " is moving.");
    }
}
class Warrior extends Character {
    public Warrior(String name, int health) {
        super(name, health);
    }
    @Override
    void attack() {
        System.out.println(name + " attacks with a sword.");
    }
}
class Mage extends Character {
    public Mage(String name, int health) {
        super(name, health);
    }
    @Override
    void attack() {
        System.out.println(name + " casts a spell.");
    }
}

这里 Character 抽象类作为父类,定义了一些通用的属性和方法,WarriorMage 子类继承自 Character,形成了一个继承层次结构。

当需要使用成员变量和构造函数时

抽象类可以包含成员变量和构造函数,这在某些情况下非常有用。例如,在一个电子商务系统中,有不同类型的商品,抽象的商品类可以包含商品的基本信息,如名称、价格等,并通过构造函数进行初始化。

abstract class Product {
    protected String name;
    protected double price;
    public Product(String name, double price) {
        this.name = name;
        this.price = price;
    }
    abstract double calculateTotalPrice(int quantity);
}
class Book extends Product {
    public Book(String name, double price) {
        super(name, price);
    }
    @Override
    double calculateTotalPrice(int quantity) {
        return price * quantity;
    }
}
class Electronic extends Product {
    private double discount;
    public Electronic(String name, double price, double discount) {
        super(name, price);
        this.discount = discount;
    }
    @Override
    double calculateTotalPrice(int quantity) {
        return (price - discount) * quantity;
    }
}

在上述代码中,Product 抽象类通过成员变量和构造函数来管理商品的基本信息,BookElectronic 子类继承自 Product 并根据自身特点实现 calculateTotalPrice 方法。

何时选择接口

在另外一些场景下,接口则更能满足需求。

当需要实现多重继承功能时

由于 Java 类只能继承一个父类,但可以实现多个接口,当一个类需要具备多种不同类型的行为时,接口是最佳选择。例如,一个机器人可能既需要具备移动的能力,又需要具备抓取物品的能力。

interface Movable {
    void move();
}
interface Grabbable {
    void grab();
}
class Robot implements Movable, Grabbable {
    @Override
    public void move() {
        System.out.println("Robot is moving.");
    }
    @Override
    public void grab() {
        System.out.println("Robot is grabbing.");
    }
}

在这个例子中,Robot 类实现了 MovableGrabbable 两个接口,使其同时具备移动和抓取的能力。

当需要定义一种契约时

接口可以定义一种契约,所有实现该接口的类必须遵守这个契约。例如,在一个支付系统中,不同的支付方式(如支付宝、微信支付)都需要实现一个统一的支付接口。

interface Payment {
    void pay(double amount);
}
class AlipayPayment implements Payment {
    @Override
    public void pay(double amount) {
        System.out.println("Paid " + amount + " via Alipay.");
    }
}
class WeChatPayment implements Payment {
    @Override
    public void pay(double amount) {
        System.out.println("Paid " + amount + " via WeChat.");
    }
}

这里 Payment 接口定义了支付的契约,AlipayPaymentWeChatPayment 类必须实现 pay 方法来遵守这个契约。

当需要实现功能的动态添加时

接口使得可以在运行时动态地为对象添加功能。例如,在一个图形编辑软件中,用户可以根据需要为图形对象添加不同的功能,如旋转、缩放等。

interface Rotatable {
    void rotate();
}
interface Scalable {
    void scale();
}
class Square {
    void draw() {
        System.out.println("Drawing a square.");
    }
}
class RotatableSquare extends Square implements Rotatable {
    @Override
    public void rotate() {
        System.out.println("Rotating the square.");
    }
}
class ScalableSquare extends Square implements Scalable {
    @Override
    public void scale() {
        System.out.println("Scaling the square.");
    }
}
class RotatableAndScalableSquare extends Square implements Rotatable, Scalable {
    @Override
    public void rotate() {
        System.out.println("Rotating the square.");
    }
    @Override
    public void scale() {
        System.out.println("Scaling the square.");
    }
}

在这个例子中,Square 类可以通过继承不同的接口来动态地获得旋转或缩放的功能。

接口与抽象类的选择原则总结

在选择接口与抽象类时,我们可以遵循以下一些原则:

基于功能需求

  • 如果多个类之间有共同的实现逻辑,并且希望通过继承来复用这些逻辑,那么抽象类是一个不错的选择。例如,在图形绘制的场景中,不同图形可能有共同的初始化操作,抽象类可以将这些操作封装在具体方法中。
  • 如果一个类需要具备多种不同类型的行为,或者需要在运行时动态地添加功能,接口则更为合适。比如机器人需要同时具备移动和抓取的能力,或者图形对象需要动态添加旋转、缩放功能。

基于继承结构

  • 如果希望构建一个继承层次结构,并且在父类中定义一些通用的属性和方法,抽象类是理想的选择。像在游戏角色的例子中,不同类型的角色继承自抽象的角色类,形成了清晰的继承层次。
  • 如果不需要构建复杂的继承层次,只是希望定义一种契约,让不同的类来实现,接口更为合适。例如支付系统中不同支付方式实现统一的支付接口。

基于灵活性与扩展性

  • 接口通常提供了更高的灵活性,因为一个类可以实现多个接口,并且接口的修改不会影响实现类的继承结构。例如,在图形编辑软件中,图形对象可以根据需要实现不同的接口来获得新的功能。
  • 抽象类在继承层次上相对较为固定,子类只能继承一个抽象类。但是,如果抽象类的设计合理,它可以有效地封装共同的逻辑,提高代码的复用性。例如,在商品系统中,抽象的商品类封装了商品的基本信息和计算总价的抽象方法,子类可以根据自身特点实现具体的计算逻辑。

在实际的 Java 开发中,正确选择接口与抽象类对于代码的结构、复用性和可维护性都有着重要的影响。需要根据具体的业务需求和场景,综合考虑以上因素,做出最合适的选择。