接口和抽象类:如何使用普通类模拟接口和抽象类

目录

1.引言

2.抽象类和接口的定义与区别

3.抽象类和接口存在的意义

4.模拟实现抽象类和接口

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


1.引言

        在面向对象编程中,抽象类和接口是两个经常被提及的语法概念,也是面向对象编程的四大特性,以及很多设计模式和设计原则编程实现的基础。例如,我们可以使用接口实现面向对象的抽象特性、多态特性和基于接口而非实现的设计原则,使用抽象类实现面向对象的继承特性和模板设计模式,等等。

        不过,并不是所有的面向对象编程语言都支持这两个语法概念,如C++这种编程语言只支持抽象类,不支持接口;而像 Python 这样的动态编程语言,既不支持抽象类,又不支持接口。尽管有些编程语言没有提供现成的语法来支持接口和抽象类,但是我们仍然可以通过一些手段模拟实现这两个语法概念。

        这两个语法概念不但在工作中经常会被用到,而且在面试中经常被提及。接口和抽象类的区别是什么?什么时候使用接口?什么时候使用抽象类?抽象类和接口存在的意义是什么?通过阅读本节内容,相信读者可以从中找到答案。

2.抽象类和接口的定义与区别

        不同的编程语言对接口和抽象类的定义方式可能有差别,但差别并不会很大。因为Java既支持抽象类,又支持接口,所以我们使用Java进行举例讲解,以便读者对这两个有直观的认识。

        首先,我们看一下如何在 Java 中定义抽象类。

       下面这段代码是一个典型的抽象类使用场景(模板设计模式)。Logger 是一个记录日志抽象类,FileLogger 类和 MessageQueueLogger 类继承 Logger 类,分别实现不同的日志记式:将日志输出到文件中和将日志输出到消息队列中。FileLogger和MessageQueueLogger 两个子类复用了父类 Logger 中的 name、enabled、minPermittedLevel 属性,以及log()方法,因为这两个子类输出日志的方式不同,所以它们又各自重写了父类中的 doLog()方法。

public abstract class Logger{
    private String name;
    private boolean enabled;
    private evel 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.intValve());
        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.Ievel minPermittedLevel,String filepath)
    {
       super(name,enabled,minPermittedLevel);
       this.fileWriter = new FileWriter(filepath);
    }

    @0verride
    public void doLog(Level level,string mesage){
            //格式化leve1和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){
        //格式化1evel和message,并输出到消息中间件
        msgQueueClient.send(...);
    }
}

        结合上述示例,我们总结了下列抽象类的特点:

        1)抽象类不允许被实例化,只能被继承。也就是说,我们不能通过关键字 new定义一个

抽象类的对象(编写“Logger logger=new Logger(..);”语句会报编译错误)。

        2)抽象类可以包含属性和方法。方法可以包含代码实现(如Logger类中的log()方法)也可以不包含代码实现(如Logger 类中的 doLog()方法)。不包含代码实现的方法称为抽象方法。

        3)子类继承抽象类时,必须实现抽象类中的所有抽象方法。对应到示例代码中,所有继承Logger 抽象类的子类都必须重写 doLog()方法。

        上面是对抽象类的定义。接下来,我们看一下如何在Java 中定义接口。我们还是先看一段示例代码。

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 {
        //...省略限流逻辑
    }
}

//过滤器使用示例
public class Application {
    private list<Filter> filters = new ArrayList<>();

    public Application(){
        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 和RateLimitFiliter是接口的两个实现类,分别实现了对RPC求鉴权和限流。结合上述代码,我们总结了下列接口的特点:

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

        2)接口只能声明方法,方法不能包含代码实现。

