设计模式学习笔记 - 面向对象 - 5.接口和抽象类的区别

简述

在面向对象编程中,抽象类和接口是常被用到的语法概念,是面向对象四大特性,以及很多设计模式、设计思想、设计原则实现的基础。它们之间的区别是什么?什么时候用接口?什么时候用抽象类?抽象类和接口存在的意义是什么?等等


1.什么是抽象类和接口?它们有什么区别?

我们来看下 Java 语言中是如何定义抽象类的 。下面这段代码是一个比较典型的抽象类的使用场景(模板方法模式)。Logger 是一个记录日志的抽象类,FileLoggerMessageQueueLogger 继承 Logger ,分别实现两种不同的日志记录方式:记录日志到文件和记录日志到消息队列。FileLoggerMessageQueueLogger 两个子类复用了父类 Logger 中的 nameenabledminPermittedLevel 属性和 log() 方法,但是因为这两个子类的写日志方式不同,它们又各种重写了父类中的 doLog() 方法。

java 复制代码
// 抽象类
public abstract class Logger {
    private String name;
    private boolean enabled;
    private Level minPermittedLevel;

    public Logger(String name, boolean enabled, Level minPermittedLevel) {
        this.name = name;
        this.enabled = enabled;
        this.minPermittedLevel = minPermittedLevel;
    }

    public void log(Level level, String message) {
        boolean loggable = enabled && (minPermittedLevel.intValue() <= level.intValue());
        if (!loggable) return;
        doLog(level, message);
    }

    protected abstract void doLog(Level level,String message);
}

// 抽象的子类:输出日志到文件
public class FileLogger extends Logger {
    private Writer fileWriter;
    public FileLogger(String name, boolean enabled,
                      Level minPermittedLevel, String filepath) {
        super(name, enabled, minPermittedLevel);
        this.fileWriter = new FileWriter(filepath);
    }

    @Override
    protected void doLog(Level level, String message) {
        // 格式化level和message,输出日志文件
        fileWriter.write(...);
    }
}

// 抽象的子类:输出日志到消息中间件(比如Kafka)
public class MessageQueueLogger extends Logger {
    private MessageQueueClient msgQueueClient;
    public MessageQueueLogger(String name, boolean enabled,
                              Level minPermittedLevel, MessageQueueClient msgQueueClient) {
        super(name, enabled, minPermittedLevel);
        this.msgQueueClient = msgQueueClient;
    }

    @Override
    protected void doLog(Level level, String message) {
        // 格式化level和message,输出到消息中间件
        msgQueueClient.send(...);
    }
}

通过上面这个例子,我们来总结下抽象类的特性:

  • 抽象类不允许被实例化,只能被继承。
  • 抽象类可以包含属性和方法。方法既可以包含代码实现(比如 Logger 中的 log()),也可以不包含代码实现(比如 Logger 中的 doLog())。不包含代码实现的方法叫做抽象方法。
  • 子类继承抽象类,必须实现抽象类中的所有抽象方法,对应到例子代码中,所有继承 Logger 的子类,都必须重写 doLog()

再来看下,在 Java 语言中,如何定义接口

java 复制代码
// 接口
public interface Filter {
    void doFilter(RpcRequest req) throws RpcException;
}

// 接口实现类:鉴权过滤器
public class AuthFilter implements Filter {
    @Override
    public void doFilter(RpcRequest req) throws RpcException {
        // 鉴权逻辑...
    }
}

// 接口实现类:限流过滤器
public class RateLimitFilter implements Filter {
    @Override
    public void doFilter(RpcRequest req) throws RpcException {
        // 限流逻辑...
    }
}

// 过滤器使用Demo
public class Application {
    private List<Filter> filters = new ArrayList<>();
    // filters.add(new AuthFilter());
    // filters.add(new RateLimitFilter());

    public void handleRpcRequest(RpcRequest req) {
        try {
            for (Filter filter : filters) {
                filter.doFilter(req);
            }
        } catch (RpcException e) {
            // 处理过滤异常...
        }
        // 省略其他逻辑...
    }
}

上面这段代码是一个比较典型的接口的使用场景。我们通过 Java 语言中的 interface 关键字定义了一个 Filter 接口。AuthFilterRateLimitFilter 是接口的两个实现类,分别实现了对 RPC 请求鉴权和限流的过滤功能。

