简单聊聊java接口vs抽象类比较

张开发
2026/4/3 15:18:43 15 分钟阅读
简单聊聊java接口vs抽象类比较
03.接口vs抽象类比较01.面向对象设计特性1.1 抽象和接口特性1.2 一些问题思考1.3 抽象的设计思想1.4 抽象思想案例02.抽象类介绍2.1 看抽象案例2.2 抽象类特点2.3 抽象类设计注意要点2.4 抽象的思想2.5 模拟抽象类03.接口设计介绍3.1 看接口案例3.2 接口的特点3.3 接口设计注意点3.4 接口的思想3.5 Marker Interface3.6 模拟接口设计04.解决什么编程问题4.1 抽象类的由来4.2 接口的由来05.抽象类VS接口5.1 理解抽象和接口5.2 语法上不同5.3 编程角度不同5.4 通俗理解两者区别5.5 设计层次上区别06.如何选择场景6.1 判断标准很简单6.2 场景上的区别6.3 一些具体的例子6.4 开发总结一下01.面向对象设计特性1.1 抽象和接口特性在面向对象编程中抽象类和接口是两个经常被用到的语法概念是面向对象四大特性以及很多设计模式、设计思想、设计原则编程实现的基础。比如我们可以使用接口来实现面向对象的抽象特性、多态特性和基于接口而非实现的设计原则使用抽象类来实现面向对象的继承特性和模板设计模式等等。并不是所有的面向对象编程语言都支持这两个语法概念比如C 这种编程语言只支持抽象类不支持接口而像 Python 这样的动态编程语言既不支持抽象类也不支持接口。尽管有些编程语言没有提供现成的语法来支持接口和抽象类我们仍然可以通过一些手段来模拟实现这两个语法概念。1.2 一些问题思考这两个语法概念不仅在工作中经常会被用到在面试中也经常被提及。比如“接口和抽象类的区别是什么什么时候用接口什么时候用抽象类抽象类和接口存在的意义是什么能解决哪些编程问题”等等。1.3 抽象的设计思想抽象思想是指将事物或概念从具体的、特定的细节中抽离出来关注其普遍性、共性和本质特征的思维方式。它是一种对事物进行概括、归纳和提炼的思考方式通过忽略细节和个别差异抓住事物的本质和共同点以更高层次的概念和模型来理解和描述事物。1.4 抽象思想案例代码如下所示。抽象类除了有抽象类特性之外还可以解决代码复用问题。/*抽象类作为参数的时候如何进行调用*/ abstract class Animal { protected int x; private int y; // 定义一个抽象方法 public abstract void eat() ; public void func2() { System.out.println(func2); } } // 定义一个类猫 class Cat extends Animal { public void eat(){ System.out.println(吃鱼.................) ; } } // 定义一个类狗 class Dog extends Animal { public void eat(){ System.out.println(吃骨头.................) ; } } // 定义一个类动物类 class AnimalDemo { public void method(Animal a) { a.eat() ; } } // 测试类 class ArgsDemo2 { public static void main(String[] args) { // 创建AnimalDemo的对象 AnimalDemo ad new AnimalDemo() ; // 对Animal进行间接实例化 // Animal a new Cat() ; // Animal a new Dog() ; Cat a new Cat() ; // 调用method方法 ad.method(a) ; } }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(...); } }2.2 抽象类特点抽象类是一种在面向对象编程中的概念它是不能被实例化的类只能被继承。抽象类用于定义一组抽象方法和可能的具体方法以提供一种通用的接口和行为供其子类实现和继承。通过上面的这个例子来看一下抽象类具有哪些特性。抽象类不允许被实例化只能被继承。也就是说你不能 new 一个抽象类的对象出来Logger logger new Logger(…); 会报编译错误。抽象类可以包含属性和方法。方法既可以包含代码实现比如 Logger 中的 log() 方法也可以不包含代码实现比如 Logger 中的 doLog() 方法。不包含代码实现的方法叫作抽象方法。子类继承抽象类必须实现抽象类中的所有抽象方法。对应到例子代码中就是所有继承 Logger 抽象类的子类都必须重写 doLog() 方法。抽象类提供了一种抽象的概念和接口用于定义一组相关的类的共同行为和属性。它可以作为一种模板或基类为子类提供一致的接口和行为实现代码的复用和多态性。2.3 抽象类设计注意要点如果想要设计这样一个类该类包含一个特别的成员方法方法的具体实现由它的子类确定那么可以在父类中声明该方法为抽象方法Abstract关键字同样可以用来声明抽象方法抽象方法只包含一个方法名而没有方法体。声明抽象方法会造成以下两个结果如果一个类包含抽象方法则该类必须声明为抽象类子类必须重写父类的抽象方法否则自身也必须声明为抽象类2.4 抽象的思想抽象特性的定义讲完了我们再来看一下抽象的意义是什么它能解决什么编程问题实际上如果上升一个思考层面的话抽象及其前面讲到的封装都是人类处理复杂性的有效手段。在面对复杂系统的时候人脑能承受的信息复杂程度是有限的所以我们必须忽略掉一些非关键性的实现细节。抽象作为一种只关注功能点不关注实现的设计思路正好帮我们的大脑过滤掉许多非必要的信息。抽象作为一个非常宽泛的设计思想很多设计原则都体现了抽象这种设计思想比如基于接口而非实现编程、开闭原则对扩展开放、对修改关闭、代码解耦降低代码的耦合性等。举个简单例子比如 getAliPictureUrl() 就不是一个具有抽象思维的命名因为某一天如果我们不再把图片存储在阿里云上而是存储在私有云上那这个命名也要随之被修改。相反如果我们定义一个比较抽象的函数比如叫作 getPictureUrl()那即便内部存储方式修改了我们也不需要修改命名。2.5 模拟抽象类在 Python、Ruby 这些动态语言中不仅没有接口的概念也没有类似 abstract、virtual 这样的关键字来定义抽象类那该如何实现上面的讲到的 抽象类 的设计思路呢实际上除了用抽象类来模拟接口之外还可以用普通类来模拟接口。具体的 Java 代码实现如下所示。public class MockInteface { protected MockInteface() {} public void funcA() { throw new MethodUnSupportedException(); } }类中的方法必须包含实现这个不符合接口的定义。但是我们可以让类中的方法抛出 MethodUnSupportedException 异常来模拟不包含实现的接口并且能强迫子类在继承这个父类的时候都去主动实现父类的方法否则就会在运行时抛出异常。那又如何避免这个类被实例化呢实际上很简单我们只需要将这个类的构造函数声明为 protected 访问权限就可以了。03.接口设计介绍3.1 看接口案例再来看一下在 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 请求鉴权和限流的过滤功能。3.2 接口的特点代码非常简洁。结合代码再来看一下接口都有哪些特性。接口不能包含属性也就是成员变量。接口只能声明方法方法不能包含代码实现。类实现接口的时候必须实现接口中声明的所有方法。接口是一种抽象的概念用于定义一组方法的契约而不涉及具体的实现。接口定义了类应该具有的方法和行为以提供一种通用的接口供类来实现。3.3 接口设计注意点3.4 接口的思想3.5 Marker Interface接口的职责也不仅仅限于抽象方法的集合其实有各种不同的实践。有一类没有任何方法的接口通常叫作 Marker Interface顾名思义它的目的就是为了声明某些东西比如我们熟知的 Cloneable、Serializable 等。这种用法也存在于业界其他的 Java 产品代码中。3.6 模拟接口设计如果你熟悉的是 C 这种编程语言你可能会说C 只有抽象类并没有接口那从代码实现的角度上来说是不是就无法实现 接口 的设计思路了呢先来回忆一下接口的定义接口中没有成员变量只有方法声明没有方法实现实现接口的类必须实现接口中的所有方法。只要满足这样几点从设计的角度上来说我们就可以把它叫作接口。实际上要满足接口的这些语法特性并不难。在下面这段 C 代码中就用抽象类模拟了一个接口下面这段代码实际上是策略模式中的一段代码。class Strategy { // 用抽象类模拟接口 public: ~Strategy(); virtual void algorithm()0; protected: Strategy(); };抽象类 Strategy 没有定义任何属性并且所有的方法都声明为 virtual 类型等同于 Java 中的 abstract 关键字。这样所有的方法都不能有代码实现并且所有继承这个抽象类的子类都要实现这些方法。从语法特性上来看这个抽象类就相当于一个接口。04.解决什么编程问题4.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() 方法。这也增加了类被误用的风险。当然这个问题可以通过设置私有的构造函数的方式来解决。不过显然没有通过抽象类来的优雅。4.2 接口的由来为什么需要接口它能够解决什么编程问题抽象类更多的是为了代码复用而接口就更侧重于解耦。接口是对行为的一种抽象相当于一组协议或者契约调用者只需要关注抽象的接口不需要了解具体的实现具体的实现代码对调用者透明。接口实现了约定和实现相分离可以降低代码间的耦合性提高代码的可扩展性。实际上接口是一个比抽象类应用更加广泛、更加重要的知识点。比如经常提到的“基于接口而非实现编程”就是一条几乎天天会用到并且能极大地提高代码的灵活性、扩展性的设计思想。05.抽象类VS接口5.1 理解抽象和接口这两个语法概念不仅在工作中经常会被用到在面试中也经常被提及。比如“接口和抽象类的区别是什么什么时候用接口什么时候用抽象类抽象类和接口存在的意义是什么能解决哪些编程问题”等等。abstract class和interface之间在对于抽象类定义的支持方面具有很大的相似性甚至可以相互替换避免使用时在进行抽象类定义时对于 abstract class和interface的选择随意。其实两者之间还是有很大的区别的对于它们的选择甚至反映出对于问题领域本质的理解、对于设计意图的理解是否正确、合理。本文将对它们之间的区别进行一番剖析试图给开发者提供一个在二者之间进行选择的依据。5.2 语法上不同抽象类abstract class Student abstract void method1(); abstract void method2(); public void method3() { System.out.println(func2); } 接口interface Student { //接口中的变量其实就是常量默认被final修饰 int age 10; void method1(); void method2(); }在abstract class方式中Demo可以有自己的数据成员也可以有非abstract的成员方法而在interface方式的实现中Demo只能够有静态的不能被修改的数据成员也就是必须是static final的不过在interface中一般不定义数据成员所有的成员方法都是abstract的。从某种意义上说interface是一种特殊形式的abstract class。抽象类实际上就是类只不过是一种特殊的类这种类不能被实例化为对象只能被子类继承。我们知道继承关系是一种 is-a 的关系那抽象类既然属于类也表示一种 is-a 的关系。相对于抽象类的 is-a 关系来说接口表示一种 has-a 关系表示具有某些功能。对于接口有一个更加形象的叫法那就是协议contract。两者语法上的区别抽象类方式中抽象类可以拥有任意范围的成员数据同时也可以拥有自己的非抽象方法但是接口方式中它仅能够有静态、不能修改的成员数据但是我们一般是不会在接口中使用成员数据同时它所有的方法都必须是抽象的。在某种程度上来说接口是抽象类的特殊化。对子类而言它只能继承一个抽象类这是java为了数据安全而考虑的但是却可以实现多个接口。5.3 编程角度不同abstract class在Java语言中表示的是一种继承关系一个类只能使用一次继承关系。但是一个类却可以实现多个interface。也许这是Java语言的设计者在考虑Java对于多重继承的支持方面的一种折中考虑吧。其次在abstract class的定义中我们可以赋予方法的默认行为。但是在interface的定义中方法却不能拥有默认行为不过在JDK1.8中可以使用default关键字实现默认方法。interface InterfaceA { default void foo() { System.out.println(InterfaceA foo); } }在 Java 8 之前接口与其实现类之间的耦合度太高了tightly coupled当需要为一个接口添加方法时所有的实现类都必须随之修改。默认方法解决了这个问题它可以为接口添加新的方法而不会破坏已有的接口的实现。这在 lambda 表达式作为Java 8 语言的重要特性而出现之际为升级旧接口且保持向后兼容backward compatibility提供了途径。5.4 通俗理解两者区别接口和抽象类的概念不一样。接口是对动作的抽象抽象类是对根源的抽象。从设计理念上接口反映的是“like-a”关系抽象类反映的是“is-a”关系。抽象类表示的是这个对象是什么。接口表示的是这个对象能做什么。比如男人女人这两个类如果是类的话……他们的抽象类是人。说明他们都是人。人可以吃东西狗也可以吃东西你可以把“吃东西”定义成一个接口然后让这些类去实现它.所以在高级语言上一个类只能继承一个类抽象类(正如人不可能同时是生物和非生物)但是可以实现多个接口(吃饭接口、走路接口)。5.5 设计层次上区别抽象层次不同抽象类是对类抽象而接口是对行为的抽象。抽象类是对整个类整体进行抽象包括属性、行为但是接口却是对类局部行为进行抽象。跨域不同抽象类所跨域的是具有相似特点的类而接口却可以跨域不同的类。我们知道抽象类是从子类中发现公共部分然后泛化成抽象类子类继承该父类即可但是接口不同。实现它的子类可以不存在任何关系共同之处。例如猫、狗可以抽象成一个动物类抽象类具备叫的方法。鸟、飞机可以实现飞Fly接口具备飞的行为这里我们总不能将鸟、飞机共用一个父类吧所以说抽象类所体现的是一种继承关系要想使得继承关系合理父类和派生类之间必须存在is-a 关系即父类和派生类在概念本质上应该是相同的。对于接口则不然并不要求接口的实现者和接口定义在概念本质上是一致的 仅仅是实现了接口定义的契约而已。设计层次不同对于抽象类而言它是自下而上来设计的我们要先知道子类才能抽象出父类而接口则不同它根本就不需要知道子类的存在只需要定义一个规则即可至于什么子类、什么时候怎么实现它一概不知。比如我们只有一个猫类在这里如果你这是就抽象成一个动物类是不是设计有点儿过度我们起码要有两个动物类猫、狗在这里我们在抽象他们的共同点形成动物抽象类吧所以说抽象类往往都是通过重构而来的但是接口就不同比如说飞我们根本就不知道会有什么东西来实现这个飞接口怎么实现也不得而知我们要做的就是事前定义好飞的行为接口。所以说抽象类是自底向上抽象而来的接口是自顶向下设计出来的。06.如何选择场景6.1 判断标准很简单实际上判断的标准很简单。如果我们要表示一种 is-a 的关系并且是为了解决代码复用的问题就用抽象类如果我们要表示一种 has-a 关系并且是为了解决抽象而非代码复用的问题那就可以使用接口。从类的继承层次上来看抽象类是一种自下而上的设计思路先有子类的代码重复然后再抽象成上层的父类也就是抽象类。而接口正好相反它是一种自上而下的设计思路。我们在编程的时候一般都是先设计接口再去考虑具体的实现。6.2 场景上的区别抽象类和接口在设计上有一些区别尽管它们都是面向对象编程中的抽象概念。以下是它们之间的一些主要区别实现方式抽象类通过继承的方式被子类实现而接口通过实现的方式被类实现。一个类只能继承一个抽象类但可以实现多个接口。方法实现抽象类可以包含具体方法的实现而接口只能包含方法的声明没有具体的实现。类继承抽象类时可以直接继承具体方法的实现而实现接口时必须提供方法的具体实现。关注点抽象类更适合用于描述一种 是什么 的关系即类与类之间的继承关系。接口更适合用于描述一种 能做什么 的关系即类具有哪些方法和行为。灵活性抽象类可以包含实例变量而接口只能包含常量。抽象类可以提供一些默认的实现而接口只能定义方法的契约没有默认实现。使用场景抽象类通常用于描述一组相关的类提供一种通用的基类而接口通常用于定义一组方法的契约用于实现多态性和解耦合。6.3 一些具体的例子那么在实际开发中应该如何选择抽象类和接口的案例如果需要提供一组相关类的通用行为和属性可以使用抽象类。比如Android中要抽象出公共的BaseActivity可以做到子类复用如果需要定义一组方法的契约以实现多态性和解耦合可以使用接口。比如Android中要定义MVP的View和Presenter的交互接口则要用接口在某些情况下抽象类和接口可以结合使用以满足更复杂的设计需求。6.4 开发总结一下抽象类和接口的语法特性抽象类不允许被实例化只能被继承。它可以包含属性和方法。方法既可以包含代码实现也可以不包含代码实现。不包含代码实现的方法叫作抽象方法。子类继承抽象类必须实现抽象类中的所有抽象方法。接口不能包含属性只能声明方法方法不能包含代码实现。类实现接口的时候必须实现接口中声明的所有方法。抽象类和接口存在的意义抽象类是对成员变量和方法的抽象是一种 is-a 关系是为了解决代码复用问题。接口仅仅是对方法的抽象是一种 has-a 关系表示具有某一组行为特性是为了解决解耦问题隔离接口和具体的实现提高代码的扩展性。抽象类和接口的应用场景区别什么时候该用抽象类什么时候该用接口实际上判断的标准很简单。如果要表示一种 is-a 的关系并且是为了解决代码复用问题我们就用抽象类如果要表示一种 has-a 关系并且是为了解决抽象而非代码复用问题那我们就用接口。从语法特性上对比这两者有比较大的区别语法特性的区别比如抽象类中可以定义属性、方法的实现而接口中不能定义属性方法也不能包含代码实现等等。设计角度的区别WEIBO.COM/ttarticle/p/show?id2309405283342345437220抽象类实际上就是类只不过是一种特殊的类这种类不能被实例化为对象只能被子类继承。我们知道继承关系是一种的关系那抽象类既然属于类也表示一种的关系。相对于抽象类的关系来说接口表示一种关系表示具有某些功能。对于接口有一个更加形象的叫法那就是协议。

更多文章