Java面向对象编程(OOP)的多态性(Polymorphism)

Made by Mike_Zhang


Java主题:


OOP Introduction

请阅读本博客的Java面向对象编程详细介绍 - Java OOP Detailed Introduction


1. Polymorphism Introduction

多态性(Polymorphism)与继承性(Inheritance)有着密不可分的联系,是对继承性(Inheritance)的延伸与扩展。其能改进代码的组织,增加可读性,以及使代码具有跟好的扩展性

我归纳的多态性(Polymorphism)定义:

Polymorphic method invoking allows one (sub)class to perform differently from another similar (sub)class, and both of them are inherited from the same base class. And the different performance refer to the methods that can be invoked through the base class, which means the base class is invoking the method of the derived class(late banding).
The three conditions of Polymorphism are the occurrence of inheritance, overwriting and upcasting.

先看一个案例:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Triangle { // 三角形类
void Print(){
System.out.println("A Triangle printed!"); // 父类方法
}
}

class RightTriangle extends Triangle{ //定义直角三角形子类
void Print(){
System.out.println("A Right Triangle printed!"); // 重写父类方法
}
}

class IsoscelesRightTriangle extends Triangle{ //定义等腰直角三角形子类
void Print(){
System.out.println("A Isosceles Right Triangle printed!"); // 重写父类方法
}
}

class EquilateralTriangle extends Triangle{ //定义等边三角形子类
void Print(){
System.out.println("A Isosceles Equilateral Triangle printed!"); // 重写父类方法
}
}

public class PrintTriangle { // 定义一个打印三角形的类
public static void PrintOut(RightTriangle x){ // 打印直角三角形的方法
x.Print();
}
public static void PrintOut(IsoscelesRightTriangle x){ // 打印等腰直角三角形的方法
x.Print();
}
public static void PrintOut(EquilateralTriangle x){ // 打印等边三角形的方法
x.Print();
}

public static void main(String[] args){ // 主函数 main()

RightTriangle aNewRightTriangle = new RightTriangle();
PrintOut(aNewRightTriangle); // 打印直角三角形

IsoscelesRightTriangle aNewIsoscelesRightTriangle = new IsoscelesRightTriangle();
PrintOut(aNewIsoscelesRightTriangle); // 打印等腰直角三角形

EquilateralTriangle aNewEquilateralTriangle = new EquilateralTriangle();
PrintOut(aNewEquilateralTriangle); // 打印等边三角形
}
}

上面的例子中定义了一个Triangle父类,衍生出了三个子类RightTriangleIsoscelesRightTriangleEquilateralTriangle。定义了一个打印工具PrintTriangle,为了打印出不同类的对象,在其中定义了有对应类型参数的方法PrintOut(Type x).

上面的例子运行没有问题,但是也带来了一些问题:

  1. 为了实现一个相同的方法PrintOut() 要针对每一类写一个方法,增加了很多行代码;
  2. 如果要增加一个类似于PrintOut()的方法或者增加Triangle的衍生类,就需要添加或者修改很多代码。

为了解决这些问题,可以把这些相同的PrintOut()方法写成一个方法,并用父类当作参数,而不是具体的某一个衍生类。这样可以不考虑这个父类有多少个衍生类,让方法直接与父类沟通,让父类决定把方法具体给哪一个子类调用。

而这就是多态性(Polymorphism)的作用。

因此可以把代码修改成以下:

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
32
33
34
35
36
37
38
39
40
class Triangle { // 三角形类
void Print(){
System.out.println("A Triangle printed!"); // 父类方法
}
}

class RightTriangle extends Triangle{ //定义直角三角形子类
void Print(){
System.out.println("A Right Triangle printed!"); // 重写父类方法
}
}

class IsoscelesRightTriangle extends Triangle{ //定义等腰直角三角形子类
void Print(){
System.out.println("A Isosceles Right Triangle printed!"); // 重写父类方法
}
}

class EquilateralTriangle extends Triangle{ //定义等边三角形子类
void Print(){
System.out.println("A Isosceles Equilateral Triangle printed!"); // 重写父类方法
}
}

