Saul's blog Saul's blog
首页
后端
分布式
前端
更多
分类
标签
归档
友情链接
关于
GitHub (opens new window)

Saul.J.Wu

立身之本,不在高低。
首页
后端
分布式
前端
更多
分类
标签
归档
友情链接
关于
GitHub (opens new window)
  • Java入门基础

    • 计算机常识

    • Java语言概述

    • 基本语法

    • 数组

    • 面向对象

      • 面向对象
      • 类和对象
      • 类的成员-属性
      • 类的成员-方法
      • 类的成员-构造器
      • 面向对象特性-封装
      • this关键字
      • package关键字
      • import关键字
      • 面向对象特征-继承
      • 方法的重写
      • 访问权限修饰符
      • super关键字
      • 子类对象实例化的全过程
      • 面向对象特征-多态
        • 前言
        • 什么是多态?
        • 虚拟方法调用
        • 为什么要有多态?
        • 注意
        • 方法调用绑定
        • 动态绑定
        • 面试题1
        • 面试题2
        • 面试题3
        • 面试题4
        • 方法的重载与重写
        • 总结
      • 强制类型转换
      • Object类
      • 包装类
      • static关键字
      • 单例设计模式
      • main方法
      • 类的成员-代码块
      • final关键字
      • 抽象类与抽象方法
      • 接口
      • 类的成员-内部类
    • 异常处理

  • Java核心基础

  • 设计模式

  • Web开发

  • SpringBoot

  • 微服务

  • Elasticsearch

  • 运维

  • 后端
  • Java入门基础
  • 面向对象
SaulJWu
2020-12-11

面向对象特征-多态

# 前言

[TOC]

面向对象的三大特征:

  • 封装 (Encapsulation)

  • 继承 (Inheritance)

  • 多态 (Polymorphism)

接下来我们学习多态,为了方便通过代码学习,先定义三个类。

package polymorphism;

public class Person {
    String name;
    int age;

    public void eat() {
        System.out.println("人:吃饭");
    }

