Java 中的多态

2025 年 3 月 23 日 | 10 分钟阅读

Java 中的多态性是一个概念,通过它我们可以用不同的方式执行一个单一的动作。多态性来源于两个希腊词:poly 和 morphs。“poly”的意思是“多”,“morphs”的意思是“形式”。所以多态性就是“多种形式”。

多态性的优点

1. 代码重用

多态性允许子类中的方法覆盖其超类中的方法,从而实现代码重用并在相关类之间保持一致的接口。

2. 灵活性和可扩展性

多态性允许子类提供超类中定义的方法的自己的实现,从而更容易扩展和定制行为,而无需修改现有代码。

3. 动态方法调用

多态性支持动态方法调用,其中调用的方法在运行时由实际对象类型决定,从而提供了方法调度的灵活性。

4. 接口实现

Java 中的接口允许多个类使用自己的实现来实现相同的接口,从而促进多态行为,并使不同类的对象可以基于公共接口进行互换处理。

5. 方法重载

多态性也通过方法重载实现,其中可以在一个类或其子类中定义具有相同名称但不同参数列表的多个方法,从而增强代码可读性并允许根据参数类型灵活调用方法。

6. 降低代码复杂性

多态性通过促进模块化和层次化的类结构来帮助降低代码复杂性,从而更容易理解、维护和扩展大型软件系统。

多态性类型

Java 中有两种多态性

  1. 编译时多态
  2. 运行时多态性。

我们可以通过方法重载和方法覆盖在 Java 中实现多态性。

1) Java 中的编译时多态性

在 Java 中,方法重载用于实现编译时多态性。由于方法重载,一个类可以有许多同名但参数列表不同的方法。编译器在编译期间使用提供给它的参数的数量和种类来决定调用哪个方法。这个选择是在编译期间进行的,这就是它被称为“编译时多态性”的原因。

方法重载中的方法必须具有相同的名称,但参数的数量或种类不同。根据方法调用期间传入的输入,编译器选择合适的重载方法。如果完美匹配,则使用该过程。否则,编译器会使用拓宽来根据参数类型找到最接近的匹配项。

示例

编译并运行

输出

Sum of integers: 8
Sum of doubles: 6.2

说明

在此示例中,Calculation 类有两个 add 方法——一个接受两个整数,另一个接受两个双精度数。在方法调用中给定的参数类型决定了编译器在编译期间调用哪个 add 方法。这个选择是在编译时做出的,证明了多态性。

2) Java 中的运行时多态性

运行时多态性动态方法调度是一个在运行时而不是编译时解析对重写方法的调用的过程。

在此过程中,通过超类的引用变量调用重写方法。要调用的方法的确定基于引用变量所引用的对象。

让我们先了解运行时多态性之前的向上转型。

向上转型

如果父类的引用变量引用子类的对象,则称为向上转型。例如

Upcasting in Java

对于向上转型,我们可以使用类类型或接口类型的引用变量。例如

这里,B 类的关系将是

B IS-A A
B IS-A I
B IS-A Object

由于 Object 是 Java 中所有类的根类,所以我们可以写 B IS-A Object。

Java 运行时多态性示例

在此示例中,我们创建了两个类 Bike 和 Splendor。Splendor 类扩展了 Bike 类并覆盖了其 run() 方法。我们通过父类的引用变量调用 run 方法。由于它引用了子类对象,并且子类方法覆盖了父类方法,因此在运行时调用了子类方法。

由于方法调用由 JVM 而不是编译器决定,因此它被称为运行时多态性。

示例

编译并运行

输出

running safely with 60km.

说明

此 Java 示例使用方法覆盖来演示运行时多态性。Splendour 类扩展了 Bike 并覆盖了 run() 方法以输出“以 60km 安全运行”,而 Bike 类提供了一个名为 run() 的方法,它打印“正在运行”。由于向上转型,在 main 方法中创建了一个名为 b 的 Bike 引用变量,它指向一个 Splendour 对象。当使用此引用变量调用 run() 方法时,将调用 Splendour 类中被覆盖的方法,从而演示了运行时多态性。JVM 在运行时根据实际对象类型决定调用哪个方法。

Java 运行时多态性示例:银行

考虑这样一个场景:Bank 是一个提供获取利率的方法的类。然而,利率可能因银行而异。例如,SBI、ICICI 和 AXIS 银行分别提供 8.4%、7.3% 和 9.7% 的利率。

Java Runtime Polymorphism example of bank

注意:此示例也在方法覆盖中给出,但没有向上转型。

示例

编译并运行

输出

SBI Rate of Interest: 8.4
ICICI Rate of Interest: 7.3
AXIS Rate of Interest: 9.7

说明

此 Java 示例使用方法覆盖来演示多态性。它在 Bank 类型上提供了一个名为 getRateOfInterest() 的方法,该方法返回 0。Bank 类被子类 SBI、ICICI 和 AXIS 扩展,这些子类覆盖 getRateOfInterest() 函数以返回特定的利率。在 TestPolymorphism 类的 main 方法中创建 SBI、ICICI 和 AXIS 的对象并将其分配给 Bank 引用变量 b。通过多态性,根据提供给 b 的实际对象类型动态执行相关的 getRateOfInterest() 方法,并打印出 SBI、ICICI 和 AXIS 银行的利率。

Java 运行时多态性示例:形状

示例

编译并运行

输出