public class PrintTriangle { // 定义一个打印三角形的类
public static void PrintOut(Triangle x){ // 打印三角形的方法,使用父类当作参数类型
x.Print();
}

public static void main(String[] args){ // 主函数 main()
Triangle aNewRightTriangle = new RightTriangle(); // upcasting
PrintOut(aNewRightTriangle);; // 打印直角三角形

Triangle aNewIsoscelesRightTriangle = new IsoscelesRightTriangle(); // upcasting
PrintOut(aNewIsoscelesRightTriangle); // 打印等腰直角三角形

Triangle aNewEquilateralTriangle = new EquilateralTriangle(); // upcasting
PrintOut(aNewEquilateralTriangle); // 打印等边三角形
}
}

2. Conditions of polymorphic method call

Polymorphic method call 实现需要有三个条件:

  1. Inheritance (继承)
  2. Overriding (重写)
  3. Upcasting (向上转型)

Condition1-Inheritance

Polymorphic method call适用的不同类之间需要有继承关系,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Triangle { // 三角形类
void Print(){
System.out.println("A Triangle printed!"); // 父类方法
}
}

class RightTriangle extends Triangle{ //定义直角三角形子类
void Print(){
System.out.println("A Right Triangle printed!"); // 重写父类方法
}
}

class IsoscelesRightTriangle extends Triangle{ //定义等腰直角三角形子类
void Print(){
System.out.println("A Isosceles Right Triangle printed!"); // 重写父类方法
}
}

class EquilateralTriangle extends Triangle{ //定义等边三角形子类
void Print(){
System.out.println("A Isosceles Equilateral Triangle printed!"); // 重写父类方法
}
}

Condition2-Overriding

正如上面例子中每个子类中都有重写父类方法的方法
条件:

  1. 重写父类的实例方法,不能是static修饰的方法(包括thissuper关键字);
  2. 方法必须是从父类继承的;
  3. 与父类的方法有相同的Signature(i.e. method name, method argument type list)。

Condition3-Upcasting

Upcasting(向上转型)指的是使一个对象引用至它的父类,如下:

1
Triangle aNewRightTriangle = new RightTriangle(); // upcasting

例子中首先创建了一个RightTriangle类的对象,然后马上赋值给了一个Triangle类对象。这并不会使编译器报错,因为这两个类有继承的关系,形成了Upcasting(向上转型)。


3. Late Binding

Polymorphsim又称late binding(后链接)、dynamic binding(动态链接) 或 run-time binding(运行时链接)

Binding(链接):
连接一个方法调用语句方法体叫做Binding(链接)。

Early binding(前链接):
Binding(链接)发生在程序运行之前

1
2
Triangle aNewRightTriangle = new RightTriangle(); // upcasting
aNewRightTriangle.Print(); // 打印直角三角形

但是在例子中运行到以上代码的时候,编译器并不能用Early binding(前链接)去找到Print()方法的正确的方法体,因为在Print()方法的定义中只有一个Triangle类的参数,它并不知道具调用了哪一个类型的参数类型以找到对应的正确方法体。


解决方法就是late binding(后链接),也就是Polymorphic method call。late binding(后链接)发生在运行时,因此也被称为dynamic binding(动态链接) 或 run-time binding(运行时链接)。

当late binding(后链接)被使用时,一种后链接机制判断运行时的对象类型,并调用其相对应正确的方法体,尽管编译器还是不知道现时的对象类型。

在Java中,所有的方法链接都是使用late binding(后链接),除了static或者final方法。
一旦定义成final方法,此方法就无法被修改,也不会被子类重写。这就保证了其可以在编译前就被连接起来,即,使用了early binding(前链接),使得late binding(后链接)变得没有必要,不用去在final方法使用。这也会使调用final方法更加有效率(但大多数情况下并不会有明显的提升,因为JVM会自动处理好运行的效率)。


当在PrintOut(Triangle x)方法体中运行以下代码时:

1
x.Print();

看似调用的是Triangle父类的Print()方法,但是因为late binding(后链接),实际上调用的是RightTriangle.Print()方法,这就体现出了Polymorphism的用处。


