引言:
接口VS抽象类的区别? 如何用普通的类模拟抽象类和接口?
在面向对象编程中, 接口和抽象类是两个经常用到的语法概念, 是面向对象四大特性, 设计思想, 设计原则等编程实现的基础.
如:
- 用接口来实现面向对象抽象特性, 多态特性和基于接口而非实现的设计原则.
- 用抽象类来实现面向对象的继承特性和模版设计模式等
提问:
- 接口和抽象类的区别是什么?
- 什么时候用接口? 什么时候用抽象类?
- 抽象类和接口存在的意义是什么?能解决哪些编程问题?
一. 什么是抽象类和接口? 区别在哪里?
设计模式中: 模版设计模式就是一种典型的抽象类的使用例子.
举例子
Logger是一个记录日志的抽象类, FileLogger和MessageQueueLogger都继承Logger, 分别实现2种不同的记录日志的方式; FileLogger记录到文件中, MessageQueueLogger则记录到消息队列中.
2个子类复用了父类中的name, enabled, minPermittedLevel成员属性和log()方法.
由于2个子类实现写日志的方式不一致, 所以他们各自重写了父类的doLog()抽象方法;
1.1 抽象类实现方式
package org.example.case_02;
import java.util.logging.Level;
/**
* 记录日志 抽象类
* @Author: Nisy
* @Date: 2023/06/01/23:36
*/
public abstract class Logger {
private String name;
private boolean enabled;
private Level minPermittedLevel;
/**
* 构造方法
* @param name
* @param enabled
* @param minPermittedLevel
*/
public Logger(String name, boolean enabled, Level minPermittedLevel) {
this.name = name;
this.enabled = enabled;
this.minPermittedLevel = minPermittedLevel;
}
/**
* 对外暴露的log方法 (这里是模版方法模式)
* @param level
* @param message
*/
public void log(Level level, String message){
//统一校验参数, 达到代码复用
boolean loggable = enabled && (minPermittedLevel.intValue() <= level.intValue());
if (!loggable){
return;
}
//校验完参数后, 记日志, 走各自的抽象方法的重写
doLog(level, message);
}
/**
* 抽象记录日志的方式
* @param level
* @param message
*/
protected abstract void doLog(Level level, String message);
}
package org.example.case_02;
import java.io.IOException;
import java.io.Writer;
import java.util.logging.Level;
/**
* 记录日志到文件中
* @Author: Nisy
* @Date: 2023/06/01/23:40
*/
public class FileLogger extends Logger{
private Writer fileWriter;
public FileLogger(String name, boolean enabled, Level minPermittedLevel, Writer fileWriter) {
super(name, enabled, minPermittedLevel);
this.fileWriter = fileWriter;
}
/**
* 重写抽象方法doLog()
* @param level
* @param message
* @desc: 子类继承抽象类, 必须重写父类中所有的抽象方法
*/
@Override
protected void doLog(Level level, String message) {
try {
fileWriter.write("写日志到文件中");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
package org.example.case_02;
import java.util.logging.Level;
/**
* 记录日志到消息队列中
* @Author: Nisy
* @Date: 2023/06/01/23:45
*/
public class MessageQueueLogger extends Logger{
private MessageQueueClient messageQueueClient;
public MessageQueueLogger(String name, boolean enabled, Level minPermittedLevel, MessageQueueClient messageQueueClient) {
super(name, enabled, minPermittedLevel);
this.messageQueueClient = messageQueueClient;
}
/**
* 重写抽象方法doLog()
* @param level
* @param message
*/
@Override
protected void doLog(Level level, String message) {
messageQueueClient.send();
}
}
1.1.1 抽象类特性总结
- 抽象类不能被实例化, 只能被继承
- 抽象类可以包含属性和方法, 包含实现的方法可对外提供, 不包含实现的方法叫抽象方法, 由子类去实现
- 子类继承抽象类, 必须重写抽象类中所有的抽象方法
1.2 接口的实现方式
过滤器接口:
package org.example.case_02.intr;
import org.example.case_02.intr.resp.RpcRequest;
/**
* 过滤器接口
* @Author: Nisy
* @Date: 2023/06/08/22:45
*/
public interface Filter {
void doFilter(RpcRequest req) throws Exception;
}
接口实现类: 鉴权过滤器
package org.example.case_02.intr;
import org.example.case_02.intr.resp.RpcRequest;
/**
* 接口实现类: 鉴权过滤器
* @Author: Nisy
* @Date: 2023/06/08/22:47
*/
public class AuthencationFilter implements Filter {
@Override
public void doFilter(RpcRequest req) throws Exception {
//鉴权逻辑
}
}
接口实现类: 限流过滤器
package org.example.case_02.intr;
import org.example.case_02.intr.resp.RpcRequest;
/**
* 接口实现类: 限流过滤器
* @Author: Nisy
* @Date: 2023/06/08/22:55
*/
public class RateLimitFilter implements Filter{
@Override
public void doFilter(RpcRequest req) throws Exception {
//...限流逻辑...
}
}
过滤器使用Demo:
package org.example.case_02.intr;
import org.example.case_02.intr.resp.RpcRequest;
import java.util.ArrayList;
import java.util.List;
/**
* 过滤器使用demo
* @Author: Nisy
* @Date: 2023/06/08/22:55
*/
public class Application {
private List<Filter> filters = new ArrayList<>();
public void handRequest(RpcRequest req){
try {
for (Filter filter : filters) {
filter.doFilter(req);
}
} catch (Exception e) {
//处理过滤异常
}
}
}
1.2.1 接口特性总结
- 接口不能包含属性(成员变量).
- 接口只能声明方法, 方法不能包含代码实现(jdk1.8之后, 可以有default实现).
- 类实现接口的时候, 必须实现接口中声明的所有方法.
1.3 总结
接口和抽象类的不同:
(1). 语法特性 抽象类: 可以定义属性, 方法实现 接口: 不能定义属性, 方法也不能包含代码实现(jdk8可以用default关键字)
(2). 设计角度 抽象类是一种类的定义, 而接口更像是协议. 前者只能被子类继承, 继承是: is-a关系. 而接口是: has-a关系, 表示具有某些功能.
二. 抽象类和接口能解决什么编程问题?
2.1 为什么需要抽象类? 能解决什么编程问题?
- 代码复用
- 优雅的多态实现
因为抽象类不能被实例化, 只能被继承.而继承能解决代码复用问题. 所以得出结论: 抽象类是为了代码复用而生.
问: 普通的类也可以实现继承, 为什么非得要抽象类呢? 是不是还存在其他的意义?
答: 类 + 继承可以实现代码复用, 但是抽象 + 类 + 继承可以实现代码复用 + 多态, 让代码更加优美.
2.2 错误示范
还是用之前"打印日志"的例子, 来讲解.
定义一个Logger普通类
package org.example.case_02.abs;
import java.util.logging.Level;
/**
* 普通logger父类(非抽象类)
* @Author: Nisy
* @Date: 2023/06/09/12:37
*/
public class Logger {
private String name;
private boolean enabled;
private Level minPermittedLevel;
/**
* 构造函数
* @param name
* @param enabled
* @param minPermittedLevel
* @desc 构造函数不变
*/
public Logger(String name, boolean enabled, Level minPermittedLevel) {
this.name = name;
this.enabled = enabled;
this.minPermittedLevel = minPermittedLevel;
}
/**
* 新增isLoggable方法
* @return
*/
protected boolean isLoggable(){
boolean loggable = enabled && (minPermittedLevel.intValue() <= Level.INFO.intValue());
return loggable;
}
}
子类: 输出日志到文件
package org.example.case_02.abs;
import java.io.IOException;
import java.io.Writer;
import java.util.logging.Level;
/**
* 子类: 输出到日志文件中
* @Author: Nisy
* @Date: 2023/06/09/12:41
*/
public class FileLogger extends Logger{
private Writer fileWriter;
private String filePath;
public FileLogger(String name, boolean enabled, Level minPermittedLevel, String filePath) {
super(name, enabled, minPermittedLevel);
this.filePath = filePath;
}
@Override
protected boolean isLoggable() {
return super.isLoggable();
}
public void log(Level level, String message){
if (!isLoggable()){
return;
}
//格式化level和message, 输出到日志文件中
try {
fileWriter.write("....");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
子类: 输出日志到消息中间件
package org.example.case_02.abs;
import org.example.case_02.MessageQueueClient;
import java.util.logging.Level;
/**
* 子类: 输出日志到消息中间件中(比如: kafka)
* @Author: Nisy
* @Date: 2023/06/09/12:44
*/
public class MessageQueueLogger extends Logger{
private MessageQueueClient messageQueueClient;
/**
* 构造函数不变
* @param name
* @param enabled
* @param minPermittedLevel
* @param messageQueueClient
*/
public MessageQueueLogger(String name, boolean enabled, Level minPermittedLevel, MessageQueueClient messageQueueClient) {
super(name, enabled, minPermittedLevel);
this.messageQueueClient = messageQueueClient;
}
public void log(Level level, String message){
if (!isLoggable()){
return;
}
//格式化level和message, 输出到消息中间件
messageQueueClient.send();
}
}
应用:
package org.example.case_02.abs;
import java.util.logging.Level;
/**
* @Author: Nisy
* @Date: 2023/06/09/12:47
*/
public class LogMain {
public static void main(String[] args) {
Logger logger = new FileLogger("access-log", true, Level.WARNING, "/users/nisy/access");
//编译报错, 因为父类Logger中并没有定义log方法
logger.log(Level.WARNING, "This is a test log message");
}
}
总结: 我们发现logger.log在编译的时候报错了, 因为我们父类Logger中并没有定义log方法. 虽然以上这段代码达到了代码复用的目的, 但是无法使用多态特性了.
有人会说, 我们可以在Logger类中定义一个空的log()方法, 让子类重写父类的log()方法, 不就可以了吗?
答案是否定的, 有以下几点原因:
- Logger类中定义空方法log(), 会影响代码的可读性(在不熟悉logger背后的设计思想时, 会让读者产生疑惑)
- 当创建新的子类继承Logger父类时, 我们可能会忘记重写log()方法. 但是基于抽象类的设计思路, 编译器会强制我们重写log方法.
- Logger类可以被实例化, 我们可以new一个Logger类, 调用空的log方法, 增加了类的误调用
2.3 为什么需要接口? 它能够解决什么编程问题?
抽象类解决的是代码复用, 而接口侧重于解耦.接口是对行为的一种抽象, 相当于一组协议或契约, 调用者只关心抽象出的接口, 不关心具体的实现. 接口实现了约定与实现相分离, 可以降低代码的耦合性, 提供了代码的可扩展性.
我们经常提到: 基于接口而非实现编程
三. 如何决定该用抽象类还是接口?
如果我们想要代码复用, 想实现的是一种is-a关系, 那么就用抽象类.
如果我们仅仅关注的是行为本身, 解决解耦, 想实现的是has-a关系, 那么就用接口.
从类的继承层次上来看, 抽象类是一种自下而上的设计思路, 先有子类的代码重复, 然后再抽象成上层的父类.而接口洽洽相反, 它是一种自上而下的设计思路, 我们一般都是先设计接口, 再去考虑实现.