跳到主要内容

第11章-多态

方法覆盖(方法重写)

为什么要进行方法覆盖?

  • 有如下应用,通过对人员抽象

    • 抽象出了人类(People),具有姓名(name)属性,具备打招呼(speakHi)方法

    • 同时,不同国籍的人类虽具备人员抽象出的特点,都继承于人员People,也继承了People的属性与方法,典型的有中国人类ChinaPeople、美国人类AmericaPeople、越南人类等

    • 具体代码如下

      • 人类

        package com.bjpowernode.demo;

        /**
        * 人类
        */
        public class People {
        private String name;

        public String getName() {
        return name;
        }

        public void setName(String name) {
        this.name = name;
        }

        /**
        * 打招呼
        */
        public void speakHi(){
        System.out.println(this.name + "和别人打招呼!");
        }
        }
      • 中国人类

        package com.bjpowernode.demo;

        /**
        * 中国人类,继承于人员People,具备人员的属性与方法
        */
        public class ChinaPeople extends People {
        }
      • 美国人类

        package com.bjpowernode.demo;

        /**
        * 美国人类,继承于人员People,具备人员的属性与方法
        */
        public class AmericaPeople extends People {
        }
      • 人类测试类

        package com.bjpowernode.demo;

        /**
        * 人类测试类
        */
        public class PeopleDemo {
        public static void main(String[] args) {
        //中国人类使用,并打招呼
        ChinaPeople chinaPeople = new ChinaPeople();
        chinaPeople.setName("张三");
        chinaPeople.speakHi();

        //美国人类使用,并打招呼
        AmericaPeople americaPeople = new AmericaPeople();
        americaPeople.setName("jack");
        americaPeople.speakHi();
        }
        }
      • 输出结果如下

        多态-为什么要进行方法覆盖

  • 问题:上述代码中“打招呼”的业务有没有什么不合适的地方?

  • 回答:上述的代码中,从业务角度看,不同国籍的人,打招呼(speakHi)方法的语言和方式都是一样的,这样并不符合现实;也就是说,中国人类(ChinaPeople)、美国人类(AmericaPeople)或是其他国籍人的子类,打招呼方法的语言和方式应该是不一样的

  • 这样的业务诉求,就需要"方法覆盖"

如何实现方法覆盖?

  • 在子类中,定义与父类方法签名完全一样的方法,但方法体上的逻辑具备自身子类的特点,就实现了方法覆盖

  • 具体来说,满足下列条件,就叫方法覆盖

    • 应用于有继承关系的父子类中
    • 父类、子类都定义签名完全相同的方法,包括方法名、形式参数、返回类型、访问修饰符都完全相同
  • 通过方法覆盖子类可以重新实现父类的的某些方法,使其具有自己特色的行为

  • 针对上述的代码和需要,只需要修改子类中国人类(ChinaPeople)、美国人类(AmericaPeople)等其他子类,将打招呼方法speakHi进行覆盖,并编写实际的打招呼逻辑即可,具体如下

    • 中国人类

      package com.bjpowernode.demo;

      /**
      * 中国人类,继承于人员People,具备人员的属性与方法
      */
      public class ChinaPeople extends People {
      /**
      * 打招呼,覆盖父类方法
      */
      public void speakHi() {
      System.out.println("你好,我叫" + this.getName() + ",很高兴认识你!");
      }
      }
    • 美国人类

      package com.bjpowernode.demo;

      /**
      * 美国人类,继承于人员People,具备人员的属性与方法
      */
      public class AmericaPeople extends People {
      /**
      * 打招呼,覆盖父类方法
      */
      public void speakHi() {
      System.out.println("Hi,My name is " + this.getName() + ",Nice to meet you!");
      }
      }
    • 运行前述的“人类测试类”,将输出结果如下

      多态-如何进行方法覆盖

  • 方法覆盖注意点

    • 只针对普通方法,不能针对属性、构造方法、静态方法
    • 不能针对private修饰的方法进行,因为这种方法不能被继承,也无法做到覆盖,就变成了父类、子类各自的方法,只是碰巧相同而已
    • 子类覆盖方法不能抛出比父类方法更多异常,后续课程讲解异常后,再回头理解
    • 可选择使用@Override注解标识,后续课程会讲解

方法覆盖的意义

  • 实际的业务需要,如上述的应用
  • 用于实现多态,使用程序可动态切换类,兼容性更好

【练习】

  1. 练习应用实例内容,完成代码编写;掌握方法覆盖的使用

  2. 重构代码,重构”第10章-继承“的实战和练习第2题的老师类和销售员工类,要求如下

    1. 覆盖吃饭方法,老师在食堂吃,销售人员出差在外面吃
    2. 覆盖睡觉方法,老师睡家里,销售人员睡宾馆
    3. 进行测试

    面向对象基础-示例