总结下接口的三个特性:

  • 接口不能包含属性
  • 接口只能申明方法,方法不能包含代码实现。
  • 类实现接口的时候,必须实现接口类中声明的所有方法。

从语法特性上对比,两种有比较大的差别,比如抽象类中可以定义属性、实现方法,而接口中不能定义属性,方法也不能实现。除了语法特性,从设计角度,两种也是由较大的区别的。

抽象类实际上是类,只不过是一种特殊的类,这种类不能被实例化为对象,只能被子类继承。我们知道继承是一种 is-a 的的关系,那抽象类既然属于类,也表示一种 is-a 的关系。相对于抽象类的 is-a 来说,接口表示一种 has-a 关系,表示具有某些功能。对于接口,有一种更加形象的叫法,那就是协议(contract)。

2.抽象类和接口能解决什么问题?

首先,看一下为什么需要抽象类?它能够解决什么编程问题?

刚刚讲过抽象类不能实例化,只能被继承。而继承能解决代付复用问题,所以抽象类也是为代码复用而生的。多个子类可以继承抽象类中定义的熟悉和方法,避免在子类在编写重复的代码。

既然,继承本身能到到代码复用的目的,那不用抽象类也能实现继承和复用。那抽象类除了解决代码复用的问题,还有什么其他的意义吗?

还是之前日志的例子,来讲解。Logger 不再是抽象类,而是一个普通的父类,删除了 log()doLog() 方法,新增了 isLoggable() 方法。FileLoggerMessageQueueLogger 还是继承 Logger ,以达到代码复用的目的。

java 复制代码
// 父类:非抽象类,就是普通的类. 删除了log(),doLog(),新增了isLoggable().
public class Logger {
    private String name;
    private boolean enabled;
    private Level minPermittedLevel;

    public Logger(String name, boolean enabled, Level minPermittedLevel) {
        //...构造函数不变,代码省略...
    }

    public boolean isLoggable(Level level) {
        boolean loggable = enabled && (minPermittedLevel.intValue() <= level.intValue());
        return loggable;
    }
}

// 子类:输出日志到文件
public class FileLogger extends Logger {
    private Writer fileWriter;
    public FileLogger(String name, boolean enabled,
                      Level minPermittedLevel, String filepath) {
        //...构造函数不变,代码省略...
    }
    public void log(Level level, String message) {
        if (!isLoggable(level)) return;
        // 格式化level和message,输出到日志文件
        fileWriter.write(...);
    }
}
// 子类: 输出日志到消息中间件(比如kafka)
public class MessageQueueLogger extends Logger {
    private MessageQueueClient msgQueueClient;
    public MessageQueueLogger(String name, boolean enabled,
                              Level minPermittedLevel, MessageQueueClient msgQueueClient) {
        //...构造函数不变,代码省略...
    }
    public void log(Level level, String message) {
        if (!isLoggable(level)) return;
        // 格式化level和message,输出到消息中间件
        msgQueueClient.send(...);
    }
}

这个涉及思路虽然达到了代码复用的目的,但是无法使用多态特性了。像下面这样编写代码,就会出现编译报错。

java 复制代码
Logger logger = new FileLogger("access-log", true, Level.WARN, "/file/access.log");
logger.log(Level.ERROR, "This is a test log message.");

你可以能会说,这个问题解决起来很简单啊,在 Logger 中定义一个空的 log() 方法,让子类重写父类的 log() 方法,实现自己的记录日志的逻辑,不就可以了吗?

java 复制代码
public class Logger {
    // 省略其他代码...
    public void log(Level level, String message) { //do nothing... }
}

public class FileLogger extends Logger {
    // 省略其他代码...
    @Override
    public void log(Level level, String message) {
        if (!isLoggable(level)) return;
        // 格式化level和message,输出到日志文件
        fileWriter.write(...);
    }
}
public class MessageQueueLogger extends Logger {
    // 省略其他代码...
    @Override
    public void log(Level level, String message) {
        if (!isLoggable(level)) return;
        // 格式化level和message,输出到消息中间件
        msgQueueClient.send(...);
    }
}

这个设计思路虽然可以用,但是,它显然没有之前通过抽象类的实现思路优雅。主要有以下几点原因:

