编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和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.3封装和继承设计思想

目录介绍

  • 01.快速了解继承
    • 1.1 为何需要继承
    • 1.2 什么是继承
    • 1.3 继承的格式
    • 1.4 继承的意义
  • 02.继承使用介绍
    • 2.1 看一个案例
    • 2.2 继承主要事项
    • 2.3 继承好处和弊端
  • 04.初始化基类
    • 4.1 无参数构造方法
    • 4.2 有参数构造方法
  • 05.委托
    • 5.1 什么是委托
    • 5.2 有哪些使用场景
  • 06.不支持多继承影响
  • 07.分析继承初始化顺序

01.快速了解继承

1.1 为何需要继承

  • 多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那个类即可。
  • 举个例子,猫,狗,马,鸡都具有吃和睡觉的行为,都有年龄和性别属性。那么可以把共有的提取到一个动物类中,那么XX动物都可以继承动物类。

1.2 什么是继承

  • 继承是用来表示类之间的 is-a 关系,比如猫是一种哺乳动物。从继承关系上来讲,继承可以分为两种模式,单继承和多继承。单继承表示一个子类只继承一个父类,多继承表示一个子类可以继承多个父类,比如猫既是哺乳动物,又是爬行动物。

1.3 继承的格式

  • 为了实现继承这个特性,编程语言需要提供特殊的语法机制来支持,比如 Java 使用 extends 关键字来实现继承。
  • class 子类名 extends 父类名 {}
  • 单独的这个类称为父类,基类或者超类;这多个类可以称为子类或者派生类

1.4 继承的意义

  • 继承存在的意义是什么?它能解决什么编程问题?
  • 继承最大的一个好处就是代码复用。
    • 假如两个类有一些相同的属性和方法,我们就可以将这些相同的部分,抽取到父类中,让两个子类继承父类。这样,两个子类就可以重用父类中的代码,避免代码重复写多遍。不过,这一点也并不是继承所独有的,我们也可以通过其他方式来解决这个代码复用的问题,比如利用组合关系而不是继承关系。
  • 符合人的认知
    • 如果我们再上升一个思维层面,去思考继承这一特性,可以这么理解:我们代码中有一个猫类,有一个哺乳动物类。猫属于哺乳动物,从人类认知的角度上来说,是一种 is-a 关系。我们通过继承来关联两个类,反应真实世界中的这种关系,非常符合人类的认知,而且,从设计的角度来说,也有一种结构美感。
  • 过度继承的弊端
    • 过度使用继承,继承层次过深过复杂,就会导致代码可读性、可维护性变差。为了了解一个类的功能,我们不仅需要查看这个类的代码,还需要按照继承关系一层一层地往上查看“父类、父类的父类……”的代码。还有,子类和父类高度耦合,修改父类的代码,会直接影响到子类。
  • 所以,继承这个特性也是一个非常有争议的特性。很多人觉得继承是一种反模式。我们应该尽量少用,甚至不用。关于这个问题,在后面讲到“复用设计”这种设计思想的时候,在进行讨论。

02.继承使用介绍

