Java 接口和抽象类的区别详解

作者:yanliang 时间:2021-07-26 13:30:21 

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

    • 抽象类

    • 接口

  • 抽象类和接口解决了什么问题?

    • 如何模拟抽象类和接口

      • 如何决定该用抽象还是接口?

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

        不同的编程语言对接口和抽象类的定义方式可能有些差别,但是差别并不大。本文使用 Java 语言。

        抽象类

        下面我们通过一个例子来看一个典型的抽象类的使用场景。

        Logger 是一个记录日志的抽象类,FileLogger 和 MessageQueueLogger 继承Logger,分别实现两种不同的日志记录方式:

        • 记录日志到文件中

        • 记录日志到消息队列中

        FileLogger 和 MessageQueuLogger 两个子类复用了父类 Logger 中的name、enabled 以及 minPermittedLevel 属性和 log 方法,但是因为两个子类写日志的方式不同,他们又各自重写了父类中的doLog方法。

        父类


        import java.util.logging.Level;

        /**
        * 抽象父类
        * @author yanliang
        * @date 9/27/2020 5:59 PM
        */
        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);
        }

        FileLogger


        import java.io.FileWriter;
        import java.io.IOException;
        import java.io.Writer;
        import java.util.logging.Level;

        /**
        * 抽象类Logger的子类:输出日志到文件中
        * @author yanliang
        * @date 9/28/2020 4:44 PM
        */
        public class FileLogger extends Logger {

        private Writer fileWriter;

        public FileLogger(String name, boolean enabled, Level minPermittedLevel, String filePath) throws IOException {
               super(name, enabled, minPermittedLevel);
               this.fileWriter = new FileWriter(filePath);
           }

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

        MessageQueuLogger


        import java.util.logging.Level;

        /**
        * 抽象类Logger的子类:输出日志到消息队列中
        * @author yanliang
        * @date 9/28/2020 6:39 PM
        */
        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;
           }

        @Override
           protected void doLog(Level level, String message) {
               // 格式化level 和 message,输出到消息队列中
               messageQueueClient.send(...)
           }
        }

        通过上面的例子,我们来看下抽象类有哪些特性。

        • 抽象类不能被实例化,只能被继承。(new 一个抽象类,会报编译错误)

        • 抽象类可以包含属性和方法。方法既可以包含实现,也可以不包含实现。不包含实现的方法叫做抽象方法

        • 子类继承抽象类,必须实现抽象类中的所有抽象方法。

        接口

        同样的,下面我们通过一个例子来看下接口的使用场景。


        /**
        * 过滤器接口
        * @author yanliang
        * @date 9/28/2020 6:46 PM
        */
        public interface Filter {
           void doFilter(RpcRequest req) throws RpcException;
        }

        /**
        * 接口实现类:鉴权过滤器
        * @author yanliang
        * @date 9/28/2020 6:48 PM
        */
        public class AuthencationFilter implements Filter {

        @Override
           public void doFilter(RpcRequest req) throws RpcException {
               // 鉴权逻辑
           }
        }

        /**
        * 接口实现类:限流过滤器
        * @author yanliang
        * @date 9/28/2020 6:48 PM
        */
        public class RateLimitFilter implements Filter{

        @Override
           public void doFilter(RpcRequest req) throws RpcException {
               // 限流逻辑
           }
        }

        /**
        * 过滤器使用demo
        * @author yanliang
        * @date 9/28/2020 6:48 PM
        */
        public class Application {
           // 过滤器列表
           private List<Filter> filters = new ArrayList<>();
           filters.add(new AuthencationFilter());
           filters.add(new RateLimitFilter());

        public void handleRpcRequest(RpcRequest req) {
               try {
                   for (Filter filter : filters) {
                       filter.doFilter(req);
                   }
               } catch (RpcException e) {
                   // 处理过滤结果
               }
               // ...
           }
        }

        上面的案例是一个典型的接口使用场景。通过Java中的 interface 关键字定义了一个Filter 接口,AuthencationFilter 和 RetaLimitFilter 是接口的两个实现类,分别实现了对Rpc请求的鉴权和限流的过滤功能。

        下面我们来看下接口的特性:

        • 接口不能包含属性(也就是成员变量)

        • 接口只能生命方法,方法不能包含代码实现

        • 类实现接口时,必须实现接口中生命的所有方法。

        综上,从语法上对比,这两者有比较大的区别,比如抽象类中可以定义属性、方法的实现,而接口中不能定义属性,方法也不能包含实现等。

        除了语法特性的不同外,从设计的角度,这两者也有较大区别。抽象类本质上就是类,只不过是一种特殊的类,这种类不能被实例化,只能被子类继承。属于is-a的关系。接口则是 has-a 的关系,表示具有某些功能。对于接口,有一个更形象的叫法:协议(contract)

        抽象类和接口解决了什么问题?

        下面我们先来思考一个问题~

        抽象类的存在意义是为了解决代码复用的问题(多个子类可以继承抽象类中定义的属性哈方法,避免在子类中,重复编写相同的代码)。

        那么,既然继承本身就能达到代码复用的目的,而且继承也不一定非要求是抽象类。我们不适用抽象类,貌似也可以实现继承和复用。从这个角度上讲,我们好像并不需要抽象类这种语法呀。那抽象类除了解决代码复用的问题,还有其他存在的意义吗?

        这里大家可以先思考一下哈~

        我们还是借用上面Logger的例子,首先对上面的案例实现做一些改造。在改造之后的实现中,Logger不再是抽象类,只是一个普通的父类,删除了Logger中的两个方法,新增了 isLoggable()方法。FileLogger 和 MessageQueueLogger 还是继承Logger父类已达到代码复用的目的。具体代码如下:


        /**
        * 父类:非抽象类,就是普通的类
        * @author yanliang
        * @date 9/27/2020 5:59 PM
        */
        public 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 boolean isLoggable(Level level) {
               return enabled && (minPermittedLevel.intValue() <= level.intValue());
           }

        }

        /**
        * 抽象类Logger的子类:输出日志到文件中
        * @author yanliang
        * @date 9/28/2020 4:44 PM
        */
        public class FileLogger extends Logger {

        private Writer fileWriter;

        public FileLogger(String name, boolean enabled, Level minPermittedLevel, String filePath) throws IOException {
               super(name, enabled, minPermittedLevel);
               this.fileWriter = new FileWriter(filePath);
           }

        protected void log(Level level, String message) {
               if (!isLoggable(level)) return ;
               // 格式化level 和 message,输出到日志文件
               fileWriter.write(...);
           }
        }

        package com.yanliang.note.java.abstract_demo;

        import java.util.logging.Level;

        /**
        * 抽象类Logger的子类:输出日志到消息队列中
        * @author yanliang
        * @date 9/28/2020 6:39 PM
        */
        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;
           }

        protected void log(Level level, String message) {
               if (!isLoggable(level)) return ;
               // 格式化level 和 message,输出到消息队列中
               messageQueueClient.send(...)
           }
        }

        以上实现虽然达到了代码复用的目的(复用了父类中的属性),但是却无法使用多态的特性了。

        像下面这样编写代码就会出现编译错误,因为Logger中并没有定义log()方法。


        Logger logger = new FileLogger("access-log", true, Level.WARN, "/user/log");
        logger.log(Level.ERROR, "This is a test log message.");

        如果我们在父类中,定义一个空的log()方法,让子类重写父类的log()方法,实现自己的记录日志逻辑。使用这种方式是否能够解决上面的问题呢? 大家可以先思考下~

        这个思路可以用使用,但是并不优雅,主要有一下几点原因:

        • 在Logger中定义一个空的方法,会影响代码的可读性。如果不熟悉Logger背后的设计思想,又没有代码注释的话,在阅读Logger代码时就会感到疑惑(为什么这里会存在一个空的log()方法)

        • 当创建一个新的子类继承Logger父类时,有时可能会忘记重新实现log方法。之前是基于抽象类的设计思想,编译器会强制要求子类重写父类的log方法,否则就会报编译错误。

        • Logger可以被实例化,这也就意味着这个空的log方法有可能会被调用。这就增加了类被误用的风险。当然,这个问题 可以通过设置私有的构造函数的方式来解决,但是不如抽象类优雅。

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

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

        如何模拟抽象类和接口

        在前面列举的例子中,我们使用Java的接口实现了Filter过滤器。不过,在 C++ 中只提供了抽象类,并没有提供接口,那从代码的角度上说,是不是就无法实现 Filter 的设计思路了呢? 大家可以先思考下 🤔 ~

        我们先会议下接口的定义:接口中没有成员变量,只有方法声明,没有方法实现,实现接口的类必须实现接口中的所有方法。主要满足以上几点从设计的角度上来说,我们就可以把他叫做接口。

        实际上,要满足接口的这些特性并不难。下面我们来看下实现:


        class Strategy {
         public:
           -Strategy();
           virtual void algorithm()=0;
         protected:
           Strategy();
        }

        抽象类 Strategy 没有定义任何属性,并且所有的方法都声明为 virtual 类型(等同于Java中的abstract关键字),这样,所有的方法都不能有代码实现,并且所有继承了这个抽象类的子类,都要实现这些方法。从语法特性上看,这个抽象类就相当于一个接口。

        处理用抽象类来模拟接口外,我们还可以用普通类来模拟接口。具体的Java实现如下所示:


        public class MockInterface {
         protected MockInteface();
         public void funcA() {
           throw new MethodUnSupportedException();
         }
        }

        我们知道类中的方法必须包含实现,这个不符合接口的定义。但是,我们可以让类中的方法抛出 MethodUnSupportedException 异常,来模拟不包含实现的接口,并且强迫子类来继承这个父类的时候,都主动实现父类的方法,否则就会在运行时抛出异常。

        那又如何避免这个类被实例化呢? 实际上很简单,我们只需要将这个类的构造函数声明为 protected 访问权限就可以了。

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

        上面的讲解可能偏理论,现在我们就从真实项目开发的角度来看下。在代码设计/编程时,什么时候该用接口?什么时候该用抽象类?

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

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

        好了,你是否掌握了上面的内容呢。你可以通过一下几个维度来回顾自检一下:

        • 抽象类和接口的语法特性

        • 抽象类和接口存在的意义

        • 抽象类和接口的应用场景有哪些

        来源:https://gyl-coder.top/java/abstract_interface/

        标签:Java,接口,抽象类
        0
        投稿

        猜你喜欢

      • C# 7.2中结构体性能问题的解决方案

        2022-08-12 23:04:26
      • .net使用Aspose.Words进行Word替换操作的实现代码

        2023-03-23 21:28:59
      • springboot-2.3.x最新版源码阅读环境搭建(基于gradle构建)

        2022-11-19 01:47:41
      • Flutter 通过Clipper实现各种自定义形状的示例代码

        2023-06-19 14:25:11
      • java 发送带Basic Auth认证的http post请求实例代码

        2021-11-03 06:21:20
      • Spring Boot和Thymeleaf整合结合JPA实现分页效果(实例代码)

        2023-11-25 07:05:15
      • C#求n个数中最大值和最小值的方法

        2022-04-20 08:00:01
      • Android下拉刷新以及GridView使用方法详解

        2022-12-26 18:01:24
      • Java并发编程之线程创建介绍

        2021-11-14 13:55:40
      • 创建动态代理对象bean,并动态注入到spring容器中的操作

        2021-09-04 01:02:43
      • 详解Java中的三种流程控制语句

        2023-11-04 17:06:34
      • Android实现图片左右滑动效果

        2022-11-30 09:30:35
      • 浅谈java的守护线程与非守护线程

        2023-11-25 06:50:23
      • SWT(JFace) 文本编辑器 实现代码

        2023-01-27 22:39:37
      • Spring实战之属性覆盖占位符配置器用法示例

        2023-02-02 00:05:30
      • java如何实现字符串中的字母排序

        2021-09-17 14:55:11
      • Android中去掉标题栏的几种方法(三种)

        2023-04-01 10:57:37
      • c#使用windows服务更新站点地图的详细示例

        2021-07-24 10:45:52
      • java锁synchronized面试常问总结

        2023-08-01 05:11:37
      • java多线程之停止线程的方法实例代码详解

        2023-03-23 04:35:21
      • asp之家 软件编程 m.aspxhome.com