1. 多态的概念
1.1 多态的基本介绍
方法或对象具有多种形态。是面向对象的第三大特征,多态是建立在封装和继承基础之上的,可以说没有封装和继承,就不会有多态的概念
1.2 为什么需要多态
先看一个问题:
有三个类:Animal类、Food类和Master(主人)类
Animal类:通过Animal类创建多个动物子类:cat、Dog、Pig等(使用extends继承父类,下同)
Food类:通过Food类创建多种食物子类:Fish、Bone、Rice等
Master类:在这里定义多种方法,实现主人给每个动物喂食。(如下)
//主任给小狗喂食 骨头
public void feed(Dog dog, Bone bone) {
System.out.println("主人 " + name + "给 " + dog.getName() + " 吃 " + bone.getName());
}
//主人给 小猫喂 黄花鱼
public void feed(Cat cat, Fish fish) {
System.out.println("主人 " + name + "给 " + cat.getName() + " 吃 " + fish.getName());
}
这里只有两个方法,主人也只是喂了两个动物,如果动物多了起来,就要写好多好多的喂养方法。这就是传统方法的弊端:代码复用性不高,且不利于维护。
由此引出多态!
把上述传统的方法改成以下形式
public void feed(Animal animal, Food food) {
System.out.println("主人 " + name + " 给 " + animal.getName() + " 吃 " + food.getName());
}
使用多态机制,可以统一的管理主人喂食的问题
animal 编译类型是Animal,可以指向(接收)Animal子类的对象
food 编译类型是Food,可以指向(接收)Food子类的对象
如此就可以在test类中通过调用一个方法,来实现给多个动物喂食的问题。
test类代码如下:
public static void main(String[] args) {
Master tom = new Master("汤姆");
Dog dog = new Dog("大黄");
Bone bone = new Bone("骨头");
tom.feed(dog,bone);
Cat cat = new Cat("小花猫");
Fish fish = new Fish("黄花鱼");
System.out.println("===========");
tom.feed(cat,fish);
}
1.3 多态的具体体现
1.3.1 方法的多态
重写方法和重载方法就体现了多态,这里举例说明
1. 创建一个B类,写一个say()方法
2. 创建一个A类继承B类,在A类中写一个重载方法,写一个say()方法
3. 创建test类,在类中创建A和B对象,分别调用重写和重载方法
B类
public class B {
public void say(){
System.out.println("B say()方法被调用...");
}
}
A类
public class A extends B{
//以下两个sum方法构成重载
public int sum(int n1,int n2){
return n1 + n2;
}
public int sum(int n1, int n2, int n3){
return n1 + n2 +n3;
}
public void say(){
System.out.println("A say()方法被调用...");
}
}
test类
public static void main(String[] args) {
//方法重载体现多态
A a = new A();
//这里我们传入不同的参数,就会调用不同sum方法,就体现多态
System.out.println(a.sum(10, 20));
System.out.println(a.sum(10, 20, 30));
//方法重写体现多态
B b = new B();
a.say();
b.say();
}
方法重载是同一个对象调用不同的方法(因为形参不同,构成了重载),
方法重写是具有继承关系的两个类调用了一个方法,
二者都体现了多态性。
1.3.2 对象的多态
1.3.2.1 编译类型和运行类型
我们要先明白两个概念,在上文中也提到的编译类型和运行类型,编译类型可以简单的理解为是在终端命令行中写出的javac... 运行类型可以理解为是java...
先来看两行代码
A a = new A();
Animal animal = new Cat();
这两行代码=左边的A和Animal就是编译类型;=右边的A和Cat就是运行类型。
1.3.2.2 关于编译类型和运行类型的注意事项
- 一个对象的编译类型和运行类型可以不一致
- 编译类型在定义对象时,就确定了,不能改变
- 运行类型是可以变化的
- 编译类型看定义时 = 的左边,运行类型看 = 号的右边
解释:
1. 我们已经知道在Animal animal = new Cat();语句中,Animal是编译类型,Cat是运行类型。而在内存中我们对这句话的理解是Cat是我们在堆中创建的对象实例,Animal是父类的编译类型,animal 是父类的引用 指向了子类创建的Cat对象。这就是 "1" 编译类型和运行类型可以不一致的描述。当然它也可以一致。就是我们一般写的A a = new A();这样的语句。
2. 这句话就没那么多要解释的了,就是Animal这个编译类型在定义之后就是不可以改变的了。
3. 什么叫运行类型可以改变呢?看这一句,animal = new Dog(); 我们说过animal是父类的对象引用,在之前,我们把它指向了Cat对象,现在我们把它指向了Dog对象,而Cat和Dog在运行阶段又叫运行类型,所以我们说运行类型是可以改变的。
以上几条语句都体现了对象的多态性。仔细理解一下。
2. 多态的注意事项和使用细节
2.1 多态的前提
多态的前提是,两个对象(类)存在继承关系
2.2 多态的向上转型
- 本质:父类的引用指向了子类的对象
- 语法:父类类型 引用名 = new 子类类型()
- 特点:编译类型看=左边,运行类型看=右边
可以调用父类中的所有成员(要遵守访问权限)
不能调用子类中的特有方法
最终实现效果看子类的具体实现
其实向上转型已经见过了,如下代码就是一个向上转型的语法。
Animal animal = new Cat();
为了讲清楚其特点,我们写一段代码来具体分析
案例概况:
1. 创建Animal类,定义几个方法
2.创建Cat类继承Animal类,定义一个特有方法
3. 创建test类,创建cat对象实例
Animal类:
public class Animal {
String name = "动物";
int age = 10;
public void sleep(){
System.out.println("睡");
}
public void run(){
System.out.println("跑");
}
public void eat(){
System.out.println("吃");
}
public void show(){
System.out.println("hello,你好");
}
}
Cat类 :
public class Cat extends Animal{
public void eat() { //父类得方法重写
System.out.println("猫吃鱼");
}
public void catchMouse(){// 子类得特有方法
System.out.println("猫抓老鼠");
}
}
test类:
public static void main(String[] args) {
//向上转型:父类得引用指向了子类得对象
// Cat cat = new Cat();
Animal animal = new Cat();
Object object = new Cat();
// 能调用的方法和不能调用的方法
animal.eat();//可以
// animal.catchMouse();//不可以
animal.eat(); //可以
animal.run(); //可以
animal.show(); //可以
animal.sleep(); //可以
}
对应其特点,有两个如下问题:
question1:为什么不能调用子类中的特有方法?
我们在Cat类中定义了一个catchMouse()的特有方法,创建的对象也是子类对象,但是却不能调用这个特有方法。这是因为我们把Cat进行了向上转型,也就是编译类型由Cat变成了Animal,在编译阶段,能调用哪些成员是由编译类型决定的,编译器在Animal类中找不到catchMouse方法,所以会报错。
question2:最终的运行效果要看子类实现是什么意思?
这句话的意思是,调用方法时,要按照从子类(运行类型)开始查找方法。就是在jvm的内存布局中,由子类开始,一层一层的向上,往父类中寻找调用的方法,如果子类中有该方法,直接运行,若没有,向上找。
这两个问题总结出一句话:调用的时候看编译类型,运行的时候看运行类型。
2.3 多态的向下转型
我们在上面说到,向上转型不可以调用子类中特有的方法,那我们就要用这个方法怎么呢,这就引申了向下转型。我们看一下向下转型的概念和特点:
- 语法:子类类型 引用名 = (子类类型)父类引用
- 只能强转父类的引用,不能强转父类的对象
- 要求父类的引用必须指向的是当前目标类型的对象
- 当向下转型后,可以调用子类类型中所有的成员
对以上特点进行一个解释:
2. 父类对象创建之后,它是堆中存在的一个实例,是不可以更改的,我们能改的只是它在栈中的引用类型。就像我们出生之后,这个人就是实实在在的一个人,不可能再变,但是我们的名字是可以更改的。
3. 我们在进行向下的强转之前,要求这个父类引用必须指向的子类对象的类型必须是当前目标类型。举例说:
我们要写一个Cat cat = (Cat)animal的强转语句,但是在这之前,animal这个父类引用指向的必须也是这个Cat类型的这个对象。(这里还是有问题,我不太明白是Cat类型的其他对象也可以还是必须是这个类型的同一个对象,我搞清楚再来更新,如果有小伙伴明白烦请评论区告知一声。)
这时我们还没有成功的调用catchMouse方法,我们在test类中增加一些语句再去调用,为了方便,我删掉了调用父类方法的一些语句,可对照上文查看。
public static void main(String[] args) {
//向上转型:父类得引用指向了子类得对象
// Cat cat = new Cat();
Animal animal = new Cat();
Object object = new Cat();
// 能调用的方法和不能调用的方法
animal.eat();//可以
// animal.catchMouse();//不可以
Cat cat = (Cat) animal;
cat.catchMouse();
}
这时,Cat cat = (Cat) animal;这条语句的编译类型是Cat,运行类型也是Cat。还是看 = 左右来看编译类型和运行类型。
2.3.1 instanceOf 比较操作符
instanceOf 比较操作符 用于判断对象的运行类型是否为XX类型或XX类型的子类型。
在运行过程中会出现强制类型转换异常。我们可以使用instanceof运算符来避免出现强制类型转换异常。
语法:System.out.println(bb instanceof BB);用来判断bb是不是BB类型或者其子类型,返回结果是true或flase。
我们举个例子来看一下
public class test {
public static void main(String[] args) {
B b = new B();
System.out.println(b instanceof B); //返回true
System.out.println(b instanceof A); //返回true
//a的编译类型是A 运行类型是B
//instanceof判断的是该对象的运行类型是否是该类型或者该类型的子类
A a = new B();
System.out.println(a instanceof B); //返回true
System.out.println(a instanceof A); //返回true
//举个例子
Object obj = new Object();
System.out.println(obj instanceof A);//这里返回的是false,因为obj的运行类型是Object,不是A类或者A的子类
}
}
class A{ }
class B extends A{ }
要明确的是,在向下转型中,两个类之间要具有明确的父子关系时,才能强转,否则有会出现类型转换异常的报错(java.lang.ClassCastException)。而instanceof的作用在于此,判断某一个对象是否属于该类型或者该类型的子类型。从而进一步准确的进行强转。
2.4 向上类型和向下类型的总结:
- 向上转型和向下转型转的都是编译类型
- 向上转型语法:Person p = new Student()
- 向下转型语法:Student s = (Student)P;
- 区别:向上转型是在创建对象时就进行的,不是强转
向下转型是强转,不需要创建一个新的对象,可以用instanceof来判断是否可以进行强转 - 转型后要注意编译阶段和运行阶段能够调用的方法和属性是哪个类的
2.5 java的动态绑定机制
概念:
- 当调用对象方法的时候,该方法会和该对象的内存地址(运行类型)绑定
- 当调用对象属性的时候,没有动态绑定机制。在哪声明,就在哪使用。
我们通过一段代码来解释这两句话。
代码解析:
- 创建一个A类,在类中定义属性 i 并赋值,创建sum()、sum1()和getI()等方法
- 创建一个B类继承A类,在类中定义属性 i 并赋值,创建sum()、sum1()和getI()等方法
- 在main方法中创建B类对象并向上转型为A类。
public class DynamicBingding {
public static void main(String[] args) {
//a的编译类型A,运行类型B
A a = new B(); //向上转型
System.out.println(a.sum()); // 40 -> 30
System.out.println(a.sum1()); // 30 -> 20
}
}
class A{
public int i = 10;
public int sum(){
return getI() + 10;
}
public int sum1(){
return i + 10;
}
public int getI() {
return i;
}
}
class B extends A{
public int i = 20;
public int sum(){
return i+ 20;
}
public int getI() {
return i;
}
public int sum1(){
return i+10;
}
}
以上代码运行过后输出的结果为40和30。
这时注释掉B类中的sum()方法,运行时就会去父类A中寻找sum()方法,但是A类中的sum()方法包含了getI()方法,可是A类和B类中都有getI方法,该调用哪个呢?这就体现了java的动态绑定机制。
当调用对象方法时,该方法会和该对象的内存地址(运行类型)绑定,我们创建的对象a,其运行类型是B类,所以在调用方法时,首先在B类中找方法,若B类中没有该方法,才会向上级父类中寻找。
这样在B类的getI()方法得到的I值是20,最终运行输出的值为30.
注释掉B类中的sum1()方法,运行时就会去父类A中寻找sum1()方法,A类中的sum1()方法包含了 i 这个属性,当调用对象属性的时候,没有动态绑定机制。在哪声明,就在哪使用。所以这里直接使用A类中的属性 i ,最终输出为20.
3. 多态的应用
3.1 多态数组
概念:
数组的定义类型为父类类型,里面保存的实际元素类型为子类类型。
应用实例:
现有一个继承结构如下:要求创建1个Person对象、2个Student对象和2个Teacher对象,统一放在数组中,并调用每个对象的say()方法
Person类:
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String say(){
return name + "\t" + age;
}
}
Teacher类:
public class Teacher extends Person{
private double salary;
public Teacher(String name, int age, double salary) {
super(name, age);
this.salary = salary;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
//重写父类方法say
@Override
public String say() {
return super.say() + " salary=" + salary;
}
public void teach(){
System.out.println("老师"+getName()+" 正在授课");
}
}
Student类:
public class Student extends Person{
private double score;
public Student(String name, int age, double score) {
super(name, age);
this.score = score;
}
public double getScore() {
return score;
}
public void setScore(double score) {
this.score = score;
}
//重写父类的say
@Override
public String say() {
return super.say() + " score=" + score;
}
//特有的方法
public void study(){
System.out.println("学生" + getName()+" 正在听课");
}
}
父子类的继承结构如上,下面主要看main方法中 多态数组是如何定义的,以及怎么调用各个对象的say方法
main方法
public static void main(String[] args) {
Person[] persons = new Person[5];
persons[0] = new Person("jack",20);
persons[1] = new Student("jack",18,100);
persons[2] = new Student("smith",19,30.1);
persons[3] = new Teacher("scott",30,20000);
persons[4] = new Teacher("King",50,25000);
//循环遍历数组,调用say
for (int i = 0; i < persons.length; i++) {
//person[i]编译类型是Person,运行类型是根据实际情况由jvm判断
System.out.println(persons[i].say()); //动态绑定机制
}
这时发现,say()方法是父子类共有的方法,那如何调用子类的特有方法呢?
在main方法中写出如下代码;
for (int i = 0; i < persons.length; i++) {
//这时会想到向下转型,同时可以用instanceof来判断强转类型可不可以
if (persons[i] instanceof Student){
// Student student = (Student)persons[i]; //向下转型
// student.study(); //调用特有方法
//上面两条语句等同于如下一条
((Student)persons[i]).study();
} else if (persons[i] instanceof Teacher) {
// Teacher teacher = (Teacher)persons[i];
// teacher.teach();
//上面两条语句等同于如下一条
((Teacher) persons[i]).teach();
} else if (persons[i] instanceof Person) {
//这里不写输出也不调用
//因为Person也是一个类型,
//如果不写这个判断条件,在运行至person[0]这条语句时,会输出下面的输出语句的内容
//但是我们不需要对person类进行强壮(因为调用的是子类的特有方法),所以这里空着,不做处理就行
} else {
System.out.println("你的类型有误,请自己检查");
}
}
3.2 多态参数
概念:
方法定义的形参类型为父类类型,实参类型允许为子类类型。
在引入多态概念时,讲了一个主人喂动物的案例,其喂养方法就是一个多态参数。代码如下
public void feed(Animal animal, Food food) {
System.out.println("主人 " + name + " 给 " + animal.getName() + " 吃 " + food.getName());
}
这里的形参和实参就满足了多态参数的概念。
为理解透彻,再举一个案例:
案例要求:
- 定义员工类Employee,包含姓名和月工资[private],以及计算年工资getAnnual的方法
- 普通员工类继承了员工,多了work方法
- 经理类继承了员工类,多了奖金bonus属性和管理manage方法
- 普通员工和经理类要求分别重写getAnnual方法
- 测试类中添加一个方法showEmpAnnual(Employee e),实现获取任何员工对象的年工资,并在main方法中调用该方法
- 测试类中添加一个方法,testWork,如果是普通员工,则调用work方法,如果是经理,则调用manage方法
Employee类:
public class Employee {
private String name;
private double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
public double getAnnual(){
return getSalary()*12;
}
}
Worker类:
public class Worker extends Employee{
public Worker(String name, double salary) {
super(name, salary);
}
public void work(){
System.out.println("员工" + getName() + " 在工作");
}
//重写getAnnual方法
@Override
public double getAnnual() {
return super.getAnnual();
}
}
Manager类:
public class Manager extends Employee{
private int bonus;
public Manager(String name, double salary, int bonus) {
super(name, salary);
this.bonus = bonus;
}
public int getBonus() {
return bonus;
}
public void setBonus(int bonus) {
this.bonus = bonus;
}
public void manage(){
System.out.println("经理"+getName()+" 正在管理工作");
}
//重写getAnnual方法
@Override
public double getAnnual() {
return super.getAnnual() + bonus;
}
}
测试类:
public class PloyParameter{
public static void main(String[] args) {
Worker tom = new Worker("tom",2500);
Manager milan = new Manager("milan", 5000, 200000);
PloyParameter ployParameter = new PloyParameter();
ployParameter.showEmpAnnual(tom);
ployParameter.showEmpAnnual(milan);
ployParameter.testWork(tom);
ployParameter.testWork(milan);
}
public void showEmpAnnual(Employee e){
System.out.println(e.getAnnual());
}
public void testWork(Employee e){
if (e instanceof Worker){
((Worker)e).work();
} else if (e instanceof Manager) {
((Manager)e).manage();
}else {
System.out.println("不能处理...");
}
}
}
还是比较简单的,说一下测试类的东西就好。在测试类中有两个方法,传的参数是Employee类型和其子类类型,用了动态绑定机制,看具体传入的参数e是什么类型就是什么类型。
多态的内容到这里结束,内容比较多,也比较粗糙,有哪些具体的问题后续会更新!