接口vs抽象类的区别?

发布时间 2023-03-25 10:15:46作者: 码农界的小田

什么是抽象类和接口?区别在哪里?


   下面代码是一个比较典型的抽象类的使用场景(模板设计模式).

Logger是一个记录日志的抽象类,FileLogger和MessageQueueLogger继承Logger,分别实现两种不同的日志记录方式:记录日志到文件中和记录日志到消息队列中.FileLogger和MessageQueueLogger两个子类复用父类的Logger中的name、enabled、minPermittedLevel属性和log()方法,但因为这两个子类写日志的方式不同,它们又各自重写了父类中的doLog()方法.

点击查看代码
// 抽象类
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
  public void doLog(Level level, String mesage) {
    // 格式化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 mesage) {
    // 格式化level和message,输出到消息中间件
    msgQueueClient.send(...);
  }
}

   通过上面的例子,可以得到抽象类的以下结论:
      - 抽象类不允许实例化,只能被继承,也就是说,你不能new一个抽象类的对象出来(Logger logger = new Logger(...),会报编译错误).
      - 抽象类可以包含属性和方法,方法既可以包含代码实现,也可以不包含代码实现.不包含代码实现的方法叫做抽象方法.
      - 子类继承抽象类,必须实现抽象类中的所有抽象方法,对应到上面的代码就是,所有继承Logger抽象类的子类,都必须重写doLog()方法.


下面代码是一个比较典型的接口的使用场景.

  

点击查看代码
// 接口
public interface Filter {
  void doFilter(RpcRequest req) throws RpcException;
}
// 接口实现类:鉴权过滤器
public class AuthencationFilter 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 {
  // filters.add(new AuthencationFilter());
  // filters.add(new RateLimitFilter());
  private List<Filter> filters = new ArrayList<>();
  
  public void handleRpcRequest(RpcRequest req) {
    try {
      for (Filter filter : filters) {
        filter.doFilter(req);
      }
    } catch(RpcException e) {
      // ...处理过滤结果...
    }
    // ...省略其他处理逻辑...
  }
}

   我们通过Java中的interface关键字定义了一个Filter接口.AuthencationFilter和RateLimitFilter是接口的两个实现类.分别实现了对RPC请求鉴权和限流的过滤功能.
   接口的特性:
      1. 接口不能包含属性(也就是成员变量)
      2. 接口只能声明方法,方法不能包含代码实现
      3. 类实现接口的时候,必须实现接口中声明的所有方法.

抽象类和接口的应用场景

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