  • Logger 中定义一个空的 log() 方法,会影响代码的可读性 。如果我们不熟悉 Logger 背后的设计思想,代码注释有不太好,我们在阅读 Logger 代码的时候,就可能对为什么定义一个空的 log() 方法感到疑惑,需要查看 LoggerFileLoggerMessageQueueLogger 之间的继承关系,才能弄明白其设计意图。
  • 当创建一个新的子类继承 Logger 的时候,我们可能会忘记重新实现 log() 方法 。不像抽象类,编译器会强制要求子类重写 log() 方法。你可能会说,怎么可能会忘记重新实现呢? 我们举个简单的例子,如果 Logger 代码有几百行,有 n 个方法,这个时候你很有可能会忘记重写 log() 方法。
  • Logger 可以被实例化 ,换句话说,我们可以 new 一个 Logger 出来,并调用空的 log() 方法。这增加了类被误用的风险。当然,这个问题可以通过设置私有的构造函数的方式来解决。不过显然没有通过抽象类来的优雅。

其次,我们再来看一下,我们为什么需要接口?它能解决什么问题?

抽象类更多的是为了代码复用,而接口就侧重于解耦。接口是对行为的一种抽象,相当于一组协议或契约,你可以类比 API 接口。调用者只需要关注抽象的接口,不需要了解具体的实现。接口实现了约定和实现分离,可以降低代码间的耦合性,提供代码的可扩展性

实际上,接口是一个比较抽象类更加广发、更加重要的知识点。比如,我们经常提到的"基于接口而非实现编程",就是一条几乎天天会被用到,并且能极大地提高代码的灵活性、扩展性的设计思想。

3. 如何决定该用抽象类还是接口?

实际上判断标准很简单。如果我们要表示一种 is-a 的关系,并且是为了解决代码复用的问题 ,我们就用抽象类 ;如果我们要表示一种 has-a 的关系,并且是为了解决抽象而非代码复用的问题 ,我们就用接口

从类层次上来看,抽象类是一种自下而上的设计思路,现有子类的代付重复,然后再抽象成上层的父类(也就是抽象类)。而接口正好相反,它是一种自上而下的设计思路。我们在编程的时候,一般都是先设计接口,再去考虑具体实现。

总结

1.抽象类和接口的语法特性

抽象类不允许实例化,只能被继承。它可以包含属性和方法。方法既可以包含代码实现,也可以不包含代码实现。不包含代码实现的方法叫做抽象方法,必须由子类实现。

接口不能包含属性,只能声明方法,方法不能包含代码实现。类实现接口的时候,必须实现接口中申明的所有方法。

2.抽象类和接口的意义

抽象类是对成员变量和方法的抽象,是一种 is-a 的关系,是为了解决代付复用的问题。

接口仅是对方法的抽象,是一种 has-a 的关系,表示某一组行为特性,是为了解决解耦问题,隔离接口和具体的实现,提高带代码的可扩展性。

3.抽象类和接口的应用场景

判断标准很简单。

如果要表示一种 is-a 的关系,并且是为了解决代付复用的问题,我们就用抽象类。

如果要表示一种 has-a 的关系,并且是为了解决抽象而非代码复用问题,我们就用接口。

相关推荐
bryant_meng10 天前
【C++】Virtual function and Polymorphism
c++·多态·抽象类·虚函数·纯虚函数
learning-striving18 天前
eNSP中路由器的CON/AUX接口、GE Combo接口、Mini USB接口、USB接口、WAN侧uplink接口、FE接口、GE接口介绍
网络·智能路由器·接口·路由器·ensp
架构文摘JGWZ18 天前
11 个接口性能优化技巧(下)
后端·学习·性能优化·接口
蟹至之1 个月前
类和对象(6)——Object类、内部类
java·开发语言·接口·深拷贝与浅拷贝·内部类·object类
@PHARAOH1 个月前
HOW - 服务接口超时时间和建议策略
前端·接口·接口超时
蟹至之1 个月前
类和对象(5)——抽象类和接口
java·接口·类和对象·抽象类
lihan_freak2 个月前
java中的抽象类和接口
java·开发语言·接口·抽象类
涛tao讲道2 个月前
FPGA的IP核接口引脚含义-快解
接口·fpga·vivado·xilinx·ip核·涛tao讲道·抗单粒子翻转
小张认为的测试2 个月前
Liunx上Jenkins 持续集成 Java + Maven + TestNG + Allure + Rest-Assured 接口自动化项目
java·ci/cd·jenkins·maven·接口·testng
ZenasLDR2 个月前
Type-C充电与智能家居的结合
接口·芯片·usb