Effective Java笔记:接口优于抽象类

在 《Effective Java 第三版》中,作者 Joshua Bloch 对“接口优于抽象类”这一原则有着深入的探讨。


一、接口 vs 抽象类:本质区别

在理解“接口优于抽象类”之前,我们需要明确二者的本质区别:

特性接口(Interface)抽象类(Abstract Class)
多重继承支持一个类可以实现多个接口(Java 提供多继承的能力)。一个类只能继承一个抽象类(单继承限制)。
构造函数接口没有构造函数,无法保存实现细节或状态。抽象类可以有构造函数,可以用来初始化子类状态。
默认方法从 Java 8 开始支持默认方法(default 方法)。抽象类可以有默认的实现方法,无需借助 default
实现细节只能定义方法签名和常量(从 Java 8 开始,可以有默认方法和静态方法)。抽象类允许拥有实例变量、方法实现,以及访问权限控制。
继承设计表现“类的行为”,强调 “是做什么”(What it can do)表现“类的本质”,强调 “是什么”(What it is)

二、为什么接口优于抽象类?

书中的核心观点是:接口提供了更大的灵活性、更广泛的适用性以及更好的未来兼容性。因此,Joshua Bloch 在条目20中推荐开发者优先选择接口,原因如下:

1. 接口支持多重类型继承

  • Java 中的类只能继承一个父类(单继承),但可以实现多个接口。
  • 使用接口可以赋予一个类多种行为,而不受单继承限制。例如,一个类可以同时实现 Serializable 接口(序列化)、Comparable 接口(比较)、Observer 接口(观察者模式)。

示例:接口的多重继承

public interface Flyable {
    void fly();
}

public interface Swimmable {
    void swim();
}

public class Duck implements Flyable, Swimmable { // 同时实现两个接口
    @Override
    public void fly() {
        System.out.println("Flying like a duck.");
    }

    @Override
    public void swim() {
        System.out.println("Swimming like a duck.");
    }
}

相比抽象类,接口让一个类可以轻松拥有“多个行为”(复合式能力),而不需要受到单继承的限制。


2. 接口更灵活,适用性更广(关注行为)

  • 接口更适用于定义“行为契约”(What it can do),而不是具体实现。
  • 例如:如果我们需要定义多种具有排序能力的对象,可以直接用接口而不是抽象类:
public interface Comparable<T> {
    int compareTo(T o);
}

public class Person implements Comparable<Person> { // 定义排序规则
    private String name;
    private int age;

    @Override
    public int compareTo(Person other) {
        return Integer.compare(this.age, other.age); // 按年龄比较
    }
}

书中例子常见的场景: ComparableComparator 模式

  • Comparable 定义了对象的自然排序规则。
  • Comparator 则是可扩展的排序工具,用户可以根据自己的场景灵活实现。

相比抽象类,接口更加关注对象“能做什么”(like sort, compare, etc.),而不是“是什么”。


3. 避免类层次结构的限制

  • 抽象类引入的层次结构会限制代码的灵活性,因为类的子类只能从单一抽象类继承。
  • 接口可以避免这种问题:一个类实现多个接口时,不会强制它继承某个具体的类层级。

书中反例:继承限制类的灵活性
假设我们有一个 AbstractVehicle 抽象类提供基本的移动行为,但后来我们需要使车辆继承另一个功能(如网络通信功能),会遇到继承限制的问题。

public abstract class AbstractVehicle {
    public abstract void move();
}

public abstract class AbstractNetworkDevice {
    public abstract void connect();
}

// 问题:Java 类无法同时继承两个抽象类!
public class SmartCar extends AbstractVehicle, AbstractNetworkDevice {
    ...
}

解决:改用接口

public interface Moveable {
    void move();
}

public interface Connectable {
    void connect();
}

public class SmartCar implements Moveable, Connectable {
    @Override
    public void move() {
        System.out.println("SmartCar is moving.");
    }

