编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 1.1String深入理解原理
  • 1.2浮点型数据深入研究
  • 1.3数据装箱和拆箱原理
  • 1.4泛型由来和设计思想
  • 1.5加密和解密设计和原理
  • 2.1面向对象设计思想
  • 2.2抽象类和接口设计
  • 2.3封装和继承设计思想
  • 2.4复用和组合设计思想
  • 2.5对象和引用设计思想
  • 3.1IO流设计思想和原理
  • 3.2为何设计序列化数据
  • 3.3各种拷贝数据比较
  • 3.4高效文件读写的原理
  • 4.1反射性能探索和优化
  • 4.2为何要设计注解思想
  • 4.3动态代理的设计思想
  • 4.4SPI机制设计的思想
  • 4.5异常设计和捕获原理
  • 4.6虚拟机如何处理异常
  • 4.7四种引用设计思想
  • 5.1线程的前世今生探索
  • 5.2线程通信的设计思想
  • 5.3线程监控和Debug设计
  • 5.4线程和JVM之间联系
  • 5.5线程池使用技巧介绍
  • 5.6线程池设计核心原理
  • 5.7线程如何最大优化
  • 6.1多线程并发经典案例
  • 6.2并发安全前世今生
  • 6.3线程安全如何保证
  • 6.4变量的线程安全探索
  • 6.5并发上下文切换原理
  • 6.6理解CAS设计和由来
  • 6.7协程设计思想和原理
  • 6.8事物并发模型解读
  • 6.9并发设计模型研究
  • 6.10并发编程数据一致性
  • 6.11锁问题的定位和修复
  • 6.12多线程如何性能调优
  • 7.1类的加载过程和原理
  • 7.2对象布局设计的原理
  • 7.3双亲委派机制设计思想
  • 7.5代码攻击和安全防护
  • 7.6设计动态生成Java类

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
贡献者: yangchong211
上一篇
2.1面向对象设计思想
下一篇
2.3封装和继承设计思想