🗒️理解「多态」
Mar 24, 2024
| Mar 24, 2024
0  |  Read Time 0 min
type
status
date
slug
summary
tags
category
icon
password
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样以来,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变。不修改代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。

多态的实现

在 Java 语言中,实现多态机制主要依赖于三个必要条件:继承、重写和向上转型。
  1. 继承(Inheritance):多态性要求必须存在有继承关系的子类和父类。只有存在这样的继承关系,子类才能继承父类的方法,并有机会重写它们。
  1. 重写(Overriding):在继承关系中,子类可以对父类中的某些方法进行重新定义。当调用这些方法时,实际上会调用子类的方法。这是多态性的关键实现方式之一。
  1. 向上转型(Upcasting):在多态中,子类的引用可以赋给父类的对象引用,这个过程称为向上转型。通过向上转型,使得一个引用变量可以调用在多个类中实现的方法。(关于「向上转型」,可参考本站文章《向上转型与向下转型》
 
多态分为编译时多态和运行时多态:
编译时多态,也称为静态多态,主要是通过方法重载(Overloading)实现的,它根据参数列表的不同来区分不同的方法。编译后,这些重载的方法会变成不同的函数,这在运行时不涉及多态性。
而运行时多态,也称为动态多态,是通过动态绑定(也称为晚期绑定)实现的。Java中的动态多态性是通过超类(父类)引用变量引用子类对象来实现的。在这种情况下,调用的是哪个类的成员方法由被引用对象的实际类型决定,而不是引用变量的类型。但是,被调用的方法必须是在超类中已定义的,也就是说,必须是被子类重写过的方法。

里式替换原则

里式替换原则(Liskov Substitution Principle, LSP)是面向对象设计的一个重要原则,由巴巴拉·里斯科夫(Barbara Liskov)在1987年提出。它的核心思想是:在软件中,如果一个类是另一个类的子类,那么这个子类的对象应该可以在程序中代替父类的对象,而不会导致程序出现错误或异常行为。假设“你”是“你爸”的子类,在生活中的许多情境中,“你爸”可以出现的地方,“你”也可以出现并完成相同的任务,比如去“姥姥家干活”。但是,按照LSP原则,这种替换应当确保姥姥家的事情依旧能被正确完成,不会因为是“你”去而不是“你爸”去而产生问题。
多态的实现机制就遵循里式替换原则:当「超类对象引用变量」引用「子类对象」时(也就是子类对象向上转型为父类对象),被引用对象的类型(右边)而不是引用变量的类型(左边)决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类重写的方法。
notion image
这段话到底该怎么理解?假设我们有一个超类叫做“Animal”(动物),这个类定义了一个方法“makeSound”(发出声音)。然后我们有两个子类:“Dog”(狗)和“Cat”(猫),它们分别覆盖了“makeSound”方法,以产生各自的声音。
现在,写一个函数,它接受一个Animal类型的对象作为参数,并调用其makeSound方法:
当我们传递一个Dog对象或一个Cat对象给perform方法时,虽然perform方法的参数类型是Animal,但是实际执行的是DogmakeSound方法或CatmakeSound方法,这就是多态的体现:
在这个例子中,perform方法能够接受不同类型的动物,并调用它们的makeSound方法,而无需知道具体是哪种动物,这样我们就实现了代码的复用、扩展和接口的一致性。

静态方法不支持多态

静态方法属于类,而不是类的实例,这意味着静态方法的调用是基于它被调用时左边的类型,也就是类名,而不是具体的实例类型(右边的类名)。因此,即使一个引用变量被声明为父类类型但引用了一个子类的实例,调用静态方法时,执行的仍然是声明该方法的类中的版本,而不是引用的实际对象的类的版本。这是因为静态方法不是多态的,它们不是根据对象的实际类型在运行时解析的。
假设有两个类,ParentChild,其中Child继承自Parent。两个类都定义了一个静态方法staticMethod()
在主程序中,你可以有如下的代码:
尽管childAsParent引用实际上指向一个Child类的实例,但是当我们调用staticMethod()时,输出将会是:
这是因为静态方法的调用只与引用变量的声明类型(左边类名)有关,也就是childAsParent的声明类型Parent,而不是实际指向的对象类型Child。这就解释了为什么静态方法不支持多态:因为不管引用变量的实际类型是什么,调用的都是引用变量声明时的类型对应的静态方法。

编译看左,运行看右

「编译看左,运行看右」就是对Java非静态方法多态性工作原理的一句话总结。经过前文对「多态」的讲解,现在看到「编译看左,运行看右」读者应该已经有比较清楚的认识了。
「编译看左」:指的是在编译时(Compilation),Java编译器主要关注变量的声明类型,即代码中变量左边的类型。这个类型决定了你能调用哪些方法。这是因为在编译时,编译器只能使用这个信息来检查代码的正确性,包括方法的存在性和参数匹配等。
「运行看右」:指的是在运行时(Runtime),Java虚拟机(JVM)关注的是实际对象的类型,即代码中赋值给变量的具体对象类型(变量右边的类型)。如果这个实际对象的类重写了某个方法,那么就是这个重写的方法会被调用。
  • 编译时,「编译看左」,编译器查看obj的声明类型是Parent,所以它会确保instanceMethod()方法在Parent类中存在。
  • 运行时,「运行看右」,虽然obj的声明类型是Parent,但实际指向的对象是Child类的实例,因此调用的是Child类中的instanceMethod()方法,所以输出将会是“这是子类的实例方法”。
  • java
  • java basics
  • 向上转型与向下转型java的「内存管理」与「数据传递」
    Catalog