总的来说,Polymorphism体现在继承和重写方法的基础上,并由upcasting从子类向上到父类,再由late binding从父类向下到子类,这一环形体现。


4. Extensibility

基于Polymorphism,可以向现有的代码中更方便的加入新的类或者方法。用以上案例举例,可以添加新的Triangle子类,而无需去改变PrintOut()方法,使参数只与父类联系,无须具体到某一子类。这就是提高了代码的拓展性

在原来的类中加入新的类:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class Triangle { 
void Print(){
System.out.println("A Triangle printed!");
}
}
class IsoscelesTriangle extends Triangle{ //定义等腰三角形子类
void Print(){
System.out.println("A Isosceles Triangle printed!"); // 重写父类方法
}
}

class RightTriangle extends Triangle{
void Print(){
System.out.println("A Right Triangle printed!");
}
}

class SpecialRightTriangle extends RightTriangle{ //定义特殊直角三角形子类继承直角三角形
void Print(){
System.out.println("A Special Right Triangle printed!"); // 重写父类方法
}
}

class IsoscelesRightTriangle extends Triangle{
void Print(){
System.out.println("A Isosceles Right Triangle printed!");
}
}

class EquilateralTriangle extends Triangle{
void Print(){
System.out.println("A Isosceles Equilateral Triangle printed!");
}
}

public class PrintTriangle { // 无须改变此类
public static void PrintOut(Triangle x){ // 打印三角形的方法,使用父类当作参数类型
x.Print();
}

public static void main(String[] args){ // 主函数 main()
Triangle aNewRightTriangle = new RightTriangle();
PrintOut(aNewRightTriangle);

Triangle aNewIsoscelesRightTriangle = new IsoscelesRightTriangle();
PrintOut(aNewIsoscelesRightTriangle);

Triangle aNewEquilateralTriangle = new EquilateralTriangle();
PrintOut(aNewEquilateralTriangle);

Triangle aNewIsoscelesTriangle = new IsoscelesTriangle(); // upcasting
PrintOut(aNewIsoscelesTriangle); // 打印等腰三角形

Triangle aNewSpecialRightTriangle = new SpecialRightTriangle(); // upcasting
PrintOut(aNewSpecialRightTriangle); // 打印特殊直角三角形
}
}

添加了两个新的类,IsoscelesTriangle类继承了Triangle类,SpecialRightTriangle继承了RightTriangle类,并且这两个新的类都重写了父类的Print()方法。而因为Polymorphism,PrintOut()方法不需要改变,就可以使新的类调用,因为他们都是父类Triangle的衍生类。

Polymorphism可以使程序员把需要改变的部分保持不变的部分分开来。


5. WARNING-private method “overriding”

在之前文章中提到过重写父类的private方法,看似可行,实则子类不能访问父类的private方法,无法重写,只是创造了一个同名的方法。
因此当子类中出现了“重写父类的private方法”,并没有满足Polymorphism条件之一的Overriding,所以无法应用Polymorphism。


6. WARNING-invoking filed and static method

只有调用方法的时候才会用到Polymorphism,若直接访问对象属性,则就会在编译时访问,不会应用Polymorphism,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Triangle { // 三角形类
public int Angle = 30;
public int getAngle() {return Angle;}
}

class RightTriangle extends Triangle{ //定义直角三角形子类
public int Angle = 90;
public int getAngle() {return Angle;}
}

public class PrintTriangle { // 定义一个打印三角形的类

public static void main(String[] args){ // 主函数 main()
Triangle aNewRightTriangle = new RightTriangle(); // upcasting
System.out.println("Angle1: "+ aNewRightTriangle.Angle); // 直接访问Triangle类对象的属性
System.out.println("getAngle1: "+ aNewRightTriangle.getAngle()+"\n"); // Polymorphism应用

RightTriangle aNewRightTriangle2 = new RightTriangle(); // NO upcasting
System.out.println("Angle2: "+ aNewRightTriangle2.Angle); // 直接访问RightTriangle类对象的属性
System.out.println("getAngle2: "+ aNewRightTriangle2.getAngle()); //直接调用RightTriangle类对象的方法
}
}

