2.2抽象类和接口设计
目录介绍
- 01.抽象类详细介绍
- 1.1 为何需要抽象类
- 1.2 看一个抽象类案例
- 1.3 抽象类特点
- 1.4 抽象类设计注意要点
- 1.5 抽象类含义
- 02.接口详细介绍
- 2.1 为何需要接口
- 2.2 看一个接口案例
- 2.3 接口特点
- 2.4 Marker Interface
- 03.抽象类和接口
- 3.1 理解抽象和接口概念
- 3.2 抽象和接口语法上不同
- 3.3 编程角度不同
- 3.4 通俗理解两者区别
- 3.5 解决什么编程问题
- 3.6 设计层次上区别
- 04.两者如何选择
- 4.1 如何选择
- 4.2 抽象类重点是复用
- 4.3 接口重点是解决耦合
- 4.4 为何说基于接口开发
- 05.简单总结一下
01.抽象类详细介绍
1.1 为何需要抽象类
- 什么是抽象
- 封装主要讲的是如何隐藏信息、保护数据,而抽象讲的是如何隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的。
- 在面向对象编程中,我们常借助编程语言提供的接口类(比如 Java 中的 interface 关键字语法)或者抽象类(比如 Java 中的 abstract 关键字语法)这两种语法机制,来实现抽象这一特性。
- 把编程语言提供的接口语法叫作“接口类”而不是“接口”。之所以这么做,是因为“接口”这个词太泛化,可以指好多概念,比如 API 接口等,所以,我们用“接口类”特指编程语言提供的接口语法。
1.2 看一个抽象类案例
- 代码如下所示。抽象类除了有抽象类特性之外,还可以解决代码复用问题。
/*抽象类作为参数的时候如何进行调用*/ 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) ; } }
1.3 抽象类特点
- 抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。
- 抽象类和抽象方法都使用 abstract 关键字进行声明。抽象类一般会包含抽象方法,抽象方法一定位于抽象类中。
- 抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类。其目的主要是代码重用。
- 抽象类大多用于抽取相关 Java 类的共用方法实现或者是共同成员变量,然后通过继承的方式达到代码复用的目的。
- 注意抽象类是不能被实例化的,也就是不能new出来的!
- 如果执意需要new,则会提示。xxx is abstract ; cannot be instantiated
1.4 抽象类设计注意要点
- 如果想要设计这样一个类,该类包含一个特别的成员方法,方法的具体实现由它的子类确定,那么可以在父类中声明该方法为抽象方法
- Abstract关键字同样可以用来声明抽象方法,抽象方法只包含一个方法名,而没有方法体。声明抽象方法会造成以下两个结果:
- 如果一个类包含抽象方法,则该类必须声明为抽象类
- 子类必须重写父类的抽象方法,否则自身也必须声明为抽象类
1.5 抽象类含义
- 抽象特性的定义讲完了,我们再来看一下,抽象的意义是什么?它能解决什么编程问题?
- 实际上,如果上升一个思考层面的话,抽象及其前面讲到的封装都是人类处理复杂性的有效手段。在面对复杂系统的时候,人脑能承受的信息复杂程度是有限的,所以我们必须忽略掉一些非关键性的实现细节。而抽象作为一种只关注功能点不关注实现的设计思路,正好帮我们的大脑过滤掉许多非必要的信息。
- 除此之外,抽象作为一个非常宽泛的设计思想,在代码设计中,起到非常重要的指导作用。很多设计原则都体现了抽象这种设计思想,比如基于接口而非实现编程、开闭原则(对扩展开放、对修改关闭)、代码解耦(降低代码的耦合性)等。
- 换一个角度来考虑,我们在定义(或者叫命名)类的方法的时候,也要有抽象思维,不要在方法定义中,暴露太多的实现细节,以保证在某个时间点需要改变方法的实现逻辑的时候,不用去修改其定义。
- 举个简单例子,比如 getAliPictureUrl() 就不是一个具有抽象思维的命名,因为某一天如果我们不再把图片存储在阿里云上,而是存储在私有云上,那这个命名也要随之被修改。相反,如果我们定义一个比较抽象的函数,比如叫作 getPictureUrl(),那即便内部存储方式修改了,我们也不需要修改命名。
02.接口详细介绍
2.1 为何需要接口
- 什么是接口
- 接口(Interface)在Java语言中是一个抽象类型,是服务提供者和服务使用者之间的一个协议,在JDK1.8之前一直是抽象方法的集合,一个类通过实现接口从而来实现两者间的协议
- 接口可以定义字段和方法。在JDK1.8之前,接口中所有的方法都是抽象的,从JDK1.8开始,也可以在接口中编写默认的和静态的方法。除非显式指定,否则接口方法都是抽象的
2.2 看一个接口案例
- 在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类。从 Java 8 开始,interface 增加了对 default method 的支持。Java 9 以后,甚至可以定义 private default method。Default method 提供了一种二进制兼容的扩展已有接口的办法。比如,我们熟知的 java.util.Collection,它是 collection 体系的 root interface,在 Java 8 中添加了一系列 default method,主要是增加 Lambda、Stream 相关的功能。
public interface Collection<E> extends Iterable<E> { default Stream<E> stream() { return StreamSupport.stream(spliterator(), false); } }
- 从 Java 8 之后,接口可以定义如下所示:
public interface Name { //接口中的变量其实就是常量,默认被final修饰 int age = 10; //抽象方法 String getName(); // 等价于以下三种形式 // public String getName(); // public abstract String getName(); // abstract String getName(); // 静态方法,可以省略public声明,因为在接口中的静态方法默认就是公有的 public static void setName(String name) { // 实现具体业务 } // 默认方法 default void defaultMethod(){ // 实现具体业务 System.out.println("defaultMethod"); } }
- 对于抽象这个特性,我举一个例子来进一步解释一下。
public interface IPictureStorage { void savePicture(Picture picture); Image getPicture(String pictureId); void deletePicture(String pictureId); void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo); } public class PictureStorage implements IPictureStorage { // ...省略其他属性... @Override public void savePicture(Picture picture) { ... } @Override public Image getPicture(String pictureId) { ... } @Override public void deletePicture(String pictureId) { ... } @Override public void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo) { ... } }
- 在上面的这段代码中,我们利用 Java 中的 interface 接口语法来实现抽象特性。调用者在使用图片存储功能的时候,只需要了解 IPictureStorage 这个接口类暴露了哪些方法就可以了,不需要去查看 PictureStorage 类里的具体实现逻辑。
- 实际上,抽象这个特性是非常容易实现的,并不需要非得依靠接口类或者抽象类这些特殊语法机制来支持。换句话说,并不是说一定要为实现类(PictureStorage)抽象出接口类(IPictureStorage),才叫作抽象。即便不编写 IPictureStorage 接口类,单纯的 PictureStorage 类本身就满足抽象特性。
- 之所以这么说,那是因为,类的方法是通过编程语言中的“函数”这一语法机制来实现的。通过函数包裹具体的实现逻辑,这本身就是一种抽象。调用者在使用函数的时候,并不需要去研究函数内部的实现逻辑,只需要通过函数的命名、注释或者文档,了解其提供了什么功能,就可以直接使用了。
- 抽象这个概念是一个非常通用的设计思想,并不单单用在面向对象编程中,也可以用来指导架构设计等。而且这个特性也并不需要编程语言提供特殊的语法机制来支持,只需要提供“函数”这一非常基础的语法机制,就可以实现抽象特性、所以,它没有很强的“特异性”,有时候并不被看作面向对象编程的特性之一。
2.3 接口特点
- 接口的特点有哪些呢?
- 接口没有构造方法
- 接口不能用于实例化对象
- 接口中的字段必须初始化,并且隐式地设置为公有的、静态的和final的。因此,为了符合规范,接口中的字段名要全部大写
- 接口不是被类继承,而是要被类实现
- 接口中每一个方法默认是公有和抽象的,即接口中的方法会被隐式的指定为 public abstract。从JDK 1.8开始,可以在接口中编写默认的和静态的方法。声明默认方法需要使用关键字default。并且不允许定义为 private 或者 protected。
- 当类实现接口时,类要实现接口中所有的方法。否则,类必须声明为抽象的
- 接口支持多重继承,即可以继承多个接口
2.4 Marker Interface
- 接口的职责也不仅仅限于抽象方法的集合,其实有各种不同的实践。
- 有一类没有任何方法的接口,通常叫作 Marker Interface,顾名思义,它的目的就是为了声明某些东西,比如我们熟知的 Cloneable、Serializable 等。这种用法,也存在于业界其他的 Java 产品代码中。
03.抽象类和接口
3.1 理解抽象和接口概念
这两个语法概念不仅在工作中经常会被用到,在面试中也经常被提及。比如,“接口和抽象类的区别是什么?什么时候用接口?什么时候用抽象类?抽象类和接口存在的意义是什么?能解决哪些编程问题?”等等。
abstract class和interface是Java语言中对于抽象类定义进行支持的两种机制,正是由于这两种机制的存在,才赋予了Java强大的面向对象能力。
abstract class和interface之间在对于抽象类定义的支持方面具有很大的相似性,甚至可以相互替换,避免使用时在进行抽象类定义时对于 abstract class和interface的选择随意。
其实,两者之间还是有很大的区别的,对于它们的选择甚至反映出对于问题领域本质的理解、对于设计意图的理解是否正确、合理。本文将对它们之间的区别进行一番剖析,试图给开发者提供一个在二者之间进行选择的依据。
- 1.抽象类和接口都不能直接实例化,如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。
- 2.抽象类要被子类继承,接口要被类实现。
- 3.接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。
- 4.抽象类里可以没有抽象方法。
- 5.从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类,抽象类只能被单继承。
- 6.接口中没有
this
指针,没有构造函数,不能拥有实例字段(实例变量)或实例方法。 - 7.抽象类不能在Java 8 的 lambda 表达式中使用。
- 8.接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。
- 9.接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。
3.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为了数据安全而考虑的),但是却可以实现多个接口。
3.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)提供了途径。
3.4 通俗理解两者区别
- 接口和抽象类的概念不一样。接口是对动作的抽象,抽象类是对根源的抽象。从设计理念上,接口反映的是 “like-a” 关系,抽象类反映的是 “is-a” 关系。
- 抽象类表示的是,这个对象是什么。接口表示的是,这个对象能做什么。比如,男人,女人,这两个类(如果是类的话……),他们的抽象类是人。说明,他们都是人。
- 人可以吃东西,狗也可以吃东西,你可以把“吃东西”定义成一个接口,然后让这些类去实现它.
- 所以,在高级语言上,一个类只能继承一个类(抽象类)(正如人不可能同时是生物和非生物),但是可以实现多个接口(吃饭接口、走路接口)。
3.5 解决什么编程问题
- 抽象类也是为代码复用而生的。多个子类可以继承抽象类中定义的属性和方法,避免在子类中,重复编写相同的代码。
- 不过,既然继承本身就能达到代码复用的目的,而继承也并不要求父类一定是抽象类,那我们不使用抽象类,照样也可以实现继承和复用。从这个角度上来讲,我们貌似并不需要抽象类这种语法呀。那抽象类除了解决代码复用的问题,还有什么其他存在的意义吗?
- 还是拿之前那个打印日志的例子。我们先对上面的代码做下改造。在改造之后的代码中,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/muchen/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 子类,怎么会忘记重新实现 log() 方法呢?我们举的例子比较简单,Logger 中的方法不多,代码行数也很少。但是,如果 Logger 有几百行,有 n 多方法,除非你对 Logger 的设计非常熟悉,否则忘记重新实现 log() 方法,也不是不可能的。
- Logger 可以被实例化,换句话说,我们可以 new 一个 Logger 出来,并且调用空的 log() 方法。这也增加了类被误用的风险。当然,这个问题可以通过设置私有的构造函数的方式来解决。不过,显然没有通过抽象类来的优雅。
- 为什么需要接口?它能够解决什么编程问题?
- 抽象类更多的是为了代码复用,而接口就更侧重于解耦。接口是对行为的一种抽象,相当于一组协议或者契约,你可以联想类比一下 API 接口。调用者只需要关注抽象的接口,不需要了解具体的实现,具体的实现代码对调用者透明。接口实现了约定和实现相分离,可以降低代码间的耦合性,提高代码的可扩展性。
- 实际上,接口是一个比抽象类应用更加广泛、更加重要的知识点。比如,经常提到的“基于接口而非实现编程”,就是一条几乎天天会用到,并且能极大地提高代码的灵活性、扩展性的设计思想。
3.6 设计层次上区别
- 抽象层次不同
- 抽象类是对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。
- 跨域不同
- 抽象类所跨域的是具有相似特点的类,而接口却可以跨域不同的类。我们知道抽象类是从子类中发现公共部分,然后泛化成抽象类,子类继承该父类即可,但是接口不同。实现它的子类可以不存在任何关系,共同之处。例如猫、狗可以抽象成一个动物类抽象类,具备叫的方法。鸟、飞机可以实现飞Fly接口,具备飞的行为,这里我们总不能将鸟、飞机共用一个父类吧!所以说抽象类所体现的是一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在"is-a" 关系,即父类和派生类在概念本质上应该是相同的。对于接口则不然,并不要求接口的实现者和接口定义在概念本质上是一致的, 仅仅是实现了接口定义的契约而已。
- 设计层次不同
- 对于抽象类而言,它是自下而上来设计的,我们要先知道子类才能抽象出父类,而接口则不同,它根本就不需要知道子类的存在,只需要定义一个规则即可,至于什么子类、什么时候怎么实现它一概不知。比如我们只有一个猫类在这里,如果你这是就抽象成一个动物类,是不是设计有点儿过度?我们起码要有两个动物类,猫、狗在这里,我们在抽象他们的共同点形成动物抽象类吧!所以说抽象类往往都是通过重构而来的!
- 但是接口就不同,比如说飞,我们根本就不知道会有什么东西来实现这个飞接口,怎么实现也不得而知,我们要做的就是事前定义好飞的行为接口。所以说抽象类是自底向上抽象而来的,接口是自顶向下设计出来的。
04.两者如何选择
4.1 如何选择
- 使用接口:
- 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法;
- 需要使用多重继承。
- 使用抽象类:
- 需要在几个相关的类中共享代码。
- 需要能控制继承来的成员的访问权限,而不是都为 public。
- 需要继承非静态和非常量字段。
- 如何决定该用抽象类还是接口
- 实际上,判断的标准很简单。如果我们要表示一种 is-a 的关系,并且是为了解决代码复用的问题,我们就用抽象类;如果我们要表示一种 has-a 关系,并且是为了解决抽象而非代码复用的问题,那我们就可以使用接口。
- 从类的继承层次上来看,抽象类是一种自下而上的设计思路,先有子类的代码重复,然后再抽象成上层的父类(也就是抽象类)。而接口正好相反,它是一种自上而下的设计思路。我们在编程的时候,一般都是先设计接口,再去考虑具体的实现。
4.2 抽象类重点是复用
- 下面这段代码是一个比较典型的抽象类的使用场景(模板设计模式)。
- Logger 是一个记录日志的抽象类,FileLogger 和 MessageQueueLogger 继承 Logger,分别实现两种不同的日志记录方式:记录日志到文件中和记录日志到消息队列中。FileLogger 和 MessageQueueLogger 两个子类复用了父类 Logger 中的 name、enabled、minPermittedLevel 属性和 log() 方法,但因为这两个子类写日志的方式不同,它们又各自重写了父类中的 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() 方法。
4.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 List<Filter> filters = new ArrayList<>(); public void handleRpcRequest(RpcRequest req) { try { for (Filter filter : fitlers) { filter.doFilter(req); } } catch(RpcException e) { // ...处理过滤结果... } // ...省略其他处理逻辑... } }
- 上面这段代码是一个比较典型的接口的使用场景。通过 Java 中的 interface 关键字定义了一个 Filter 接口。AuthencationFilter 和 RateLimitFilter 是接口的两个实现类,分别实现了对 RPC 请求鉴权和限流的过滤功能。
- 代码非常简洁。结合代码再来看一下,接口都有哪些特性。
- 接口不能包含属性(也就是成员变量)。
- 接口只能声明方法,方法不能包含代码实现。
- 类实现接口的时候,必须实现接口中声明的所有方法。
05.简单总结一下
1.抽象类和接口的语法特性
- 抽象类不允许被实例化,只能被继承。它可以包含属性和方法。方法既可以包含代码实现,也可以不包含代码实现。不包含代码实现的方法叫作抽象方法。子类继承抽象类,必须实现抽象类中的所有抽象方法。接口不能包含属性,只能声明方法,方法不能包含代码实现。类实现接口的时候,必须实现接口中声明的所有方法。
2.抽象类和接口存在的意义
- 抽象类是对成员变量和方法的抽象,是一种 is-a 关系,是为了解决代码复用问题。接口仅仅是对方法的抽象,是一种 has-a 关系,表示具有某一组行为特性,是为了解决解耦问题,隔离接口和具体的实现,提高代码的扩展性。
3.抽象类和接口的应用场景区别
- 什么时候该用抽象类?什么时候该用接口?实际上,判断的标准很简单。如果要表示一种 is-a 的关系,并且是为了解决代码复用问题,我们就用抽象类;如果要表示一种 has-a 关系,并且是为了解决抽象而非代码复用问题,那我们就用接口。
谈谈接口和抽象类有什么区别?
- https://time.geekbang.org/column/article/8471