        3)类实现接口时,必须实现接口中声明的所有方法。

        有些读者可能说,在Java 1.8版本之后,接口中的方法可以包含代码实现,并且接口可以包含静态成员变量。注意,这只不过是Java语言对接口定义的妥协,目的是方便使用。抛开Java 这一具体的编程语言,接口仍然具有上述3个特点。

        在上文中,我们介绍了抽象类和接口的定义,以及各自的语法特性。从语法特性方面对比,抽象类和接口有较大的区别,如抽象类中可以定义属性、方法的实现,而接口中不能定义属性,方法也不能包含代码实现,等等。除语法特性以外,从设计的角度对比,二者也有较大的区别。

        抽象类也属于类,只不过是一种特殊的类,这种类不能被实例化为对象,只能被子类继承。我们知道,继承关系是一种is-a关系,那么,抽象类既然属于类,也表示一种is-a关系。相比抽象类的is-a关系,接口表示一种has-a关系(或can-do关系、behave like 关系),表示具有某些功能。因此,接口有一个形象的叫法:协议(contract)。

3.抽象类和接口存在的意义

        在上面我们介绍了抽象类和接口的定义与区别,现在我们探讨一下抽象类和接口存在的意义,以便读者知其然,知其所以然。

        为什么需要抽象类?它能够在编程中解决什么问题?在上面我们讲到,抽象类不能被实例化,只能被继承。之前,我们还讲过,继承能够解决代码复用问题。因此,抽象类是为代码复用而生的。多个子类可以继承抽象类中定义的属性和方法,这样可以避免在子类中重复编写相同的代码。

        既然继承就能达到代码复用的目的,而维承并不要求父类必须是抽象类,那么,不使用抽象类照样可以实现继承和复用。从这个角度来看,抽象类语法似乎是多余的。那么,除解决代码复用问题以外,抽象类还有其他存在的意义吗?

        我们还是结合之前打印日志的示例代码进行讲解。不过,我们需要先对之前的代码进行改造。在改造之后,Logger不再是独象类。万法一个普通类。另外,我们删除了Logger类中的log()、doLog()方法,新增了isLoggable()方法,FileLogger类和 MessageQueueLogger 类仍级继承 Logger 类。具体代码如下:

//父类:Logger, 非抽象类就是普通类,删除了log()和doLog()方法,新增了 isLoggeable()方法
public abstract class Logger{
    private String name;
    private boolean enabled;
    private evel minPermittedLevel;

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

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

//抽象类的子类:输出日志到文件
public class FileLogger extends Logger{
    private Writer fileWriter;

    public FileLogger(String name,boolean enabled.Ievel minPermittedLevel,String filepath)
    {
       super(name,enabled,minPermittedLevel);
       this.fileWriter = new FileWriter(filepath);
    }

    @0verride
    public void log(Level level,string mesage){
        if (!isLoggable())
            return;
        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
    public void log(Level level,string mesage){
        if (!isLoggable())
            return;
        msgQueueClient.send(...);
    }
}

        虽然上面这段代码的设计思路达到了代码复用的目的,但是无法使用多态特性。如果我们像下面这样编写代码,就会出现编译错误,因为Logger类中并没有定义log()方法。

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

        读者可能会说,这个问题的解决很简单,在Logger类中,定义一个空的log()方法,让子类重写 Logger类的log()方法,并实现自己的日志输出逻辑,不就可以了吗?代码如下所示。

Public class Logger{
    //...省略部分代码...
    Public void log(Level level,string mesage){ //方法体为空}
}

public class FileLogger extends Logger{
    //..省略部分代码..
    @Override
    public void log(Level level,String mesage){
        if(!isLoggable())
            return;
        //格式化1evel和message,并输出到日志文件
        filewriter.write(...);
    }
}

public class MessageQueuelogger extends Logger{
   //..省略部分代码..
   @Override
   public void log(Level level, string mesage){
       if(!isLoggable())
           return;
       //格式化1evel和message,并输出到消息中间件
       msgQueueClient.send(...);
    }
}

        虽然上面这段代码的设计思路可用,能够解决问题,但是,它显然没有之前基于抽象*。设计思路优雅,理由如下。

        1)在Logger类中,定义一个空的方法,会影响代码的可读性。如果我们不熟悉Logger类背后的设计思想,加之代码的注释不详细,那么,在阅读Logger类的代码时,有可解生为什么定义一个空的log()方法的疑问。或许,我们需要通过查看Logger、FileLogger和MessageQueueLogger 之间的继承关系,才能明白其背后的设计意图。

