Skip to content

关于接口和抽象类

接口抽象类 都是实现多态代码复用的重要机制,但它们的设计目的、使用场景和语义有本质区别。 这几天有学弟刚学习Java,提到了这两者的问题,所以就来简单的聊聊我的理解。

一个例子

假设我们要做一个“通知系统”,支持邮件、短信、站内信三种方式。

方案一:全用接口

java
    public interface Notifier {
        void send(String message);
    }

然后 EmailNotifier, SmsNotifier 类各自实现。看起来干净,但问题来了:
每种通知都要写一遍“记录日志”、“检查权限”、“重试机制”……

逻辑一样,重复三遍,明显不合理。

方案二:全用抽象类

java
    public abstract class BaseNotifier {
        protected Logger logger = LoggerFactory.getLogger(getClass());
        
        public final void send(String message) {
            logger.info("准备发送通知");
            doSend(message);
            logger.info("发送完成");
        }
        
        protected abstract void doSend(String message);
    }

子类只需要实现 doSend,公共逻辑全在父类。代码复用是好了,但新问题来了:

如果某个通知类还要支持“可取消”(比如短信能撤回),怎么办?

Java 不允许多继承,没法再继承一个 Cancelable 抽象类。

其实,接口和抽象类解决的是两类不同的问题

抽象类:“它是什么?

比如 EmailNotifier 本质上和 SmsNotifier 共享“通知”的身份,所以可以把共性抽到 BaseNotifier 里。

接口:“它能做什么?

比如“可取消”不是所有通知都有的能力,而是一种附加行为。这时候定义一个 Cancelable 接口,让需要的类去实现,完全不影响继承结构。

实际的选择

我目前接触到的项目中,接口的使用频率远高于抽象类。几乎每个模块都是先定义接口,再由具体类去实现。这是一种 面向接口 的编程。

个人认为,抽象类更适合用来表达具有共同本质的实体之间的继承关系,用来作为多种实体类的基类,比如 StudentTeacher,就可以使用 Person 来抽象。

而接口,则更适合描述“某个对象能做什么”,而不关心它“是什么”。比如 Student 类的接口应当有 study() 方法,而 Teacher 类的接口应当有 teach() 方法。


总的来说,它们不是互斥的,而是互补的:

  • 抽象类组织“同类事物”的共性;
  • 接口定义“跨类能力”的契约。

虽然有网上有一种论调是“能用接口就别用抽象类”,显得更“面向接口编程”。
但是我觉得不能一刀切,强行用接口代替抽象类,反而会让代码更啰嗦、更难维护

就像刚刚聊到的通知系统的例子,

java
// 身份:它是一个通知器
public abstract class BaseNotifier { ... }

// 能力:它还能被取消
public interface Cancelable { 
    void cancel();
}

// 具体实现类
public class SmsNotifier extends BaseNotifier implements Cancelable {
    public void cancel() { ... }
}

这样,结构清晰,扩展也方便。