多态

概述

  • 生活场景
    • 人类通过分析,抽象出了动物,这个抽象的动物,都具备一些通用的行为,比如移动;但,不同种类的实际动物,移动的行为的方式并不相同,比如陆地的动物移动方式是爬行、天上的动物移动方式是飞、水中的动物移动方式是游动。---这是典型的多态
    • 企业的员工,这个”员工“的概念是个抽象,会有多种岗位的员工,他们入职的时候,都要进行培训;开发员工,培训开发技术,销售员工培训销售技巧,HR员工交流能力,等。---这是典型的多态
    • ...
  • 在Java语言中,同一系列的对象(具有共同的父类),执行同一个方法,具备不同的表现(逻辑),就称为多态

如何实现多态?

  • 要实现多态,先需要使用方法覆盖实现

  • 但,实现多态是需要使用父类的引用,指向子类的对象

  • 具体实现方式如下(针对上面方法覆盖中的人员应用)

    • 方式1,简单直接使用

      • PolymorphismDemo类

        package com.bjpowernode.demo;

        /**
        * 多态示例
        */
        public class PolymorphismDemo {
        /**
        * 入口方法
        * @param args
        */
        public static void main(String[] args) {
        //方式1,简单直接实现
        //定义父类People的引用
        People people;

        System.out.println("-----------中国人-----------");
        //指向子类ChinaPeople的对象,会进行自动类型转换
        people = new ChinaPeople();
        people.setName("张三");
        //调用父类引用People的speakHi方法,并不会使用父类People的逻辑,而是动态的使用子类ChinaPeople的逻辑
        people.speakHi();

        System.out.println("-----------美国人-----------");
        //指向子类AmericaPeople的对象,会进行自动类型转换
        people = new AmericaPeople();
        people.setName("jack");
        //调用父类引用People的speakHi方法,并不会使用父类People的逻辑,而是动态的使用子类AmericaPeople的逻辑
        people.speakHi();

        //其他国家的人
        }
        }
      • 输出结果

        多态-如何实现多态-简单

    • 方式2,实际业务中使用方式,相对复杂

      • PeopleItroduce类

        package com.bjpowernode.demo;

        /**
        * 人类简介
        */
        public class PeopleIntroduce {
        /**
        * 介绍
        * @param people 进行介绍的人类
        */
        public static void introduce(People people){
        //附加逻辑
        System.out.println("【开始进行介绍...】");

        //调用打招呼方法,会根据传递过来的people的子类,动态调用子类的方法逻辑,但并不关心实际的类似,提升兼容性、可扩展性
        people.speakHi();

        //附加逻辑
        System.out.println("【结束介绍。】");
        }
        }
      • PolymorphismDemo类

        package com.bjpowernode.demo;

        /**
        * 多态示例
        */
        public class PolymorphismDemo {
        /**
        * 入口方法
        * @param args
        */
        public static void main(String[] args) {
        //方式1,简单直接实现
        //定义父类People的引用
        People people;

        System.out.println("-----------中国人-----------");
        //指向子类ChinaPeople的对象,会进行自动类型转换
        people = new ChinaPeople();
        people.setName("张三");
        //将实际国籍的人传递给人类介绍的方法
        PeopleIntroduce.introduce(people);

        System.out.println("-----------美国人-----------");
        //指向子类AmericaPeople的对象,会进行自动类型转换
        people = new AmericaPeople();
        people.setName("jack");
        //将实际国籍的人传递给人类介绍的方法
        PeopleIntroduce.introduce(people);

        //其他国家的人
        }
        }
      • 输出结果,运行查看结果

多态的意义

  • 实际的需求,就如上面的生活场景
  • 降低类间耦合,使得业务只依赖于更高层次抽象的父类,而不依赖可能会野蛮生长的实际子类
  • 增加可扩展性,如上面的人类应用中,PeopleIntroduce.introduce方法以后可替换成越南人类、巴基斯坦人类等等,只要继承People类

【注意】:上述实例代码见”代码与工具“下本章中的people项目

应用实例

  • 针对面向对象基础示例中的汇报,实现多态,并引入老板类Boss,包含听取汇报方法,能听取不同员工的汇报,如下图

    面向对象基础-示例

  • 实例代码,参考”代码与工具“下本章中的school项目

【练习】

  1. 练习应用实例内容,完成代码编写,重点看懂并练习school项目;掌握多态的使用

类型转换