        2)当创建一个新的子类并继承Logger类时,我们很有可能忘记重新实现log()方法。前基于抽象类的设计思路,编译器会强制要求子类重写log()方法,否则会报编译错误。读者可能会问,既然要定义一个新的Logger 类的子类,那么怎么会忘记重新实现 log()方法呢?其实,我们举的例子比较简单,Logger类中的方法不多,代码行数也很少。我们可以想象一下如果Logger类中有几百行代码,包含很多方法,除非我们对Logger类的设计非常熟悉,否则极有可能忘记重新实现log()方法。

        3)Logger类可以被实例化,换句话说,我们可以通过关键字new定义一个Logger 类的对象,并且调用它的空的log()方法。这增加了类被误用的风险。当然,这个问题可以通过设置私有的构造函数的方式来解决。不过,这显然没有基于抽象类的实现思路优雅。为什么需要接口?它能够在编程中解决什么问题?抽象类侧重代码复用,而接口侧重解耦。接口是对行为的一种抽象,相当于一组协议或契约,读者可以类比API。调用者只需要关注抽象的接口,不需要了解具体的实现,具体的实对调用者透明。接口实现了约定和实现分离,可以降低代码的耦合度,提高代码的可扩展性。

4.模拟实现抽象类和接口

        有些编程语言只有抽象类,并没有接口,如C++。我们可以通过抽象类模拟接口,只要它满足接口的特性(接口中没有成员变量,只有方法声明,没有实现方法,实现接口的类必须实现接口中的所有方法)即可。在下面这段C++代码中,我们使用抽象类模拟了一个接口。

class Strategy{
    //用抽象类模拟接口
    public:
        virtual ~Strategy();
        virtual void algorithm() = 0,

    protected:
        Strategy();
}

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

        不过,现在流行的动态编程语言,如Python、Ruby等,它们不但没有接口的概念,而且没有抽象类。在这种情况下,我们可以使用普通类模拟接口。具体的Java代码实现如下。

public class MockInterface{
    protected MockInterface();

    public void funcA(){
        throw new MethodUnSupportedException();
    }
}

        我们知道,类中的方法必须包含实现,但这不符合接口的定义。其实,我们可以让类中的方法抛出 MethodUnSupportedException 异常来模拟不包含实现的接口,并且,在子类继承父类时,强迫子类主动实现父类的方法,否则会在运行时抛出异常。那么,如何避免这个类被实例化呢?我们只需要将构造函数设置成protected属性,这样就能避免非同一包(package)下的类去实例化 MockInterface。不过,这样做还是无法避免同一包下的类去实例化 MockInterface.为了解决这个问题,我们可以学习Google Guava中 @VisibleForTesting注解的做法,自定义个注解,人为地表明其不可实例化。

        上面讲了如何用抽象类来模拟接口,以及如何用普通类来模拟接口,那么,如何用普通类来模拟抽象类呢?我们可以类比 MockInterface 类的处理方式,让本该为abstract的方法内部抛出MethodUnSupportedException异常,并且将构造函数设置为protected 属性,避免实例化。

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

        在真实的项目开发中,什么时候该用抽象类?什么时候该用接口?实际上,判断的标准很简单。如果我们要表示一种is-a关系,并且是为了解决代码复用的问题,那么使用抽象类;如果我们要表示一种 has-a关系,并且是为了解决抽象而非代码复用的问题,那么使用接口。

        从类的继承层次上来看,抽象类是一种自下而上的设计思路,先有子类的代码重复,再抽象出上层的父类(也就是抽象类)。而接口正好相反,它是一种自上而下的设计思路。在编程开发时,一般先设计接口,再考虑具体的实现。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/713513.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【计算机毕业设计】240基于微信小程序的校园综合服务平台

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

