第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注解标识,后续课程会讲解
方法覆盖的意义
- 实际的业务需要,如上述的应用
- 用于实现多态,使用程序可动态切换类,兼容性更好
【练习】
练习应用实例内容,完成代码编写;掌握方法覆盖的使用
重构代码,重构”第10章-继承“的实战和练习第2题的老师类和销售员工类,要求如下
- 覆盖吃饭方法,老师在食堂吃,销售人员出差在外面吃
- 覆盖睡觉方法,老师睡家里,销售人员睡宾馆
- 进行测试
多态
概述
- 生活场景
- 人类通过分析,抽象出了动物,这个抽象的动物,都具备一些通用的行为,比如移动;但,不同种类的实际动物,移动的行为的方式并不相同,比如陆地的动物移动方式是爬行、天上的动物移动方式是飞、水中的动物移动方式是游动。---这是典型的多态
- 企业的员工,这个”员工“的概念是个抽象,会有多种岗位的员工,他们入职的时候,都要进行培训;开发员工,培训开发技术,销售员工培训销售技巧,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项目
【练习】
- 练习应用实例内容,完成代码编写,重点看懂并练习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;
}
}
}
实战和作业
编写程序,实现铲屎官投喂多种宠物的功能,要求如下
有宠物父类Pet,具有名称属性,具备吃东西的方法
有各种宠物子类如Dog、Fish、Cat、Bird等,继承Pet类,各种宠物都具备自己不同的吃东西的方法
有铲屎官类,根据不同的宠物,都能实现投喂
铲屎官投喂的效果如下:
编写程序,模拟财务人员每月给不同类型的员工计算工资的业务,注意使用合理的封装、继承与多态特性,要求如下【扩展】
- 员工类(Employee),所有各种员工类的父类
- 具有姓名、生日月份、是否为管理人员属性
- 具备工资计算方法(getSalary),计算方式:根据指定月份进行工资计算,并返回
- 所有员工通用补贴规则
- 工资计算月份如果是该员工生日月份,额外补贴100元
- 管理人员每月补贴500元
- 固定工资员工(SalariedEmployee),
- 具有月固定工资属性
- 具备自己的计算工资方法(getSalary),计算方式:直接拿月固定工资,并返回
- 小时工员工(HourlyEmployee)
- 具有每小时工资、当月工作小时数属性
- 具备自己的计算工资方法(getSalary),并返回,计算方式:
- 当月工作小时数不超过160小时部分:每小时工资*工作小时数
- 超过160小时部分:每小时工资*1.5*超过的小时数
- 销售员工(SalesEmployee)
- 具有当月销售额、提成比率属性
- 具备自己的计算工资方法(getSalary),计算方式:当月销售额*提成比率,并返回
- 有底薪销售员工(BasePlusSalesEmployee),派生于销售员工(SalesEmployee)
- 继承销售人员的特征与行为
- 具有底薪属性
- 具备自己的计算工资方法(getSalary),计算方式:底薪+当月销售额*提成比率,并返回
- 有财务人员(FinancialStaff),要求如下
- 具有给上述粗体的几类实际员工计算工资(caculateSalary)的静态方法,方法逻辑:接收指定员工和指定月份,输出员工姓名、该月工资
- 在计算每个员工工资前输出"开始计算xxx工资...",计算完成后输出"xxx工资计算完成。"
- 测试类(AppTest),使用财务人员类,对上述的业务中粗体的所有员工工资计算方法(getSalary)进行测试
- 员工类(Employee),所有各种员工类的父类