drawing rectangle...
drawing circle...
drawing triangle...

说明

在形状的上下文中,此 Java 代码通过方法覆盖作为多态性的示例。它定义了一个 draw() 方法,用于在基类 Shape 上打印“drawing...”。子类 Rectangle、Circle 和 Triangle 扩展 Shape,并在其对 draw() 函数的覆盖中为每个形状提供特定的实现。在 TestPolymorphism2 类的 main 方法中创建 Shape 引用变量 s 的对象并将其分配给 Rectangle、Circle 和 Triangle。通过多态性,当使用此引用变量调用 draw() 方法时,会根据分配给 s 的实际对象类型动态触发相应的实现。因此,对于每个形状,它输出“drawing triangle...”、“drawing circle...”和“drawing rectangle...”。这演示了 Java 编程中的多态性如何实现灵活性和动态行为。

Java 运行时多态性示例:动物

示例

编译并运行

输出

eating bread...
eating rat...
eating meat...

说明

此 Java 示例使用方法覆盖来演示多态性。它为基本类 Animal 定义了一个 eat() 函数,该函数被子类 Dog、Cat 和 Lion 以不同的饮食习惯覆盖。通过将各种子类的对象分配给 main 函数中的 Animal 引用变量 an,演示了多态行为。然后使用此引用调用 eat() 方法,该方法根据实际对象类型动态调用适当的覆盖实现。

Java 运行时多态性与数据成员

方法被覆盖,而不是数据成员,因此运行时多态性不能通过数据成员实现。

在下面的示例中,两个类都有一个数据成员 speedlimit。我们通过父类的引用变量访问数据成员,该引用变量引用子类对象。由于我们访问的是未被覆盖的数据成员,因此它将始终访问父类的数据成员。

规则:运行时多态性不能通过数据成员实现。

示例

编译并运行

输出

90

说明

基类 Bike 和子类 Honda3 是此 Java 代码定义的类层次结构的一部分。Bike 类的 speedlimit 成员变量初始化为 90,而 Honda3 类的 speedlimit 变量初始化为 150。在 Honda3 类的 main 方法中创建了一个 Bike 类型的对象,但由于向上转型,它被 Honda3 引用变量 obj 引用。当访问和打印 obj.speedlimit 时,它会产生 90 而不是 150。

这是因为 Java 中的成员变量不具有多态性;相反,编译时成员变量访问是由引用类型决定的。因为它是对 speedlimit 成员变量的引用,所以它来自 Bike 类,即使 obj 引用 Honda3 对象。

Java 运行时多态性与多层继承

让我们看一个使用多层继承实现运行时多态性的简单示例。

示例

编译并运行

输出

eating
eating fruits
drinking Milk

说明

此 Java 示例演示了动物类层次结构中的多态性和方法覆盖。Animal 类定义了一个 eat() 函数,它打印“eating”,而 Dog 和 BabyDog 类扩展 Animal 并覆盖 eat() 分别打印“eating fruits”和“drinking milk”。main 方法通过使用 eat() 方法创建和调用 Animal、Dog 和 BabyDog 对象来展示多态行为。这会根据提供给引用变量的实际对象类型动态调用适当的覆盖实现。

尝试输出

示例

编译并运行

说明

此 Java 代码示例展示了多态性和方法重写。类层次结构定义如下:Dog 扩展 Animal 并重写 eat() 以打印“dog is eating...”,BabyDog 扩展 Dog,而 Animal 有一个 eat() 方法打印“animal is eating...”在主程序中生成 BabyDog 的对象并分配给 Animal 引用变量 an。当调用 a.eat() 时,会动态调用 BabyDog 类中的重写 eat() 函数,展示了多态行为,其中使用的方法取决于运行时的实际对象类型。


Java 运行时多态性 MCQ

1. JVM 在运行时多态性中调用哪个方法?

  1. 父类中定义的方法
  2. 子类中定义的方法
  3. 父类和子类中都定义的方法
  4. 以上都不是
 

答案:b

解释:在运行时多态性中,方法调用在运行时解析,如果子类覆盖了父类方法,则调用子类中定义的方法。


2. 在 Java 中实现运行时多态性需要什么?

  1. 方法重载
  2. 继承和方法重写
  3. 抽象类
  4. Interface
 

答案:b

解释:Java 中的运行时多态性通过继承和方法重写实现。它允许子类为其超类中已定义的方法提供特定的实现。


3. 关于 Java 中的运行时多态性,以下哪项是正确的?

  1. 它在编译时解析。
  2. 它在运行时解析。
  3. 它不支持方法重写。
  4. 它可以不通过继承实现。
 

答案:b

解释:运行时多态性在程序运行时解析,而不是在编译时,这允许动态方法调度。


4. 在运行时多态性的上下文中,什么是动态方法调度?

  1. 为对象分配内存
  2. 在运行时将方法调用绑定到方法体
  3. 方法的编译
  4. 类的加载
 

答案:b

解释:动态方法调度是一种机制,通过该机制,对重写方法的调用在运行时而不是编译时解析。


5. Java 中的数据成员可以实现运行时多态性吗?

  1. 是的,总是可以
  2. 不可以,只能通过成员方法实现
  3. 仅在特殊情况下
  4. 可以,但不建议
 

答案:b

解释:Java 中的运行时多态性通过方法重写实现,而不是通过数据成员,因为数据成员的访问总是在编译时解析。