禁止methtype联网

mathtype断网_如何禁止mathtype联网-CSDN博客https://blog.csdn.net/qq_41060221/article/details/128144783

StarNet实战:使用StarNet实现图像分类任务(二)

文章目录 训练部分导入项目使用的库设置随机因子设置全局参数图像预处理与增强读取数据设置Loss设置模型设置优化器和学习率调整策略设置混合精度&#xff0c;DP多卡&#xff0c;EMA定义训练和验证函数训练函数验证函数调用训练和验证方法 运行以及结果查看测试完整的代码 在上…

微服务开发与实战Day09 - Elasticsearch

一、DSL查询 Elasticsearch提供了DSL&#xff08;Domain Specific Language&#xff09;查询&#xff0c;就是以JSON格式来定义查询条件。类似这样&#xff1a; DSL查询可以分为两大类&#xff1a; 叶子查询&#xff08;Leaf query clauses&#xff09;&#xff1a;一般是在特…

局域网内怎么访问另一台电脑?(2种方法)

案例&#xff1a;需要在局域网内远程电脑 “当我使用笔记本电脑时&#xff0c;有时需要获取保存在台式机上的文件&#xff0c;而两者都连接在同一个局域网上。我的台式机使用的是Windows 10企业版&#xff0c;而笔记本电脑则是Windows 10专业版。我想知道是否可以通过网络远程…

JVM 性能分析——jdk 自带命令分析工具(jps/jstat/jinfo/jmap/jhat/jstack)

文章目录 jps&#xff08;Java Process Status&#xff09;&#xff1a;查看正在运行的Java进程jstat&#xff08;JVM Statistics Monitoring Tool&#xff09;&#xff1a;查看 JVM 的统计信息jinfo&#xff08;Configuration Info for Java&#xff09;&#xff1a;实时查看和…

zip加密txt文件后,暴力破解时会有多个解密密码可以打开的疑问??

最近在做一个关于zip压缩文件解密的测试&#xff0c;发现通过暴力解密时&#xff0c;会有多个解密密码可以打开&#xff0c;非常疑惑&#xff0c;这里做个问题&#xff0c;希望能有大佬解惑。 1、首先在本地创建一个113449.txt的文件&#xff0c;然后右键txt文件选择压缩&…

AI赋能软件测试

AI赋能软件测试 AI赋能软件测试软件测试分类软件质量模型:用来衡量软件质量的维度AI赋能软件测试 随着AI时代的到来,如何轻松掌握软件测试新趋势,将AI技术应用于软件测试行业,提高测试速度与测试效率~~ 传智星云AI助手:https://nebula.itcast.cn tips:各种AI工具应有尽有…

图像处理方向信息

前言 Exif 规范 定义了方向标签&#xff0c;用于指示相机相对于所捕获场景的方向。相机可以使用该标签通过方向传感器自动指示方向&#xff0c;也可以让用户通过菜单开关手动指示方向&#xff0c;而无需实际转换图像数据本身。 在图像处理过程中&#xff0c;若是原图文件包含…

jeecg快速启动(附带本地运行可用版本下载)

版本整理&#xff08;windows x64位&#xff09;&#xff1a; redis&#xff1a;3.0.504 MYSQL&#xff1a;5.7 Maven&#xff1a;3.9.4(setting文件可下载) Nodejs&#xff1a;v16.20.2&#xff08;建议不要安装默认路径下&#xff0c;如已安装在c盘&#xff0c;运行yarn报…

MySQL之优化服务器设置(五)

优化服务器设置 高级InnoDB设置 innodb_old_blocks_time InnoDB有两段缓冲池LRU(最近最少使用)链表&#xff0c;设计目的是防止换出长期很多次的页面。像mysqldump产生的这种一次性的(大)查询&#xff0c;通常会读取页面到缓冲池的LRU列表&#xff0c;从中读取需要的行&…