    @Override
    public void connect() {
        System.out.println("SmartCar is connecting to the network.");
    }
}

接口让我们摆脱了类层次结构的束缚,避免了单继承的限制。


4. 默认方法用于增强兼容性

Java 8 引入了默认方法(default),解决了接口在 API 演化过程中的兼容性问题。这让接口不仅能够定义行为,还能提供部分默认实现,同时避免破坏已有实现类。

示例:向后兼容的默认方法

public interface Animal {
    void eat();

    // 默认实现
    default void walk() {
        System.out.println("This animal is walking");
    }
}

子类可以复用默认行为

public class Dog implements Animal {
    @Override
    public void eat() {
        System.out.println("Dog eats bones");
    }
}

优点:

  • 如果没有默认方法,新增接口方法(如 walk())会强制所有实现类改动,影响兼容性。
  • 有了 default,我们可以保持对已有代码的向后兼容,同时为新需求提供扩展(如新增方法)。

5. 结合接口和默认方法设计“混合类型(mixins)”

在一些场景中,接口(配合默认方法)能够提供类似多个父类行为的能力,称为"混合类型"。

示例:混合默认行为

public interface Logger {
    default void log(String message) {
        System.out.println("Default log: " + message);
    }
}

public interface FileWriter {
    default void write(String message) {
        System.out.println("Writing to file: " + message);
    }
}

// 混合类型的实现
public class FileLogger implements Logger, FileWriter {
    public void save(String message) {
        log(message);   // 使用 Logger 的默认方法
        write(message); // 使用 FileWriter 的默认方法
    }
}

public class Main {
    public static void main(String[] args) {
        FileLogger logger = new FileLogger();
        logger.save("Hello, World!");
        // 输出:
        // Default log: Hello, World!
        // Writing to file: Hello, World!
    }
}

接口通过 default 方法支持行为扩展,并允许类灵活地混合多个行为,这比抽象类的单继承限制更具优势。


6. 符合面向接口编程的原则

Joshua Bloch 强调面向接口编程的好处:与其对实现(实现类或抽象类)编程,不如对接口编程

优秀设计:根据协议编程

public interface List<E> {
    boolean add(E e);
    E get(int index);
}

List<String> myList = new ArrayList<>();  // 面向接口编程

通过依赖接口,而非具体实现(如 ArrayListLinkedList),我们可以轻松切换实现而无须修改代码。

7.骨架实现(skeletal implementation)类

骨架实现,是指为一个接口提供一个抽象基本实现类(骨架类),这个实现类:

  • 是一个 抽象类。
  • 实现了接口的大多数方法(可能包括接口中复杂方法的默认实现)。
  • 子类只需关注实现少量的方法,无需全盘实现接口的所有方法。
  • 这种模式可以组合接口的灵活性和抽象类的代码复用性,通过 “让接口更加易用” 提高程序的设计质量。

7.1 骨架实现的例子

7.1 示例一:List接口与AbstractList骨架类
List 接口的设计挑战

List 作为 Java 中的一个主要集合接口,定义了大量方法,比如 additeratorgetsize 等。如果开发者要实现一个自定义的 List,需要提供所有这些方法的实现。

为了让开发者不需要处理大部分通用逻辑,Java 提供了一个抽象类 AbstractList,它是 List 的骨架实现。


AbstractList 骨架类(骨架实现)

AbstractList 提供了 List 大部分方法的默认实现。开发者只需要重写部分核心方法(例如 get(int index)size()),就能完成一个最小的 List 实现。

如果我们用自定义的方式实现一个简单的 List

import java.util.AbstractList;
import java.util.List;

public class SimpleList<E> extends AbstractList<E> {
    private E[] elements;

    @SuppressWarnings("unchecked")
    public SimpleList(int capacity) {
        elements = (E[]) new Object[capacity];
    }