输出:

1
2
3
4
5
Angle1: 30
getAngle1: 90

Angle2: 90
getAngle2: 90

因此这也是通常把所有属性用private修饰的原因,防止直接访问属性带来的问题,经常选择用方法去访问属性


如果一个方法是被static修饰的,则它不会应用Polymorphism,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Triangle { // 三角形类
public int Angle = 30;
public int getAngle() {return Angle;}
public static String getAngleStatic() {return "30";}
}

class RightTriangle extends Triangle{ //定义直角三角形子类
public int Angle = 90;
public int getAngle() {return Angle;}
public static String getAngleStatic() {return "90";}
}

public class PrintTriangle { // 定义一个打印三角形的类

public static void main(String[] args){ // 主函数 main()
Triangle aNewRightTriangle = new RightTriangle(); // upcasting
System.out.println("getAngle: "+ aNewRightTriangle.getAngle()+"\n"); // Polymorphism应用
System.out.println("getAngleStatic: "+ aNewRightTriangle.getAngleStatic()); // 访问Triangle类对象的static方法
}
}

输出:

1
2
3
getAngle: 90

getAngleStatic: 30

因为一个方法被static修饰后,它不会被子类继承,所以不会被重写,以至于不能使用Polymorphism。


7. Polymorphic methods in constructors

当一个构造函数中调用了多态(or dynamically-bound)的方法,可能会产生错误。如下:

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
class Triangle { // 三角形类
private int Angle = 30;
public void getAngle() {System.out.println("Triangle Angle:" + Angle);}
Triangle(){ // 构造方法
System.out.println("This is a Triangle before RightTriangle");
getAngle(); // 多态(dynamically-bound)方法
System.out.println("This is a Triangle after RightTriangle");
}
}

class RightTriangle extends Triangle{ //定义直角三角形子类
private int Angle = 60;
RightTriangle(int angle){
Angle = angle;
System.out.println("Right Triangle Angle:" + Angle);
}
public void getAngle() {System.out.println("Right Triangle Angle:" + Angle);}
}

public class PrintTriangle { // 定义一个打印三角形的类

public static void main(String[] args){ // 主函数 main()
new RightTriangle(90); // upcasting
}
}

输出:

1
2
3
4
This is a Triangle before RightTriangle
Right Triangle Angle:0 // not yet initialized, only binary zero
This is a Triangle after RightTriangle
Right Triangle Angle:90

根据前面的文章,在继承中,父类的构造方法是当一个被调用完成的、最先完成初始化的。如果在父类的构造方法中调用多态(dynamically-bound)方法,并且此方法通过late binding到了其子类对应的方法,此时子类还没有被完全的初始化。如例子中,在Triangle构造函数中调用到了RightTrianglegetAngle()方法,此方法中的属性Angle还没有被初始化成设计的Angle = 60,因此返回了0,输出Right Triangle Angle:0

上述案例的运行流程为:

  1. 给对象分配内存,并初始化为0(binary zero)
  2. 按照上述过程调用父类的构造函数,并且在其中调用了子类RightTrianglegetAngle()方法(尽管子类的构造函数还没有被调用),此时属性Angle的值为0(根据上一步);
  3. 从上至下运行;
  4. 运行子类的构造函数。

因此,在构造函数中,不要调用任何类中的其他方法,除被finalprivate修饰的方法外,因为它们修饰的方法不会被重写,不会应用Polymorphism,可以防止上述问题的出现。


参考

B. Eckel, Thinking in java. Upper Saddle River, N.Y. Prentice Hall, 2014.


写在最后

Java中OOP相关的知识是十分重要的, 会继续更新.
最后,希望大家一起交流,分享,指出问题,谢谢!


原创文章,转载请标明出处
Made by Mike_Zhang




感谢你的支持

Java面向对象编程(OOP)的多态性(Polymorphism)
https://ultrafish.io/post/Java-oop-polymorphism/
Author
Mike_Zhang
Posted on
October 2, 2021
Licensed under