2.1 看一个案例

  • 继承是所有面向对象语言的一个组成部分。
    • 事实证明,在创建类时总是要继承,因为除非显式地继承其他类,否则就隐式地继承 Java 的标准根类对象(Object)。
  • 当你继承时,你说,“这个新类与那个旧类类似。你可以在类主体的左大括号前的代码中声明这一点,使用关键字 extends 后跟基类的名称。当你这样做时,你将自动获得基类中的所有字段和方法。这里有一个例子:
    class Cleanser {
      private String s = "Cleanser";
      public void append(String a) { s += a; }
      public void dilute() { append(" dilute()"); }
      public void apply() { append(" apply()"); }
      public void scrub() { append(" scrub()"); }
      @Override
      public String toString() { return s; }
      public static void main(String[] args) {
        Cleanser x = new Cleanser();
        x.dilute(); x.apply(); x.scrub();
        System.out.println(x);
      }
    }
    
    public class Detergent extends Cleanser {
      // Change a method:
      @Override
      public void scrub() {
        append(" Detergent.scrub()");
        super.scrub(); // Call base-class version
      }
      // Add methods to the interface:
      public void foam() { append(" foam()"); }
      // Test the new class:
      public static void main(String[] args) {
        Detergent x = new Detergent();
        x.dilute();
        x.apply();
        x.scrub();
        x.foam();
        System.out.println(x);
        System.out.println("Testing base class:");
        Cleanser.main(args);
      }
    }
    
    /* Output:
    Cleanser dilute() apply() Detergent.scrub() scrub()
    foam()
    Testing base class:
    Cleanser dilute() apply() scrub()
    */
  • Cleanser 和 Detergent 都包含一个 main() 方法。
    • 你可以为每个类创建一个main();这允许对每个类进行简单的测试。当你完成测试时,不需要删除 main(); 你可以将其留在以后的测试中。即使程序中有很多类都有 main() 方法,惟一运行的只有在命令行上调用的 main()。
    • 在这里,Detergent.main() 显式地调用 Cleanser.main(),从命令行传递相同的参数(当然,你可以传递任何字符串数组)。
  • Cleanser 的接口中有一组方法:
    • append()、dilute()、apply()、scrub() 和 toString()。因为 Detergent 是从 Cleanser 派生的(通过 extends 关键字),所以它会在其接口中自动获取所有这些方法,即使你没有在 Detergent 中看到所有这些方法的显式定义。那么,可以把继承看作是复用类。
    • 你可以在新类中调用基类的该方法。但是在 scrub() 内部,不能简单地调用 scrub(),因为这会产生递归调用。为了解决这个问题,Java的 super 关键字引用了当前类继承的“超类”(基类)。因此表达式 super.scrub() 调用方法 scrub() 的基类版本。

2.2 继承主要事项

  • 继承的注意事项
    • a:子类只能继承父类所有非私有的成员(成员方法和成员变量)
    • b:子类不能继承父类的构造方法,但是可以通过super(待会儿讲)关键字去访问父类构造方法。
    • c:不要为了部分功能而去继承
  • 继承中构造方法的注意事项
    • 父类没有无参构造方法,子类怎么办?
      • a: 在父类中添加一个无参的构造方法
      • b:子类通过super去显示调用父类其他的带参的构造方法
      • c:子类通过this去调用本类的其他构造方法
      • 本类其他构造也必须首先访问了父类构造
    • B:注意事项
      • super(…)或者this(….)必须出现在第一条语句上

2.3 继承好处和弊端

  • 继承的好处
    • a:提高了代码的复用性
    • b:提高了代码的维护性
    • c:让类与类之间产生了关系,是多态的前提
  • 继承的弊端
    • 类的耦合性增强了。
    • 开发的原则:高内聚,低耦合。
    • 耦合:类与类的关系
    • 内聚:就是自己完成某件事情的能力

04.初始化基类

4.1 无参数构造方法

  • 现在涉及到两个类:基类和派生类。
    • 想象派生类生成的结果对象可能会让人感到困惑。从外部看,新类与基类具有相同的接口,可能还有一些额外的方法和字段。但是继承并不只是复制基类的接口。当你创建派生类的对象时,它包含基类的子对象。这个子对象与你自己创建基类的对象是一样的。只是从外部看,基类的子对象被包装在派生类的对象中。
  • 必须正确初始化基类子对象,而且只有一种方法可以保证这一点 :
    • 通过调用基类构造函数在构造函数中执行初始化,该构造函数具有执行基类初始化所需的所有适当信息和特权。Java自动在派生类构造函数中插入对基类构造函数的调用。
  • 下面的例子展示了三个层次的继承:
    class Art {
      Art() {
        System.out.println("Art constructor");
      }
    }
    
    class Drawing extends Art {
      Drawing() {
        System.out.println("Drawing constructor");
      }
    }
    
    public class Cartoon extends Drawing {
      public Cartoon() {
        System.out.println("Cartoon constructor");
      }
      public static void main(String[] args) {
        Cartoon x = new Cartoon();
      }
    }
    /* Output:
    Art constructor
    Drawing constructor
    Cartoon constructor
    */
  • 构造从基类“向外”进行,因此基类在派生类构造函数能够访问它之前进行初始化。即使不为 Cartoon 创建构造函数,编译器也会为你合成一个无参数构造函数,调用基类构造函数。

