JAVA语法,接口和抽象类应该如何抉择
01.面向对象设计特性1.1 抽象和接口特性在面向对象编程中抽象类和接口是两个经常被用到的语法概念是面向对象四大特性以及很多设计模式、设计思想、设计原则编程实现的基础。比如我们可以使用接口来实现面向对象的抽象特性、多态特性和基于接口而非实现的设计原则使用抽象类来实现面向对象的继承特性和模板设计模式等等。并不是所有的面向对象编程语言都支持这两个语法概念比如C 这种编程语言只支持抽象类不支持接口而像 Python 这样的动态编程语言既不支持抽象类也不支持接口。尽管有些编程语言没有提供现成的语法来支持接口和抽象类我们仍然可以通过一些手段来模拟实现这两个语法概念。1.2 一些问题思考这两个语法概念不仅在工作中经常会被用到在面试中也经常被提及。比如“接口和抽象类的区别是什么什么时候用接口什么时候用抽象类抽象类和接口存在的意义是什么能解决哪些编程问题”等等。1.3 抽象的设计思想抽象思想是指将事物或概念从具体的、特定的细节中抽离出来关注其普遍性、共性和本质特征的思维方式。它是一种对事物进行概括、归纳和提炼的思考方式通过忽略细节和个别差异抓住事物的本质和共同点以更高层次的概念和模型来理解和描述事物。1.4 抽象思想案例02.抽象类和接口2.1 什么是抽象类不同的编程语言对接口和抽象类的定义方式可能有些差别但差别并不会很大。首先来看一下在 Java 这种编程语言中我们是如何定义抽象类的。下面这段代码是一个比较典型的抽象类的使用场景模板设计模式。Logger 是一个记录日志的抽象类FileLogger 和 MessageQueueLogger 继承 Logger分别实现两种不同的日志记录方式记录日志到文件中和记录日志到消息队列中。FileLogger 和 MessageQueueLogger 两个子类复用了父类方法但因为这两个子类写日志的方式不同它们又各自重写了父类中的 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.writer 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 中的 log() 方法也可以不包含代码实现比如 Logger 中的 doLog() 方法。不包含代码实现的方法叫作抽象方法。子类继承抽象类必须实现抽象类中的所有抽象方法。对应到例子代码中就是所有继承 Logger 抽象类的子类都必须重写 doLog() 方法。抽象类提供了一种抽象的概念和接口用于定义一组相关的类的共同行为和属性。它可以作为一种模板或基类为子类提供一致的接口和行为实现代码的复用和多态性。2.2 为何有抽象类2.3 什么是接口再来看一下在 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 { //...限流逻辑... } } // 过滤器使用demo public class Application { // filters.add(new AuthencationFilter()); // filters.add(new RateLimitFilter()); private ListFilter filters new ArrayList(); public void handleRpcRequest(RpcRequest req) { try { for (Filter filter : fitlers) { filter.doFilter(req); } } catch(RpcException e) { // ...处理过滤结果... } // ...省略其他处理逻辑... } }上面这段代码是一个比较典型的接口的使用场景。通过 Java 中的 interface 关键字定义了一个 Filter 接口。AuthenticationFilter 和 RateLimitFilter 是接口的两个实现类分别实现了对 RPC 请求鉴权和限流的过滤功能。代码非常简洁。结合代码再来看一下接口都有哪些特性。接口不能包含属性也就是成员变量。接口只能声明方法方法不能包含代码实现。类实现接口的时候必须实现接口中声明的所有方法。接口是一种抽象的概念用于定义一组方法的契约而不涉及具体的实现。接口定义了类应该具有的方法和行为以提供一种通用的接口供类来实现。2.4 为何设计接口2.5 抽象类和接口区别从语法特性上对比这两者有比较大的区别语法特性的区别比如抽象类中可以定义属性、方法的实现而接口中不能定义属性方法也不能包含代码实现等等。设计角度的区别抽象类实际上就是类只不过是一种特殊的类这种类不能被实例化为对象只能被子类继承。我们知道继承关系是一种 is-a 的关系那抽象类既然属于类也表示一种 is-a 的关系。相对于抽象类的 is-a 关系来说接口表示一种 has-a 关系表示具有某些功能。对于接口有一个更加形象的叫法那就是协议contract。03.解决什么编程问题3.1 抽象类的由来抽象类也是为代码复用而生的。多个子类可以继承抽象类中定义的属性和方法避免在子类中重复编写相同的代码。不过既然继承本身就能达到代码复用的目的而继承也并不要求父类一定是抽象类那我们不使用抽象类照样也可以实现继承和复用。从这个角度上来讲我们貌似并不需要抽象类这种语法呀。那抽象类除了解决代码复用的问题还有什么其他存在的意义吗还是拿之前那个打印日志的例子。我们先对上面的代码做下改造。在改造之后的代码中Logger 不再是抽象类只是一个普通的父类删除了 Logger 中 log()、doLog() 方法新增了 isLoggable() 方法。FileLogger 和 MessageQueueLogger 还是继承 Logger 父类以达到代码复用的目的。具体的代码如下// 父类非抽象类就是普通的类. 删除了log(),doLog()新增了isLoggable(). public class Logger { private String name; private boolean enabled; private Level minPermittedLevel; public Logger(String name, boolean enabled, Level minPermittedLevel) { //...构造函数不变代码省略... } protected boolean isLoggable() { boolean loggable enabled (minPermittedLevel.intValue() level.intValue()); return loggable; } } // 子类输出日志到文件 public class FileLogger extends Logger { private Writer fileWriter; public FileLogger(String name, boolean enabled, Level minPermittedLevel, String filepath) { //...构造函数不变代码省略... } public void log(Level level, String mesage) { if (!isLoggable()) return; // 格式化level和message,输出到日志文件 fileWriter.write(...); } } // 子类: 输出日志到消息中间件(比如kafka) public class MessageQueueLogger extends Logger { private MessageQueueClient msgQueueClient; public MessageQueueLogger(String name, boolean enabled, Level minPermittedLevel, MessageQueueClient msgQueueClient) { //...构造函数不变代码省略... } public void log(Level level, String mesage) { if (!isLoggable()) return; // 格式化level和message,输出到消息中间件 msgQueueClient.send(); } }这个设计思路虽然达到了代码复用的目的但是无法使用多态特性了。像下面这样编写代码就会出现编译错误因为 Logger 中并没有定义 log() 方法。Logger logger new FileLogger(access-log, true, Level.WARN, /users/yc/access.log); logger.log(Level.ERROR, This is a test log message.);你可能会说这个问题解决起来很简单啊。在 Logger 父类中定义一个空的 log() 方法让子类重写父类的 log() 方法实现自己的记录日志的逻辑不就可以了吗public class Logger { // ...省略部分代码... public void log(Level level, String mesage) { // do nothing... } } public class FileLogger extends Logger { // ...省略部分代码... Override public void log(Level level, String mesage) { if (!isLoggable()) return; // 格式化level和message,输出到日志文件 fileWriter.write(...); } } public class MessageQueueLogger extends Logger { // ...省略部分代码... Override public void log(Level level, String mesage) { if (!isLoggable()) return; // 格式化level和message,输出到消息中间件 msgQueueClient.send(...); } }这个设计思路能用但是它显然没有之前通过抽象类的实现思路优雅。为什么这么说呢主要有以下几点原因。在 Logger 中定义一个空的方法会影响代码的可读性。如果我们不熟悉 Logger 背后的设计思想代码注释又不怎么给力我们在阅读 Logger 代码的时候就可能对为什么定义一个空的 log() 方法而感到疑惑需要查看 Logger、FileLogger、MessageQueueLogger 之间的继承关系才能弄明白其设计意图。当创建一个新的子类继承 Logger 父类的时候我们有可能会忘记重新实现 log() 方法。之前基于抽象类的设计思路编译器会强制要求子类重写 log() 方法否则会报编译错误。我们举的例子比较简单Logger 中的方法不多代码行数也很少。但是如果 Logger 有几百行有 n 多方法除非你对 Logger 的设计非常熟悉否则忘记重新实现 log() 方法也不是不可能的。Logger 可以被实例化换句话说我们可以 new 一个 Logger 出来并且调用空的 log() 方法。这也增加了类被误用的风险。当然这个问题可以通过设置私有的构造函数的方式来解决。不过显然没有通过抽象类来的优雅。3.2 接口的由来为什么需要接口它能够解决什么编程问题抽象类更多的是为了代码复用而接口就更侧重于解耦。接口是对行为的一种抽象相当于一组协议或者契约调用者只需要关注抽象的接口不需要了解具体的实现具体的实现代码对调用者透明。接口实现了约定和实现相分离可以降低代码间的耦合性提高代码的可扩展性。实际上接口是一个比抽象类应用更加广泛、更加重要的知识点。比如经常提到的“基于接口而非实现编程”就是一条几乎天天会用到并且能极大地提高代码的灵活性、扩展性的设计思想。04.模拟抽象类和接口4.1 模拟接口设计如果你熟悉的是 C 这种编程语言你可能会说C 只有抽象类并没有接口那从代码实现的角度上来说是不是就无法实现 接口 的设计思路了呢先来回忆一下接口的定义接口中没有成员变量只有方法声明没有方法实现实现接口的类必须实现接口中的所有方法。只要满足这样几点从设计的角度上来说我们就可以把它叫作接口。实际上要满足接口的这些语法特性并不难。在下面这段 C 代码中就用抽象类模拟了一个接口下面这段代码实际上是策略模式中的一段代码。class Strategy { // 用抽象类模拟接口 public: ~Strategy(); virtual void algorithm()0; protected: Strategy(); };抽象类 Strategy 没有定义任何属性并且所有的方法都声明为 virtual 类型等同于 Java 中的 abstract 关键字。这样所有的方法都不能有代码实现并且所有继承这个抽象类的子类都要实现这些方法。从语法特性上来看这个抽象类就相当于一个接口。4.2 模拟抽象类在 Python、Ruby 这些动态语言中不仅没有接口的概念也没有类似 abstract、virtual 这样的关键字来定义抽象类那该如何实现上面的讲到的 抽象类 的设计思路呢实际上除了用抽象类来模拟接口之外还可以用普通类来模拟接口。具体的 Java 代码实现如下所示。public class MockInteface { protected MockInteface() {} public void funcA() { throw new MethodUnSupportedException(); } }类中的方法必须包含实现这个不符合接口的定义。但是我们可以让类中的方法抛出 MethodUnSupportedException 异常来模拟不包含实现的接口并且能强迫子类在继承这个父类的时候都去主动实现父类的方法否则就会在运行时抛出异常。那又如何避免这个类被实例化呢实际上很简单我们只需要将这个类的构造函数声明为 protected 访问权限就可以了。05.如何选择场景5.1 判断标准很简单实际上判断的标准很简单。如果我们要表示一种 is-a 的关系并且是为了解决代码复用的问题就用抽象类如果我们要表示一种 has-a 关系并且是为了解决抽象而非代码复用的问题那就可以使用接口。从类的继承层次上来看抽象类是一种自下而上的设计思路先有子类的代码重复然后再抽象成上层的父类也就是抽象类。而接口正好相反它是一种自上而下的设计思路。我们在编程的时候一般都是先设计接口再去考虑具体的实现。5.2 场景上的区别抽象类和接口在设计上有一些区别尽管它们都是面向对象编程中的抽象概念。以下是它们之间的一些主要区别实现方式抽象类通过继承的方式被子类实现而接口通过实现的方式被类实现。一个类只能继承一个抽象类但可以实现多个接口。方法实现抽象类可以包含具体方法的实现而接口只能包含方法的声明没有具体的实现。类继承抽象类时可以直接继承具体方法的实现而实现接口时必须提供方法的具体实现。关注点抽象类更适合用于描述一种 是什么 的关系即类与类之间的继承关系。接口更适合用于描述一种 能做什么 的关系即类具有哪些方法和行为。灵活性抽象类可以包含实例变量而接口只能包含常量。抽象类可以提供一些默认的实现而接口只能定义方法的契约没有默认实现。使用场景抽象类通常用于描述一组相关的类提供一种通用的基类而接口通常用于定义一组方法的契约用于实现多态性和解耦合。5.3 一些具体的例子那么在实际开发中应该如何选择抽象类和接口的案例如果需要提供一组相关类的通用行为和属性可以使用抽象类。比如Android中要抽象出公共的BaseActivity可以做到子类复用如果需要定义一组方法的契约以实现多态性和解耦合可以使用接口。比如Android中要定义MVP的View和Presenter的交互接口则要用接口在某些情况下抽象类和接口可以结合使用以满足更复杂的设计需求。5.4 开发总结一下抽象类和接口的语法特性抽象类不允许被实例化只能被继承。它可以包含属性和方法。方法既可以包含代码实现也可以不包含代码实现。不包含代码实现的方法叫作抽象方法。子类继承抽象类必须实现抽象类中的所有抽象方法。接口不能包含属性只能声明方法方法不能包含代码实现。类实现接口的时候必须实现接口中声明的所有方法。抽象类和接口存在的意义抽象类是对成员变量和方法的抽象是一种 is-a 关系是为了解决代码复用问题。接口仅仅是对方法的抽象是一种 has-a 关系表示具有某一组行为特性是为了解决解耦问题隔离接口和具体的实现提高代码的扩展性。抽象类和接口的应用场景区别什么时候该用抽象类showWEIBO.COM/ttarticle/p/show?id2309405283370409525530什么时候该用接口实际上判断的标准很简单。如果要表示一种 is-a 的关系并且是为了解决代码复用问题我们就用抽象类如果要表示一种并且是为了解决抽象而非代码复用问题那我们就用接口。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2477194.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!