面试考察点

  • 能否从设计语义而不是语法数量回答。
  • 是否理解 Java 的单继承与多接口实现。
  • 是否知道接口默认方法的冲突规则。

核心答案

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

现代接口可以有 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 可能有合理的同族抽象。但如果两个类只是碰巧使用同一段重试代码,应提取组件并组合,而不是为了复用而制造继承关系。

演进与兼容

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

常见误区

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

高频追问与参考回答

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

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

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

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

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

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

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

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

总结

接口强调能力与解耦,抽象类强调同族对象的状态和模板复用,最终选择取决于领域关系而非语法偏好。

机制全景图

下面把「接口和抽象类有什么区别?」从输入到结果压缩成一条可复述的主链路。面试时先用图建立全局坐标,再进入局部实现,能避免只背零散结论。

flowchart LR
    A["识别变化维度"]
    A --> B["抽取能力契约"]
    B --> C["决定是否共享状态"]
    C --> D["选择继承或组合"]
    D --> E["通过实现完成替换"]

完整链路:从输入到结果

沿着「识别变化维度 → 抽取能力契约 → 决定是否共享状态 → 选择继承或组合 → 通过实现完成替换」观察输入、状态与输出,下面每个阶段都对应一个可以在源码、日志或系统表中验证的位置。

1. 识别变化维度

设计开始先区分“是什么”与“能做什么”,避免仅因代码相似就建立脆弱继承关系。

2. 抽取能力契约

接口适合表达跨层能力和替换点,一个类型可以实现多个接口,调用方只依赖最小契约。

3. 决定是否共享状态

抽象类可持有受保护状态、构造逻辑和模板方法,但共享可变状态会放大子类之间的隐式耦合。

4. 选择继承或组合

组合通过显式字段委托复用行为,通常比深继承更容易测试和替换,但对象协作会多一层。

5. 通过实现完成替换

替换性需要实现遵守输入、输出和异常契约,不能只满足语法上的 implements 或 extends。

源码与实现定位

入口 阅读重点
java.util.AbstractList 骨架实现如何复用最小原语
java.util.ServiceLoader 接口作为插件发现边界

源码或系统表应按上表顺序追踪:先确认入口实际走到哪条路径,再用运行时数据验证,而不是仅凭类名或配置推测。

参数配置与可复现实验

interface PaymentChannel { Receipt pay(Command cmd); }
final class PaymentService {
  PaymentService(PaymentChannel channel, RiskPolicy risk) { }
}

用一个新增渠道验证扩展:若需要修改基类多个 if 或空实现无关方法,记录为接口隔离失败;再用替身实现运行契约测试。

验证步骤与预期结果

1. 固定输入和基线

先在没有故障注入的环境执行上述配置,固定数据规模、并发度、运行时版本和预热时间。以「实现类空方法比例」为主基线,记录值应满足「目标为 0」;同时保存 实现类数量与变更频率、基类条件分支数量,使后续变化能够回到同一时间轴比较。

2. 从实现入口确认路径

在「java.util.AbstractList」确认请求确实进入「骨架实现如何复用最小原语」对应的实现,再沿「java.util.ServiceLoader」观察「接口作为插件发现边界」。如果入口路径都未命中,就不应继续调整下游参数,而应先检查调用条件、版本或路由是否与假设一致。

3. 注入本文特有的失败模式

优先复现「把工具方法相似误判为 is-a 关系」,并把单一变量逐级放大,直到「实现类空方法比例」越过「出现空实现/Unsupported」。随后再分别验证「默认方法冲突未明确解决」和「子类破坏父类前置条件或异常契约」,三类故障分开执行,避免多个变量同时变化而无法归因。

4. 执行止损和根因修复

第一轮只应用「把胖接口拆成支付、退款、查询能力」,确认它能控制影响范围;第二轮应用「共享流程移到编排器而非可变基类」,验证核心链路恢复;最后落实「为所有实现复用同一组契约测试」,消除同类问题再次出现的条件。每一步都保留变更前后数据,不用“感觉变快了”替代测量。

5. 通过退出条件

实验只有同时满足三项才算通过:「实现类空方法比例」回到「目标为 0」、「基类条件分支」回到「不应随子类线性增长」、「契约测试通过率」回到「所有实现 100%」,并且业务结果差异为零。若性能恢复但结果不一致,仍应视为失败;若指标恢复后很快再次越线,则说明只完成了临时止损,没有消除根因。

量化基线

指标 样例基线/口径 风险线 结论
实现类空方法比例 目标为 0 出现空实现/Unsupported 拆分能力接口
基类条件分支 不应随子类线性增长 每加实现都改基类 改为组合策略
契约测试通过率 所有实现 100% 某实现失败 阻止注册该实现

这些数值是实验口径或示例告警线,不是可复制到所有系统的固定答案;上线阈值应由本系统稳态、峰值和故障演练共同确定。

事故复盘:支付渠道接入导致基类不断膨胀

所有支付方式继承同一个抽象基类后,二维码、银行卡和余额支付的差异被迫塞进条件分支。重构为支付接口、独立能力接口和可组合的风控/路由策略后,共享流程留在编排器,不再要求每个子类拥有无关状态。

失败模式 首要证据 第一处置动作
把工具方法相似误判为 is-a 关系 实现类数量与变更频率 把胖接口拆成支付、退款、查询能力
默认方法冲突未明确解决 基类条件分支数量 共享流程移到编排器而非可变基类
子类破坏父类前置条件或异常契约 接口方法被空实现的比例 为所有实现复用同一组契约测试

发布与回滚检查点

  • 发布前:确认「java.util.AbstractList」对应实现和上述配置在目标版本仍然有效,并保存「实现类空方法比例」基线。
  • 灰度中:同时观察 实现类数量与变更频率、基类条件分支数量、接口方法被空实现的比例;任一指标越过表中风险线,就停止继续扩量。
  • 回滚时:先执行「把胖接口拆成支付、退款、查询能力」控制影响,再回退代码或参数;涉及持久状态时必须额外核对结果差异。
  • 发布后:至少覆盖一个完整峰值周期,确认「把工具方法相似误判为 is-a 关系」没有再次出现,才关闭变更观察窗口。

方案对比与选型

方案 更适合的场景 主要收益 代价与边界
接口 跨实现稳定契约和多能力组合 低耦合、便于替换与测试 不能自然承载实例状态
抽象类 同族对象共享受控状态与模板流程 可复用字段和部分实现 单继承且容易形成脆弱基类
组合与委托 行为可独立变化或运行时替换 职责清晰、扩展灵活 对象数量和协作关系更多

选型至少带上 对象创建率、调用频次、输入规模和 API 边界,并用上面的量化基线验证;未知数据应明确为待测假设。

设计边界与工程取舍

接口与抽象类不是互斥选项:常见设计是接口定义边界,抽象类提供可选骨架,组合承载可变能力;是否共享状态是关键判断。

工程落地遵循:优先保证语言语义、类型契约和向后兼容,再讨论微小性能收益。回答时直接引用「java.util.AbstractList」、配置实验和事故数据,比复述固定模板更有说服力。