4.2 有参数构造方法

  • 上面的所有例子中构造函数都是无参数的;编译器很容易调用这些构造函数,因为不需要参数。如果没有无参数的基类构造函数,或者必须调用具有参数的基类构造函数,则必须使用 super 关键字和适当的参数列表显式地编写对基类构造函数的调用:
    class Game {
      Game(int i) {
        System.out.println("Game constructor");
      }
    }
    
    class BoardGame extends Game {
      BoardGame(int i) {
        super(i);
        System.out.println("BoardGame constructor");
      }
    }
    
    public class Chess extends BoardGame {
      Chess() {
        super(11);
        System.out.println("Chess constructor");
      }
      public static void main(String[] args) {
        Chess x = new Chess();
      }
    }
    /* Output:
    Game constructor
    BoardGame constructor
    Chess constructor
    */
  • 如果没有在 BoardGame 构造函数中调用基类构造函数,编译器就会报错找不到 Game() 的构造函数。此外,对基类构造函数的调用必须是派生类构造函数中的第一个操作。(如果你写错了,编译器会提醒你。)

05.委托

5.1 什么是委托

  • Java不直接支持的第三种重用关系称为委托。
    • 这介于继承和组合之间,因为你将一个成员对象放在正在构建的类中(比如组合),但同时又在新类中公开来自成员对象的所有方法(比如继承)。例如,宇宙飞船需要一个控制模块:
    public class SpaceShipControls {
      void up(int velocity) {}
      void down(int velocity) {}
      void left(int velocity) {}
      void right(int velocity) {}
      void forward(int velocity) {}
      void back(int velocity) {}
      void turboBoost() {}
    }
  • 建造宇宙飞船的一种方法是使用继承:
    public class DerivedSpaceShip extends SpaceShipControls {
      private String name;
      public DerivedSpaceShip(String name) {
        this.name = name;
      }
      @Override
      public String toString() { return name; }
    
      public static void main(String[] args) {
        DerivedSpaceShip protector = new DerivedSpaceShip("NSEA Protector");
        protector.forward(100);
      }
    }
  • 然而, DerivedSpaceShip 并不是真正的“一种” SpaceShipControls ,即使你“告诉” DerivedSpaceShip 调用 forward()。
    • 更准确地说,一艘宇宙飞船包含了 SpaceShipControls、,同时 SpaceShipControls 中的所有方法都暴露在宇宙飞船中。委托解决了这个难题:
    public class SpaceShipDelegation {
      private String name;
      private SpaceShipControls controls =
        new SpaceShipControls();
      public SpaceShipDelegation(String name) {
        this.name = name;
      }
      // Delegated methods:
      public void back(int velocity) {
        controls.back(velocity);
      }
      public void down(int velocity) {
        controls.down(velocity);
      }
      public void forward(int velocity) {
        controls.forward(velocity);
      }
      public void left(int velocity) {
        controls.left(velocity);
      }
      public void right(int velocity) {
        controls.right(velocity);
      }
      public void turboBoost() {
        controls.turboBoost();
      }
      public void up(int velocity) {
        controls.up(velocity);
      }
      public static void main(String[] args) {
        SpaceShipDelegation protector =
          new SpaceShipDelegation("NSEA Protector");
        protector.forward(100);
      }
    }
  • 方法被转发到底层 control对象,因此接口与继承的接口是相同的。但是,你对委托有更多的控制,因为你可以选择只在成员对象中提供方法的子集。

5.2 有哪些使用场景

06.不支持多继承影响

  • Java 不支持多继承影响
    • Java 相比于其他面向对象语言,如 C++,设计上有一些基本区别,比如Java 不支持多继承。这种限制,在规范了代码实现的同时,也产生了一些局限性,影响着程序设计结构。Java 类可以实现多个接口,因为接口是抽象方法的集合,所以这是声明性的,但不能通过扩展多个抽象类来重用逻辑。
    • 在一些情况下存在特定场景,需要抽象出与具体实现、实例化无关的通用逻辑,或者纯调用关系的逻辑,但是使用传统的抽象类会陷入到单继承的窘境。
  • 为什么是单继承而不能多继承呢?
    • 若为多继承,那么当多个父类中有重复的属性或者方法时,子类的调用结果会含糊不清,因此用了单继承。
    • 多继承虽然能使子类同时拥有多个父类的特征,但是其缺点也是很显著的,主要有两方面:
      • 如果在一个子类继承的多个父类中拥有相同名字的实例变量,子类在引用该变量时将产生歧义,无法判断应该使用哪个父类的变量。
      • 如果在一个子类继承的多个父类中拥有相同方法,子类中有没有覆盖该方法,那么调用该方法时将产生歧义,无法判断应该调用哪个父类的方法。
    • Java是从C++语言上优化而来,而C++也是面向对象的,为什么它却可以多继承的呢?首先,C++语言是1983年在C语言的基础上推出的,Java语言是1995年推出的。其次,在C++被设计出来后,就会经常掉入多继承这个陷阱,虽然它也提出了相应的解决办法,但Java语言本着简单的原则舍弃了C++中的多继承,这样也会使程序更具安全性。
  • 为什么是多实现呢?
    • 通过实现接口拓展了类的功能,若实现的多个接口中有重复的方法也没关系,因为实现类中必须重写接口中的方法,所以调用时还是调用的实现类中重写的方法。