分类

  • 自动类型转换,也称为向上转型
    • 就像基本数据类型中可以将short类型的变量赋值给int类型的变量一样,子类的对象可直接赋值给父类的引用,此时会进行自动类型转换
    • 自动类型转换调用对象方法时,会使用当前实际子类型方法的逻辑,即实现了多态
    • 但,同时也泯灭了子类的个性,不能通过父类的引用去调用指向的子类对象中定义的属性和方法
  • 强制类型转换,也称为向下转型
    • 在有些场景,需要将一个父类引用指向的对象转换成子类引用指向,此时需要强制类型转换
    • 强制类型转换时,如果真正的对象类型与转换类型不匹配,将会引发运行时异常(类型转换异常ClassCastException)
    • 一般可先使用instanceof关键字判断类型后,再进行处理,防止异常导致程序奔溃

应用实例

  • 实例1,自动类型转换和强制类型转换示例,(接上一个应用实例定义的类,见见”代码与工具“下本章中的people项目下的文件TypeConvertTest.java)

    package com.bjpowernode.demo;

    /**
    * 数据类型转换
    */
    public class TypeConvertTest {
    /**
    * 入口方法
    *
    * @param args
    */
    public static void main(String[] args) {
    //定义子类的引用,批向子类对象
    //中国人类
    ChinaPeople chinaPeople = new ChinaPeople();
    //美国人类
    AmericaPeople americaPeople = new AmericaPeople();

    //【自动类型转换】
    //父类的引用指向子类的对象,将进行自动类型转换,分别尝试指向ChinaPeople、AmericaPeople类型对象
    People people = chinaPeople;

    //【强制类型转换】
    //方式1、错误使用方式,子类的引用指向父类的对象,将会编译出错
    //ChinaPeople refChinaPeople1 = people;
    //方式2、语法正确,但先要确定people确实是ChinaPeople类型
    ChinaPeople refChinaPeople2 = (ChinaPeople) people;
    //方式3、语法正确,逻辑严谨,最佳使用方式
    ChinaPeople refChinaPeople3;
    //先使用instanceof判断类型
    if (people instanceof ChinaPeople) {
    refChinaPeople3 = (ChinaPeople) people;
    }
    }
    }

实战和作业

  1. 编写程序,实现铲屎官投喂多种宠物的功能,要求如下

    1. 有宠物父类Pet,具有名称属性,具备吃东西的方法

    2. 有各种宠物子类如Dog、Fish、Cat、Bird等,继承Pet类,各种宠物都具备自己不同的吃东西的方法

    3. 有铲屎官类,根据不同的宠物,都能实现投喂

    4. 铲屎官投喂的效果如下:

      多态-示例-宠物

  2. 编写程序,模拟财务人员每月给不同类型的员工计算工资的业务,注意使用合理的封装、继承与多态特性,要求如下【扩展】

    1. 员工类(Employee),所有各种员工类的父类
      1. 具有姓名、生日月份、是否为管理人员属性
      2. 具备工资计算方法(getSalary),计算方式:根据指定月份进行工资计算,并返回
      3. 所有员工通用补贴规则
        1. 工资计算月份如果是该员工生日月份,额外补贴100元
        2. 管理人员每月补贴500元
    2. 固定工资员工(SalariedEmployee),
      1. 具有月固定工资属性
      2. 具备自己的计算工资方法(getSalary),计算方式:直接拿月固定工资,并返回
    3. 小时工员工(HourlyEmployee)
      1. 具有每小时工资、当月工作小时数属性
      2. 具备自己的计算工资方法(getSalary),并返回,计算方式:
        1. 当月工作小时数不超过160小时部分:每小时工资*工作小时数
        2. 超过160小时部分:每小时工资*1.5*超过的小时数
    4. 销售员工(SalesEmployee)
      1. 具有当月销售额、提成比率属性
      2. 具备自己的计算工资方法(getSalary),计算方式:当月销售额*提成比率,并返回
    5. 有底薪销售员工(BasePlusSalesEmployee),派生于销售员工(SalesEmployee)
      1. 继承销售人员的特征与行为
      2. 具有底薪属性
      3. 具备自己的计算工资方法(getSalary),计算方式:底薪+当月销售额*提成比率,并返回
    6. 有财务人员(FinancialStaff),要求如下
      1. 具有给上述粗体的几类实际员工计算工资(caculateSalary)的静态方法,方法逻辑:接收指定员工和指定月份,输出员工姓名、该月工资
      2. 在计算每个员工工资前输出"开始计算xxx工资...",计算完成后输出"xxx工资计算完成。"
    7. 测试类(AppTest),使用财务人员类,对上述的业务中粗体的所有员工工资计算方法(getSalary)进行测试