Java学习笔记(六)——多态

多态

方法和对象具有多种形态。是面向对象的第三大特征,多态是建立在封装和继承基础之上的。

例子引出多态

假设有一个主人Master类,其中有一个feed(喂食)方法,可以完成主人给动物喂食的信息。假设Food类有两个子类:Fish和Bone,动物Animal类有两个子类Cat和Dog。根据前面知识可以写出代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Animal类
package com.learn.poly_;

public class Animal {
private String name;

public Animal(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
1
2
3
4
5
6
7
8
//Dog子类
package com.learn.poly_;

public class Dog extends Animal{
public Dog(String name) {
super(name);
}
}
1
2
3
4
5
6
7
8
//Cat子类
package com.learn.poly_;

public class Bone extends Food{
public Bone(String name) {
super(name);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Food父类
package com.learn.poly_;

public class Food {
private String name;

public Food(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
1
2
3
4
5
6
7
8
//Fish子类
package com.learn.poly_;

public class Fish extends Food{
public Fish(String name) {
super(name);
}
}
1
2
3
4
5
6
7
8
//Bone子类
package com.learn.poly_;

public class Bone extends Food{
public Bone(String name) {
super(name);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//Master类
package com.learn.poly_;

public class Master {
private String name;

public Master(String name) {
this.name = name;
}

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());
}

//这里如果动物、食物有很多种呢?是不是要写很多种不同的方法来重载?
//====> feed方法很多,不利于管理和维护
//====> 引出对象的多态性
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//主程序
package com.learn.poly_;

public class poly01 {
public static void main(String[] args) {
Master tom = new Master("tom");
Dog dog = new Dog("大黄");
Bone bone = new Bone("排骨");
tom.feed(dog,bone);

Cat cat = new Cat("汤姆");
Fish fish = new Fish("小鱼");
tom.feed(cat,fish);

}
}

可以看到feed方法通过重载的方式来实现对不同类别动物的喂食操作,这样如果动物、食物种类很多,那是不是意味着要写很多种不同的方法来重载呢?

这个问题可以通过对象的多态性来解决。

多态基本介绍

方法的多态:重写和重载 。

对象的多态(核心):

  • 1、一个对象的编译类型和运行类型可以不一致
1
2
3
4
//以父类Animal和子类Dog为例:
Animal animal = new Dog();//animal编译类型是Animal,运行类型是Dog
//强调:这里的animal是一个对象引用,并不是一个对象,而后面new的这个Dog()才是一个真正的对象。
//这里也就是说:一个父类的引用可以指向一个子类的对象
  • 2、编译类型在定义对象时,就确定了,不能改变
  • 3、运行类型是可以变化的
1
2
3
//接着上面那个例子
animal = new Cat();
//animal的运行类型变成了Cat,但是编译类型仍然是Animal
  • 4、编译类型看定义时=号的左边,运行类型看=号的右边。

了解了方法和对象的多态性之后,可以对上面例子的代码进行简化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//Master类
package com.learn.poly_;

public class Master {
private String name;

public Master(String name) {
this.name = name;
}

//使用多态机制可以统一管理主人喂食物的问题
//animal编译类型是Animal,可以指向(接收)Animal子类的对象
//food 编译类型是Food,可以指向(接收)Food子类的对象
public void feed(Animal animal,Food food){
System.out.println("主人 "+name+"给 "+animal.getName()+" 吃 "+food.getName());
}

// 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());
// }
}

使用多态机制后,就算想要添加新的类,也不需要重写Master类,只需要添加新的类别,在主函数中new对象即可,大大提高了写代码的效率,以及代码的复用性。

多态的细节和注意事项

  • 多态的前提是:两个对象(态)是存在继承关系

  • 多态的向上转型:

    • 1、本质:父类的引用指向了子类的对象
    • 2、语法:父类类型 引用名 = new 子类类型();
    • 3、特点:编译类型看左边,运行类型看右边。遵守的规则:
      • (1)可以调用父类中的所有成员(需遵守访问权限)
      • (2)不能调用子类中的特有成员(因为在编译阶段,能调用哪些成员是由编译类型来决定的);
      • (3)最终运行效果看子类(运行类型)的具体实现(即调用方法时,按照从子类开始查找方法,然后去调用,规则和前面讲的方法调用规则一致)。
  • 多态的向下转型:

    • 1、语法:子类类型 引用名 = (子类类型)父类引用
    1
    2
    3
    4
    5
    6
    7
    //向上转型
    Animal animal = new Cat();
    //向下转型
    Cat cat = (Cat) animal;
    cat.privateMethod();
    //这里cat的编译类型是Cat,运行类型也是Cat。这样向下转型后,有两个引用指向Cat()这个对象,一个是Animal类的引用,一个是Cat类的引用,没有动原先的animal,原先的animal依然不可以访问Cat对象中特有的方法。
    //感觉直白来说,就是少用一个new字,节省空间?而且再new一个cat也不是原来的cat了
    • 2、只能强转父类的引用,不能强转父类的对象
    • 3、要求父类的引用必须指向的是当前目标类型的对象
    1
    2
    3
    Animal animal = new Cat();
    Cat cat = (Cat) animal;//编译通过,运行正确
    Dog dog = (Dog) animal;//编译通过,但运行错误,抛出异常
    • 4、当向下转型后,可以调用子类类型中的所有成员
  • 属性没有重写之说!属性的值看编译类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class PolyDetail02{
public static void main(String[] args){
//属性没有重写之说!属性的值看编译类型
Base base = new Sub();
System.out.println(base.count);//输出的是哪个count?答案是10
}
}

class Base{
int count = 10;
}

class Sub extends Base{
int count = 20;
}
  • instanceOf比较操作符,用于判断对象的运行类型是否为XX类型或XX类型的子类型

动态绑定机制

java重要特性:动态绑定机制

  • 1、当调用对象方法的时候,该方法会和该对象的内存地址/运行类型绑定(因为运行类型是在堆中,有一个地址)。
  • 2、当调用对象属性时,没有动态绑定机制,哪里声明,哪里使用

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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 sum1(){
return i+10;
}
public int getI(){
return i;
}
}

//main方法中
A a = new B();//向上转型
System.out.println(a.sum());//输出40
System.out.println(a.sum1());//输出30

第二种情况:假设子类中的sum()被注释了,输出为何?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class A{//父类
public int i = 10;
//动态绑定机制
public int sum(){
return getI()+10;//20+10
}
public int sum1(){
return i+10;
}
public int getI(){//父类getI
return i;
}
}

class B extends A{//子类
public int i = 20;
public int sum1(){
return i+10;
}
public int getI(){
return i;
}
}

//main方法中
A a = new B();//向上转型
System.out.println(a.sum());//输出30
//这里找不到sum,所以去父类A找sum方法。调用getI()方法,与对象a动态绑定,由于a的运行类型是B类
//所以在执行A类sum方法中的 return getI()+10;这一句时,还是调用的B类中的getI方法,提取到B子类中的i属性:20.
System.out.println(a.sum1());//输出30

接着继续注销子类中的sum1()方法,结果又为何?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class A{//父类
public int i = 10;
//动态绑定机制
public int sum(){//父类sum
return getI()+10;//20+10
}
public int sum1(){//父类sum1
return i+10;//10+10
}
public int getI(){//父类getI
return i;
}
}

class B extends A{//子类
public int i = 20;
public int getI(){
return i;
}
}

//main方法中
A a = new B();//向上转型
System.out.println(a.sum());//输出30
//这里子类找不到sum,所以去父类A找sum方法。调用getI()方法,与对象a动态绑定,由于a的运行类型是B类
//所以在执行A类sum方法中的 return getI()+10;这一句时,还是调用的B类中的getI方法,提取到B子类中的i属性:20.
System.out.println(a.sum1());//输出20
//这里子类找不到sum1,所以去父类A找sum1方法。直接return i+10;
//由于调用对象属性时,没有动态绑定机制,哪里声明,哪里使用,所以直接返回的是A类中声明的i=10。i+10=20。

多态的应用

多态数组

数组的定义类型为父类类型,里面保存的实际元素类型为子类类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class PloyArray {
public static void main(String[] args) {
//应用实例:现有一个继承结构如下:要求创建一个person对象、
//2个Student 对象和2个Teacher对象,统一放到数组中,并调用每个对象的say方法
Person[] persons = new Person[5];
persons[0] = new Person("jack",29);
persons[1] = new Student("smith",19,100);
persons[2] = new Student("Alan",18,60.0);
persons[3] = new Teacher("scott",40,20000);
persons[4] = new Teacher("kobe",43,250000);

//循环遍历多态数组,调用say
for(int i = 0 ; i<persons.length;i++){
//person[i]不管是哪一个元素,编译类型是Person,运行类型根据实际情况由jvm来判断
System.out.println(persons[i].say());//动态绑定机制

//应用升级:如何调用子类特有的方法,比如Teacher有一个teach,Student有一个Study

//类型判断+向下转型
if(persons[i] instanceof Student){//判断persons[i]的运行类型是不是Student
((Student) persons[i]).study();
}else if(persons[i] instanceof Teacher){//判断persons[i]的运行类型是不是Teacher
((Teacher) persons[i]).teach();
}else if(persons[i] instanceof Person){

}else{
System.out.println("类型有误");
}
}
}
}

多态参数

方法定义的形参类型为父类类型,实参类型允许为子类类型