    @Override
    public E get(int index) {
        if (index < 0 || index >= size()) {
            throw new IndexOutOfBoundsException("Index out of bounds: " + index);
        }
        return elements[index];
    }

    @Override
    public int size() {
        return elements.length;
    }

    @Override
    public E set(int index, E element) {
        if (index < 0 || index >= size()) {
            throw new IndexOutOfBoundsException("Index out of bounds: " + index);
        }
        E oldValue = elements[index];
        elements[index] = element;
        return oldValue;
    }

    @Override
    public E remove(int index) {
        throw new UnsupportedOperationException("Remove not supported");
    }
}
  • 当我们实现 AbstractList 的子类时,只需关注 getsize 方法,无需实现所有的 List 方法。
  • AbstractList 提供了许多方法的默认实现,例如 iterator()contains(),这些逻辑是共用的,可以直接被复用。

AbstractList 的具体价值
@Override
public Iterator<E> iterator() {
    return new Iterator<E>() {
        private int cursor = 0; // 游标指示当前元素

        @Override
        public boolean hasNext() {
            return cursor < size(); // 用 size() 骨架方法检查是否有更多元素
        }

        @Override
        public E next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            return get(cursor++); // 用 get() 骨架方法获取元素
        }
    };
}

从上面的代码可以看到:

  • AbstractList 通过子类实现的 get()size(),实现了完整的 iterator 方法。
  • 骨架类根据接口契约提供了默认实现,使得继承 AbstractList 的子类无需关心复杂方法的实现。

7.2 示例二:SkeletonSet 和 Comparator
骨架实现:Comparator 的典型案例

书中还提到了 Comparator 的骨架实现,作为如何组合接口和抽象类的一个优秀例子。

Java 8 提供了一些 Comparator 的静态方法和默认方法。我们可以通过这些方法实现复杂比较器的骨架逻辑。

import java.util.Comparator;

public abstract class SkeletonComparator<T> implements Comparator<T> {
    @Override
    public Comparator<T> reversed() {
        return (a, b) -> compare(b, a); // 利用骨架方法
    }

    @Override
    public Comparator<T> thenComparing(Comparator<? super T> other) {
        return (a, b) -> {
            int res = compare(a, b); // 利用核心实现
            return (res != 0) ? res : other.compare(a, b);
        };
    }

    @Override
    public Comparator<T> thenComparingInt(ToIntFunction<? super T> keyExtractor) {
        // ...
    }
}

子类只需实现 compare 方法的基本逻辑,而骨架实现提供了用于组合的高级能力(如 reversedthenComparing)。

骨架实现经常结合模板方法模式(Template Method Pattern)进行设计:

  • 在骨架类中定义一个方法的模板或算法骨架,子类只需实现骨架方法的某些部分。
  • 父类控制整体逻辑流程,子类扩展具体步骤。

三、何时选择抽象类?

尽管接口有诸多优点,但也并非完全取代抽象类。根据书中的观点,当以下情况成立时,可以考虑使用抽象类

  1. 需要方法或字段的共享实现:

    • 如果多个子类需要共享一些状态或行为,抽象类是更好的选择。
    • 接口无法保存成员变量或提供状态信息。

    示例:

    public abstract class Shape {
        protected int x, y;  // 所有子类共享的状态
    
        public abstract double area();
    }
    
  2. 需要构造器:

    • 当子类初始化时需要通过父类共享的构造器来初始化一部分状态时,应考虑抽象类。
    • 因为接口没有构造器,无法直接支持初始化逻辑。

四、总结

《Effective Java》在“接口优于抽象类”这一条目中给出了明确的指导:

  • 优先选择接口:

    • 接口支持多继承,能更灵活地表达类的“行为”。
    • 接口的默认方法允许向后兼容,同时支持混合行为的设计。
    • 接口更加轻量化和灵活,摆脱了继承层次的限制。
  • 考虑抽象类的情况:

    • 当需要在父类中共享状态或实现,或需要构造器初始化时,可以选择抽象类。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值