07.分析继承初始化顺序

  • 大概的思路分析
    • 创建三个类动物Animal类、狗狗Dog类、哈士奇Huskie类,每个类中有一个非静态变量和无参构造函数,且Huskie类继承Dog类,Dog类继承Animal类;然后实例化一个Huskie类对象,根据输出的日志确认加载顺序。
  • 案例代码如下所示。先执行test()方法,然后再执行test1()方法,看看分别输出的日志。
    public void test(){
        Animal animal = new Animal();
        animal.eat();
    }
    
    public void test1(){
        Huskie huskie= new Huskie();
    }
    
    public class Animal {
        
        private Double weight = getWeight();
        
        private Double getWeight() {
            System.out.println("i am Animal getWeight method");
            return new Double(1.0);
        }
    
        private void eat(){
            System.out.println("i am eat method");
        }
    
        public Animal() {
            System.out.println("i am animal constructor");
        }
    }
    
    public class Dog extends  Animal{
    
        private int legNum = getLegNum();
    
        private int getLegNum(){
            System.out.println("i am Dog getLegNum method");
            return 4;
        }
    
        public Dog() {
            System.out.println("i am Dog constructor");
        }
    }
    
    public class Huskie extends  Dog {
    
        private Boolean isStupid = judgeIQ();
        private Boolean isLovely = judgeLovely();
    
        private Boolean judgeIQ(){
            System.out.println("i am Huskie judgeIQ method");
            return true;
        }
        private Boolean judgeLovely(){
            System.out.println("i am Huskie judgeLovely method");
            return true;
        }
    
        public Huskie() {
            System.out.println("i am Huskie constructor");
        }
    }
  • 执行test()方法
    • 发现创建一个对象,先执行成员变量,然后执行构造方法,在执行对象调用的方法。
    2021-08-10 10:23:07.976 8878-8878/com.yc.cn.ycnotification I/System.out: i am Animal getWeight method
    2021-08-10 10:23:07.976 8878-8878/com.yc.cn.ycnotification I/System.out: i am animal constructor
    2021-08-10 10:23:07.976 8878-8878/com.yc.cn.ycnotification I/System.out: i am eat method
  • 执行test1()方法
    2021-08-10 10:24:22.917 8878-8878/com.yc.cn.ycnotification I/System.out: i am Animal getWeight method
    2021-08-10 10:24:22.917 8878-8878/com.yc.cn.ycnotification I/System.out: i am animal constructor
    2021-08-10 10:24:22.917 8878-8878/com.yc.cn.ycnotification I/System.out: i am Dog getLegNum method
    2021-08-10 10:24:22.917 8878-8878/com.yc.cn.ycnotification I/System.out: i am Dog constructor
    2021-08-10 10:24:22.917 8878-8878/com.yc.cn.ycnotification I/System.out: i am Huskie judgeIQ method
    2021-08-10 10:24:22.917 8878-8878/com.yc.cn.ycnotification I/System.out: i am Huskie judgeLovely method
    2021-08-10 10:24:22.917 8878-8878/com.yc.cn.ycnotification I/System.out: i am Huskie constructor
  • 最后根据打印的日志可以分析
    • 分析Animal类,创建该对象,可以确认,实例化一个普通类,会先初始化变量,再调用构造函数,再调用对象的调用方法。
    • 分析Huskie类,创建该对象,实例化一个子类,会递归找到最最上层的父类,然后按照继承的顺序初始化,本案例中,会依次初始化Animal类,Dog类,Huskie类;并且在舒适化每一个类的时候,先初始化变量,在调用构造器。
  • 得出结论如下
    • 实例化一个普通类,会先初始化变量,再调用构造函数;
    • 实例化一个有继承关系的子类,会递归找到最上层的父类,然后按照继承的顺序依次初始化每一个类;
贡献者: yangchong211
上一篇
2.2抽象类和接口设计
下一篇
2.4复用和组合设计思想