安装wsl

安装wsl 先决条件&#xff1a; 打开控制面板->选择程序与功能->选择启动或关闭windows功能&#xff0c;将以下框选的勾选上 二、到Mircosoft store下载Ubuntu 三、如果以上都勾选了还报以下错误 注册表错误 0x8007019e Error code: Wsl/CallMsi/REGDB_E_CLASSNOTREG…

机器学习周报第46周

目录 摘要Abstract一、文献阅读1.1 摘要1.2 研究背景1.3 论文方法1.4 模块分析1.5 网络规格1.6 高效的端到端对象检测1.7 mobile former模块代码 目录 摘要Abstract一、文献阅读1.1 摘要1.2 研究背景1.3 论文方法1.4 模块分析1.5 网络规格1.6 高效的端到端对象检测1.7 mobile f…

9. 文本三剑客之awk

文章目录 9.1 什么是awk9.2 awk命令格式9.3 awk执行流程9.4 行与列9.4.1 取行9.4.2 取列 9.1 什么是awk 虽然sed编辑器是非常方便自动修改文本文件的工具&#xff0c;但其也有自身的限制。通常你需要一个用来处理文件中的数据的更高级工具&#xff0c;它能提供一个类编程环境来…

JVM-GC-什么是垃圾

JVM-GC-什么是垃圾 前言 所谓垃圾其实是指&#xff0c;内存中没用的数据&#xff1b;没有任何引用指向这块内存&#xff0c;或者没有任何指针指向这块内存。没有的数据应该被清除&#xff0c;垃圾的处理其实是内存管理问题。 JVM虽然不直接遵循冯诺依曼计算机体系架构&#…

SAP HCM 员工供应商过账详解 财务角度理解员工供应商过账

导读 INTRODUCTION 员工供应商:在某些情况下,特别是在大型组织或集团公司中,员工可能同时扮演着供应商的角色,为组织内部的其他部门或子公司提供产品或服务。例如,一个技术部门的员工可能为销售部门提供技术支持或定制开发服务。,还有一种,就是员工在公司挂账的欠款,每…

SpringBoot如何自定义启动Banner 以及自定义启动项目控制台输出信息 类似于若依启动大佛 制作教程

前言 Spring Boot 项目启动时会在控制台打印出一个 banner&#xff0c;下面演示如何定制这个 banner。 若依也会有相应的启动动画 _ooOoo_o8888888o88" . "88(| -_- |)O\ /O____/---\____. \\| |// ./ \\||| : |||// \/ _||||| -:- |||||- \| | \\…

GraogGNSSLib学习

GraogGNSSLib学习 程序编译环境版本项目编译结果问题 程序编译 GraphGNSSLib 环境版本 程序开源是在ubuntu16.04-kinetic环境跑通的&#xff0c;但是我的环境是UBUNTU20.04&#xff0c;所以&#xff0c;先进行了ROS的安装&#xff0c;因为我的系统是ubuntu20.04所以&#xf…

Hadoop 2.0:主流开源云架构(四)

目录 五、Hadoop 2.0访问接口&#xff08;一&#xff09;访问接口综述&#xff08;二&#xff09;浏览器接口&#xff08;三&#xff09;命令行接口 六、Hadoop 2.0编程接口&#xff08;一&#xff09;HDFS编程&#xff08;二&#xff09;Yarn编程 五、Hadoop 2.0访问接口 &am…

基于WPF技术的换热站智能监控系统13--控制设备开关

1、本节目的 本次工作量相对有点大&#xff0c;有点难度&#xff0c;需要熟悉MVVM模式&#xff0c;特别是属性绑定和命令驱动&#xff0c;目标是点击水泵开关&#xff0c;让风扇转动或停止&#xff0c;风扇连接的管道液体流动或静止。 &#xff0c;具体对应关系是&#xff1a;…