《Effective Java》解读第21条:为后代设计接口

第21条:为后代设计接口

一个接口一旦公布就几乎不可更改,因此,在设计接口时,必须具有前瞻性,充分考虑其未来的演变,否则微小的缺陷也可能导致巨大的代价。
在web开发其实还好,基本一个接口就只有一个实现类,添加新接口直接实现就好了。

接口的脆弱性

在Java 8之前,向已发布的接口添加新方法会“破坏”所有现有的实现类,因为它们没有实现新方法。Java 8引入了默认方法,这为接口的演进提供了强大的工具,但也带来了新的风险。

核心原则与最佳实践

  1. 默认方法是“修正”工具,而非“初始”设计工具

初衷:默认方法的主要目的是允许在接口发布后,以兼容的方式添加新的方法。

错误用法:在初次设计接口时,就默认提供所有方法的实现。这违背了接口定义类型的初衷,可能导致糟糕的API设计。

  1. 谨慎设计默认方法

实现冲突:如果一个类实现的多个接口有相同的默认方法签名,且没有覆盖该方法,会导致编译错误。

继承问题:默认方法不能感知实现类的状态,因为它只能在接口上定义。它不能访问实例字段。

行为不可预测:默认方法可能被实现类覆盖,其行为不是最终确定的。

例如:
存在两个接口,有相同的默认方法。

public interface TestService {
    default void testDefault() {
        System.out.println("testDefault");
    }
}
public interface Test2Service {
    default void testDefault() {
        System.out.println("testDefault");
    }
}

在这里插入图片描述
实现类没有覆盖该方法,会导致编译错误,因为两个接口中有相同的默认方法,实现类也不知道用哪个。
这时我们可以重写覆盖此默认方法,当然也可以通过使用 InterfaceName.super.methodName() 明确指定调用哪个接口的方法。

public class TestServiceImpl implements TestService, Test2Service{
    
    @Override
    public void testDefault() {
        // 选择使用其中一个
        TestService.super.testDefault();
    }
}

InterfaceName:指定要调用哪个接口的默认方法
super:表示调用父级(接口)的默认实现
methodName():具体的方法

经典的菱形继承问题,虽然Java只能单继承,但接口可以多实现啊,而且java8接口还引入了默认方法,所以就有了这个问题。

所以,尽量避免使用默认方法添加“可能破坏现有实现”的新方法。只有当新方法有合理且安全的默认实现,并且与接口的核心抽象一致时,才使用默认方法。

  1. 为每个暴露的接口提供一个抽象骨架类

上一条其实也提高了此方法。这也是本条款中最重要、最实用的设计模式。

概念:
定义一个抽象的类,实现目标接口,并为接口中的方法提供基本的实现(可以是抽象的,也可以是具体的)。这个类被称为骨架实现类(Skeletal Implementation Class),例如 AbstractCollection, AbstractList, AbstractMap。

作用:

  • 减少实现负担:实现类可以继承骨架类,只需关注其核心逻辑,无需实现所有接口方法。
  • 促进接口演变:在接口中添加新方法时,可以在骨架类中提供一个默认的(甚至是高效的)实现,所有继承该骨架类的实现类将自动获得这个新方法,而无需修改代码。
  • 命名惯例:Abstract[InterfaceName],例如 AbstractCollection 对应 Collection 接口。

例如:

// 接口
public interface MyList<E> {
    int size();
    E get(int index);
    // ... 其他方法,如 add, remove 等
}

// 骨架实现类
public abstract class AbstractMyList<E> implements MyList<E> {
    // 提供一些方法的默认实现,例如基于 get 和 size 实现迭代
    @Override
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            private int index = 0;
            @Override public boolean hasNext() { return index < size(); }
            @Override public E next() { return get(index++); }
        };
    }

    // 其他方法可以标记为抽象,由具体实现类完成
    // public abstract void add(int index, E element);
}

// 具体实现类,只需继承骨架类,大大简化
public class SimpleArrayList<E> extends AbstractMyList<E> {
    private Object[] elements;
    private int size = 0;

    @Override public int size() { return size; }
    @Override public E get(int index) { return (E) elements[index]; }
    // ... 只需要实现核心的抽象方法和自己关心的方法
}

总结

  1. 设计之初,深思熟虑:假设接口发布后就不能修改,仔细推敲每一个方法。

  2. 优先使用骨架类:对于复杂的接口,总是提供一个并行的抽象骨架实现类。这是最稳健、最灵活的演进方式。

  3. 谨慎使用默认方法:
    3.1 主要用于接口发布后的兼容性更新。
    3.2 确保默认实现是安全、高效且符合逻辑的。
    3.3 意识到默认方法可能引起的多重继承冲突。
    3.4 提供清晰的文档:告诉用户实现你接口的最佳实践。
    3.5 缺省方法不支持从接口中删除方法,也不支持修改现有方法的签名,对接口进行这些修改肯定会破坏现有的客户端代码。

最终目标:通过精心的设计,让你的接口在保持稳定性的同时,具备优雅演进的能力,从而真正地“为后代服务”。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cosmoshhhyyy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值