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

目录

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);}@0verridepublic 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;}@Overrideprotected 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 {@Overridepublic void doFilter(RpcRequest req)throws RpcException {//省略鉴权逻辑...}
}//接口实现类:限流过滤器
public class RatelimitFilter implements Filter {@Overridepublic 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);}@0verridepublic 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;}@Overridepublic 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{//..省略部分代码..@Overridepublic void log(Level level,String mesage){if(!isLoggable())return;//格式化1evel和message,并输出到日志文件filewriter.write(...);}
}public class MessageQueuelogger extends Logger{//..省略部分代码..@Overridepublic 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.mzph.cn/news/853822.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

借报告Team ID错误谈谈Mac app文件签名与公证

文章目录 目的起因流程熟悉本地证书、认证证书申请在钥匙串中创建要公证app的profile(公证的时候会用到)程序打包后App文件进行app签名压缩打包公证公证变化在WWDC19, 苹果在MacOS 10.14之后引入了公证(Notarization)这一机制来提升安全性. 主要分以下几步新的公证workflow公…

使用KVM制作镜像

资源列表 操作系统 IP Centos7&#xff0c;桌面版 192.168.10.57 安装KVM 安装软件包 yum -y install qemu-kvm qemu-kvm-tools qemu-img bridge-utils libvirt virt-install virt-manager 检查有否支持虚拟化 grep -e vmx -e svm /proc/cpuinfo #VMX是英特尔版本&…

【CT】LeetCode手撕—88. 合并两个有序数组

目录 题目1- 思路2- 实现⭐88. 合并两个有序数组——题解思路 2- ACM实现 题目 原题连接&#xff1a;88. 合并两个有序数组 1- 思路 模式识别 模式1&#xff1a;两个有序数组合并 ——> 双指针模式2&#xff1a;返回结果填充到 nums1[mn] ——> 需要开辟新的数组空间 …

lxml库在爬虫领域的贡献及应用

重头戏lxml库里面的xpath 一段代码给各位开开胃 这段代码首先导入了lxml库中的etree模块&#xff0c;然后定义了一个包含HTML内容的字符串html。接着&#xff0c;我们使用etree.HTML()函数解析这个HTML字符串&#xff0c;得到一个表示整个HTML文档的树形结构。最后&#xff0c;…

C# 字段 属性 索引器

字段声明字段字段初始化静态字段常量字段只读字段字段的访问然而 属性声明属性自动实现的属性只读属性只写属性属性的逻辑处理属性的访问修饰符属性和字段的区别属性的用途总结 索引器索引器的基本语法使用索引器索引器的关键点语法参数访问和设置异常处理性能重载使用场景封装…

qt 实现模拟实际物体带速度的移动(水平、垂直、斜角度)——————附带完整代码

文章目录 0 效果1 原理1.1 图片旋转1.2 物体带速度移动 2 完整实现2.1 将车辆按钮封装为一个类&#xff1a;2.2 调用方法 3 完整代码参考 0 效果 实现后的效果如下 可以显示属性&#xff08;继承自QToolButton&#xff09;: 鼠标悬浮显示文字 按钮显示文字 1 原理 类继承…

Folx软件安装教程及最新版下载

简介&#xff1a; Folx Pro是一款适合Mac的专业下载工具也是一款BT下载器&#xff0c;Folx中文版有一个支持Retina显示的现代界面&#xff0c;提供独特的系统排序、存储下载内容与预览下载文件。Folx中文官网提供Folx教程、激活码、下载。 安 装 包 获 取 地 址&#xff1a; …

远程连接管理服务SHH

1.了解服务端和客户端 服务端&#xff1a;提供服务 客户端&#xff1a;享受服务 思考: 我们通过网络是如何找到我们想要访问的服务的? IP(提供服务的服务器)Port(找到相应的服务) 2.了解端口号的设定 说明:端口号只有整数&#xff0c;范围是从0 到65535 1&#xff5e;2…

使用RANSAC来拟合直线

RANSAC是“RANdom SAmple Consensus”的缩写&#xff0c;是一种迭代方法&#xff0c;用于数据中估计统计参数或几何模型的算法。它通过给定数据集中随机选择样本并使用样本计算模型&#xff0c;然后测试模型的可能性来工作。如果一个模型通过了足够数量的测试&#xff0c;则认为…

warning LNK4017: DESCRIPTION 语句不支持目标平台;已忽略

文章目录 warning LNK4017: DESCRIPTION 语句不支持目标平台&#xff1b;已忽略概述笔记备注END warning LNK4017: DESCRIPTION 语句不支持目标平台&#xff1b;已忽略 概述 基于ATL的COM DLL导出函数&#xff0c;无法用__declspec(dllexport)直接在函数上标记为导出函数。 只…

【Python】torch_scatter安装报错

torch_cluster&#xff0c;torch_scatter&#xff0c;torch_sparse则在成功安装torch后&#xff0c;使用指令 pip install --no-index torch_scatter -f https://pytorch-geometric.com/whl/torch-1.x.0cu10x.html&#xff08;torch_cluster可直接修改为另外两样&#xff09;进…

Scala网络编程:代理设置与Curl库应用实例

在网络编程的世界里&#xff0c;Scala以其强大的并发模型和函数式编程特性&#xff0c;成为了开发者的得力助手。然而&#xff0c;网络请求往往需要通过代理服务器进行&#xff0c;以满足企业安全策略或访问控制的需求。本文将深入探讨如何在Scala中使用Curl库进行网络编程&…

MySQL第三方图形化工具:DBeaver

操纵数据库的语言&#xff0c;基于功能划分为4类&#xff1a; 数据定义:DDL(Data Definition Language)库的创建删除、表的创建删除等 数据操纵:DML(Data ManipulationLanguage)新增数据、删除数据、修改数据等 数据控制:DCL(Data ControlLanguage)新增用户、删除用户、密码…

DAY24 回溯算法part01 77. 组合 216.组合总和III 17.电话号码的字母组合

理论基础 #什么是回溯法 回溯法也可以叫做回溯搜索法&#xff0c;它是一种搜索的方式。 在二叉树系列中&#xff0c;我们已经不止一次&#xff0c;提到了回溯&#xff0c;例如二叉树&#xff1a;以为使用了递归&#xff0c;其实还隐藏着回溯 (opens new window)。 回溯是递…

【靶场搭建】-02- 搭建OWASP靶机

1.OWASP靶机介绍 相比较其他靶机&#xff0c;OWASP提供的环境更多&#xff0c;且包含了许多其他靶机的环境&#xff0c;属于性价比比较高的靶机了。 2.下载OWASP 访问以下地址进行下载&#xff1a; https://sourceforge.net/projects/owaspbwa/ 因为OWASP是虚拟机文件&…

玩具机器人脚本适合场景

玩具机器人脚本作为一个模拟的玩具机器人脚本&#xff0c;适合以下场合&#xff1a; 1.教育和学习&#xff1a;对于初学者和编程爱好者来说&#xff0c;这个脚本是一个很好的学习工具&#xff0c;可以帮助他们理解如何编写和执行简单的控制逻辑。 2.在计算机科学、机器人技术或…

函数(上)(C语言)

函数(上&#xff09; 一. 函数的概念二. 函数的使用1. 库函数和自定义函数(1) 库函数(2) 自定义函数的形式 2. 形参和实参3. return语句4. 数组做函数参数 一. 函数的概念 数学中我们其实就见过函数的概念&#xff0c;比如&#xff1a;一次函数ykxb&#xff0c;k和b都是常数&a…

跻身中国市场前三,联想服务器的“智变”与“质变”

IDC发布的《2024年第一季度中国x86服务器市场报告》显示&#xff0c;联想服务销售额同比增长200.2%&#xff0c;在前十厂商中同比增速第一&#xff0c;并跻身中国市场前三&#xff0c;迈入算力基础设施“第一阵营”。 十年砺剑联想梦&#xff0c;三甲登榜领风骚。探究联想服务器…

【机器学习】机器学习重要分支——强化学习:从理论到实践

文章目录 强化学习&#xff1a;从理论到实践引言第一章 强化学习的基本概念1.1 什么是强化学习1.2 强化学习的基本组成部分1.3 马尔可夫决策过程 第二章 强化学习的核心算法2.1 Q学习2.2 深度Q网络&#xff08;DQN&#xff09;2.3 策略梯度方法 第三章 强化学习的应用实例3.1 游…

【C语言】解决C语言报错:Format String Vulnerability

文章目录 简介什么是Format String VulnerabilityFormat String Vulnerability的常见原因如何检测和调试Format String Vulnerability解决Format String Vulnerability的最佳实践详细实例解析示例1&#xff1a;直接使用不受信任的输入作为格式化字符串示例2&#xff1a;未验证格…