    public void walk() {
        System.out.println("人:走路");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package polymorphism;

public class Man extends Person {
    boolean isSmoking;

    public void earnMoney() {
        System.out.println("男人负责挣钱养家");
    }

    @Override
    public void eat() {
        System.out.println("男人多吃肉,长肌肉");
    }

    @Override
    public void walk() {
        System.out.println("男人走路,六亲不认");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package polymorphism;

public class Woman extends Person {
    boolean isBeauty;

    @Override
    public void eat() {
        System.out.println("女人少吃,为了减肥");
    }

    @Override
    public void walk() {
        System.out.println("女人窈窕的走路");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

多态是面向对象编程语言中,继数据抽象和继承之外的第三个重要特性。

# 什么是多态?

多态是同一个行为具有多个不同表现形式或形态的能力。

多态就是同一个接口,使用不同的实例而执行不同操作。

image-20201211182302416

多态性,是面向对象中最重要的概念,在Java中的体现:

什么是多态?

对象的多态性:父类的引用指向子类的对象

可以直接应用在抽象类和接口上。

//对象的多态性:父类的引用指向子类的对象
Person p2 = new Man();
Person p3 = new Woman();
1
2
3

# 虚拟方法调用

多态的使用:其实就是虚拟方法调用(Virtual Method Invocation)

当调用子父类同名同参数的方法时,实际执行的是子类重写父类的方法。也叫虚拟方法调用。

//对象的多态性:父类的引用指向子类的对象
Person p2 = new Man();
Person p3 = new Woman();
p2.eat();
p3.eat();
1
2
3
4
5
男人多吃肉,长肌肉
女人少吃,为了减肥
1
2

有了对象的多态性以后,我们在编译期,只能调用父类中声明的方法,但在运行期,实际执行的是子类重写父类的方法。

子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。

# 为什么要有多态?

假设存在一个公共的方法

class Test{
    public static void main(String[] args){
        func(new Man());
        func(new Woman())
    }
    
    public static void func(Person p){
        p.eat();
        p.walk();
    }
}
1
2
3
4
5
6
7
8
9
10
11

实际执行的是子类重写父类的方法。

思考:这样做有什么好处呢?

image-20201211185516238

假设存在n种人,如果不使用多态,我必须重载n个func方法,每个方法参数一种数据类型。

但是有了多态之后,我只需要一个func方法就可以了,参数列表填他们的父类。这就是多态的好处。

在Java API中,equals,也是使用多态,只要是Object的子类,都可以使用。

多态的作用是:消除类型之间的耦合,提高了代码的通用性,常称作接口重用。

# 注意

一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法

先来看看方法的例子:

image-20201211183250558

Man类明明有一个earnMoney()的方法,但是却不能使用,那是因为编译器认为我们所写的Person类,而不是Man类,但是运行时实际执行还是Man类中的方法。

Java引用变量有两个类型:编译时类型和运行时类型。

编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。

简称:编译时,看左边;运行时,看右边

若编译时类型和运行时类型不一致,就出现了对象的多态性(Polymorphism)

多态情况下:

  • “看左边”:看的是父类的引用(父类中不具备子类特有的方法)
  • “看右边”:看的是子类的对象(实际运行的是子类重写父类的方法)

对象的多态性,只适用于方法,不适用于属性

再来看看属性的例子:

1、先去Person类中添加一个属性

int id = 1001;
1

2、再去Man类中添加一个属性

int id = 1002;
1

3、再去使用多态访问id,看看打印的是多少?

Person p2 = new Man();
System.out.println(p2.id);
1
2

结果是1001。那是因为

一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法。

一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象是什么意思?

声明一个父类的引用 = 实例化子类对象。

父类 变量名 = new 子类();
1

对象的多态性,只适用于方法,不适用于属性。

这个2个例子很好的反证了这两句话。

# 方法调用绑定 (opens new window)

将一个方法调用和一个方法主体关联起来称作绑定。若绑定发生在程序运行前(如果有的话,由编译器和链接器实现),叫做前期绑定。你可能从来没有听说这个术语,因为它是面向过程语言不需选择默认的绑定方式,例如在 C 语言中就只有前期绑定这一种方法调用。

上述程序让人困惑的地方就在于前期绑定,因为编译器只知道一个 Instrument 引用,它无法得知究竟会调用哪个方法。

解决方法就是后期绑定,意味着在运行时根据对象的类型进行绑定。后期绑定也称为动态绑定或运行时绑定。当一种语言实现了后期绑定,就必须具有某种机制在运行时能判断对象的类型,从而调用恰当的方法。也就是说,编译器仍然不知道对象的类型,但是方法调用机制能找到正确的方法体并调用。每种语言的后期绑定机制都不同,但是可以想到,对象中一定存在某种类型信息。

# 动态绑定

//对象的多态性:父类的引用指向子类的对象
Person p = new Man();
p.eat();
1
2
3

我们已经知道,上面这两行代码实际执行的是子类重写的方法。

因为编译器只知道一个引用

Person p;//是创建一个引用,而且还Person类引用,具体指向哪个对象,编译器不知道。
1

编译时p为Person类型,而方法的调用是在运行时确定的,所以调用的是Man类的eat()方法。——动态绑定

# 面试题1

多态是编译时行为还是运行时行为?

如何证明?

//多态是运行时行为
Person p = new Man();
p.eat();
1
2
3

子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的

只有运行起来,才能确定执行的具体哪种方法。

# 面试题2

add方法是重写吗?输出结果是?

package polymorphism;

public class InterviewTest2 {

    public static void main(String[] args) {
        Base base = new Sub();
        base.add(1, 2, 3);
    }
}


class Base {
    public void add(int a, int... arr) {
        System.out.println("base");
    }
}

class Sub extends Base {
    public void add(int a, int[] arr) {
        System.out.println("sub");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

是重写,因为他们本质上是同一个方法,只不过声明方式不同而已。

JavaSE 5.0 中提供了Varargs(variable number of arguments)机制,允许直接定义能和多个实参相匹配的形参。从而,可以用一种更简单的方式,来传递个数可变的实参。

//JDK 5.0以前:采用数组形参来定义方法,传入多个同一类型变量
public static void test(int a ,String[] books);
1
2
//JDK5.0:采用可变个数形参来定义方法,传入多个同一类型变量
public static void test(int a ,String...books);
1
2

结果是sub。

从内存中解释

因为实例化的是Sub类,所以肯定是输出sub。

从多态解释

子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。

# 面试题3

输出结果是?

package polymorphism;

public class InterviewTest2 {

    public static void main(String[] args) {
        Base base = new Sub();
        base.add(1, 2, 3);
    }
}


class Base {
    public void add(int a, int... arr) {
        System.out.println("base");
    }
}

class Sub extends Base {

    @Override
    public void add(int a, int[] arr) {
        System.out.println("sub_1");
    }

    public void add(int a, int b, int c) {
        System.out.println("sub_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

结果还是sub_1,因为在动态情况下,会调用子类同名同参数的方法。

子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。

# 面试题4

下面输出结果是什么?为什么?

package polymorphism;

public class InterviewTest2 {

    public static void main(String[] args) {
        Base base = new Sub();
        Sub s = (Sub)base;
        s.add(1,2,3);
    }
}


class Base {
    public void add(int a, int... arr) {
        System.out.println("base");
    }
}

class Sub extends Base {

    @Override
    public void add(int a, int[] arr) {
        System.out.println("sub_1");
    }

    public void add(int a, int b, int c) {
        System.out.println("sub_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

输出结果是sub_2,因为强转类型后,不是多态的情况了,而且从方法优先级来判断,第二个方法已经确定了参数数量,第一个方法还没确定参数个数,所以会优先调用第二个方法。

# 方法的重载与重写

从编译和运行的角度看:

重载,是指允许存在多个同名方法,而这些方法的参数不同。编译器根据方法不同的参数表,对同名方法的名称做修饰。对于编译器而言,这些同名方法就成了不同的方法。它们的调用地址在编译期就绑定了。Java的重载是可以包括父类和子类的,即子类可以重载父类的同名不同参数的方法。

所以:对于重载而言,在方法调用之前,编译器就已经确定了所要调用的方法,这称为“早绑定”或“静态绑定”;而对于多态,只有等到方法调用的那一刻,解释运行器才会确定所要调用的具体方法,这称为“晚绑定”或“动态绑定”。

引用一句Bruce Eckel的话:“不要犯傻,如果它不是晚绑定,它就不是多态。”

Bruce Eckel:《C++编程思想》作者。

# 总结

多态的作用

消除类型之间的耦合,提高了代码的通用性,常称作接口重用。

多态存在的三个必要条件

  • 类的继承关系
  • 方法重写
  • 父类引用指向子类对象:Father f = new Child();

成员方法

编译时:要查看引用变量所声明的类中是否有所调用的方法。

运行时:调用实际new的对象所属的类中的重写方法。

成员变量

不具备多态性,只看引用变量所声明的类。

帮我改善此页面 (opens new window)
上次更新: 2020/12/18, 12:50:58
子类对象实例化的全过程
强制类型转换

← 子类对象实例化的全过程 强制类型转换→

最近更新
01
zabbix学习笔记二
02-28
02
zabbix学习笔记一
02-10
03
Linux访问不了github
12-08
更多文章>
Theme by Vdoing | Copyright © 2020-2022 Saul.J.Wu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式