先说结论

接口描述一组能力和契约,适合让无关类型获得统一行为;抽象类描述同一类对象的共同骨架,可以保存实例状态、构造逻辑和受保护的复用实现。类只能继承一个父类,却可以实现多个接口。

现代接口可以有 defaultstatic 和私有方法,但不能承担普通可变实例状态;抽象类可以定义字段、构造器和非公开模板方法。

选择原则

对外扩展点优先小而稳定的接口,使调用方依赖抽象;多个实现确实共享状态和流程时,再引入抽象基类。二者可以组合,例如接口定义能力,抽象类提供可选的默认骨架。

能力对比

维度 接口 抽象类
类型关系 “能做什么” “是什么”及共同骨架
实现数量 一个类可实现多个 一个类只能继承一个
实例状态 不能声明普通实例字段 可以保存受保护状态
构造过程 无实例构造器 可定义构造器并约束初始化
方法实现 default、static、private 任意普通或抽象方法
演进风险 新增抽象方法会破坏实现者 新增具体方法通常兼容,但影响继承层次

接口字段隐式是 public static final 常量,不适合保存每个对象不同的状态。抽象类的 protected 状态虽然便于复用,却会增加子类与父类实现细节的耦合。

组合使用示例

public interface PaymentChannel {
    PayResult pay(PayCommand command);
}

public abstract class AbstractPaymentChannel implements PaymentChannel {
    @Override
    public final PayResult pay(PayCommand command) {
        validate(command);
        String requestId = createRequestId(command);
        return doPay(requestId, command);
    }

    protected abstract PayResult doPay(String requestId, PayCommand command);
}

接口让调用方只依赖支付能力,抽象类用模板方法复用校验和请求号逻辑。第三方实现可以直接实现接口,不被迫继承骨架,这比把所有能力绑在深继承树上更灵活。

默认方法冲突规则

若父类和接口提供同签名方法,类方法优先;若两个无继承关系的接口提供相同 default 方法,实现类必须显式重写,并可通过 InterfaceA.super.method() 选择一个实现。子接口重写的默认方法比父接口更具体。

默认方法主要用于接口兼容演进和少量通用行为。如果逻辑依赖多个可变字段、生命周期或外部资源,把它塞进接口会让契约和实现边界模糊。

从领域模型判断

判断“猫和狗都能发声”时,Soundable 是能力接口;若所有账户共享账户号、余额校验和开户流程,AbstractAccount 可能有合理的同族抽象。但如果两个类只是碰巧使用同一段重试代码,应提取组件并组合,而不是为了复用而制造继承关系。

演进与兼容

给接口新增抽象方法会破坏已有实现;默认方法可缓解兼容问题,但不适合塞入依赖具体状态的复杂逻辑。若父类方法与接口默认方法冲突,类方法优先;多个接口冲突时实现类必须显式选择。

容易踩坑的地方

“接口不能有实现”“抽象类一定不能实例化所以没有构造器”都不准确。抽象类虽不能直接实例化,其构造器仍用于初始化子类中的父类部分。

常见问题

追问:什么时候不该用抽象类?

当复用关系只是工具代码共享、对象并非同一种类型,或继承会限制未来扩展时,应改用组合和委托。

追问:接口可以有构造器吗?

不能。接口不代表可实例化对象,也没有实例初始化过程;其中的常量在接口初始化时处理,和抽象类构造器不是一回事。

追问:抽象类可以没有抽象方法吗?

可以。这样做可以阻止直接实例化并提供受控继承入口,但需要有清晰设计理由,否则普通类加组合可能更简单。

追问:为什么优先组合而不是继承?

组合通过明确字段委托能力,可以在运行时替换且不暴露内部状态;继承把子类绑定到父类实现和生命周期,父类变化更容易影响所有子类。