Java:多态

这几天把《Java编程思想》中的第八章多态看了几遍,感觉相比其它两大特性(封装、继承)而言,多态更抽象,更难理解,运用起来更加灵活,当然这只是我目前而言的感受,在以后可能会有更深入的理解,现在能做的只有先把书中学到的东西先记录下来巩固一下,以后有新的收获再更新。

多态

多态通过分离做什么和怎么做,从另一角度将接口和实现分离开来。多态不但能够改善代码的组织结构和可读性,还能够创建可扩展的程序——即无论在项目最初创建时还是在需要添加新功能时都可以“生长”的程序。
“封装”通过合并特征和行为来创建新的数据类型。“实现隐藏”则通过将细节“私有化”把接口和实现分离开来。而多态的作用则是消除类型之间的耦合关系。多态方法调用允许一种类型表现出与其他相似类型之间的区别,只要它们都是从同一个基类导出来的,多态也称作动态绑定、后期绑定或运行时绑定。

向上整型

对象既可以作为它自己本身的类型使用,也可以作为它的基类型使用。把某个对象的引用视为对其基类型的引用的做法被称作向上转型。

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
public enum Note {
MIDDLE_C, C_SHARP, B_FLAT;
}

class Instrument {
// 基类乐器,方法play()
public void play(Note n) {
// 输出"Instrument.play()"
}
}

public class Wind extends Instrument {
// 管乐器类继承乐器基类,并重写了play()方法
public void play(Note n) {
// 输出"Wind.play()" + n
}
}

public class Music {
public static void tune(Instrument i) {
// tune()方法:乐器对象调用play(),默认参数为Note.MIDDLE_C
i.play(Note.MIDDLE_C)
}
public static void main(String[] args) {
Wind flute = new Wind();
tune(flute); //向上转型
}
}

输出:

1
Wind.play() MIDDLE_C

分析:
Music类中的tune()方法的参数列表中接受的参数是乐器类的对象,但是主函数中我们传入的对象是Wind类的,但是仍然输出了正确的结果,这就是向上转型的多态。从Wind向上转型到Instrument可能会”缩小“接口,但不会比Instrument的全部接口更窄。

仅接受基类

首先我们梳理一下上例:1.tune()方法接受的对象为Instrument类型对象;2.主函数中传入的对象为Wind类。
基于上述例子中我们可以想象,如果Instrument的子类有很多个的话,如果要实现同样的功能,会多出很多代码量并且代码看上去会非常的冗杂:我们要给各个子类单独创造对象,然后分别传入tune()方法。这样的代码会有很大一部分是类似的,因此我们会想办法:我们能不能写一个方法,仅接受基类作为参数,而不是子类。也就是说,如果我们不管子类的存在,编写的代码只与基类打交道会不会更好呢?
例:

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
public class Shape {
public void draw() {}
}

public class Circle extends Shape {
public void draw() {
// 输出"Circle.draw()"
}
}

public class Square extends Shape {
// 同上
}

public class Triangle extends Shape {
// 同上
}

public class RandomShapeGenerator {
private Random rand = new Random(47);
public Shape next() {
switch(rand.nextInt(3)) {
default:
case 0: return new Circle();
case 1: return new Square();
case 2: return new Triangle();
}
}
}

public class Shapes {
private static RandomShapeGenerator gen = new RandomShapeGenerator();
public static void main(String[] args) {
Shape[] s = new Shape[9];
for(int i = 0; i < s.length; i++)
s[i] = gen.next();
for(Shape shp : s)
shp.draw();
}
}

输出:

1
2
3
4
5
6
7
8
9
Triangle.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Circle.draw()

分析:三个子类(Triangle,Circle,Sauqre)继承自一个基类(Shape)。RandomShapeGenerator类中的next()方法负责生成不同子类对象并最终返回一个Shape类型的对象(这其中包括了一个向上转型的过程在return里)。因此当我们在主函数中调用next()方法时,只能获得一个Shape的引用,是不知道具体是哪个子类的,主函数中通过调用next()方法随机生成9个对象并且装进Shape类型的数组中,然后让数组中的每个对象执行draw()方法。通过分析输出我们很容易就能得出结论,与子类型有关的特定行为正确地发生了。
结论:在编译时,编译器不需要获得任何特殊信息就能进行正确的调用。(第一个代码的例子中,把Wind flute = new Wind()改成Instrument flute = new Wind()也能获得正确结果)

多层继承

之前举的例子都只有一层继承,除了Instrument外,都是它的子类。现在我们添加一层继承:

1
2
3
4
5
class Woodwind extends Wind {
void play(Note n) {
System.out.println("Woodwind.play() " + n);
}
}

主函数中执行:

1
2
Instrument woodwind = new Woodwind();
tune(woodwind);

输出:

1
FakeWind.play()MIDDLE_C

分析:多层继承并不会影响结果的正确输出。
总结:我们所做的代码修改,不会对程序中其他不应受到影响的部分产生破坏。换句话说,多态是一项让程序员“将改变的事物与未变的事物分离开来”的重要技术。

缺陷

覆盖私有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class PrivateOverride {
private void f() {
System.out.println("private f()");
}

public static void main(String[] args) {
PrivateOverride po = new Derived();
po.f();
}
}

class Derived extends PrivateOverride {
public void f() {
System.out.println("public f()");
}
}

输出:

1
private f()

分析:Derived类为PrivateOverride类的子类,并且尝试重写f()方法。主函数中用PrivateOverride引用指向一个Derived对象,根据之前的向上转型原则,我们期待的输出应该是”public f()”,但是输出是”private f()”。原因在于private方法被自动认为是final方法,而且对导出类是屏蔽的。因此,在这种情况下,Derived类中的f()方法就是一个全新的方法;既然基类中的f()方法在子类Derived中不可见,因此也不能被重载。
结论:只有非private方法才可以被覆盖;但是还需要密切注意覆盖private方法的现象,这时虽然编译器不会报错,但是也不会按照我们所期望的来执行。确切的说,在导出类中,对于基类中的private方法,最好采用不同的名字。
思考:如果想达到期望输出,可以把基类中的f()方法改成public或者private,这样子类就可以覆盖。或者把子类中的f()方法改名,并且在主函数中重新调用即可。

域与静态方法

只有普通的方法调用是可以多态的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Super {
public int field = 0;
public int getField() {return field;}
}

class Sub extends Super {
public int field = 1;
public int getField() {return field;}
public int getSupperField() { return super.field;}
}

public class FieldAccess {
public static void main(String[] args) {
Super sup = new Sub();
System.out.println("sup.field = " + sup.field + ", sup.getField() = " + sup.getField());
Sub sub = new Sub();
System.out.println("sub.field = " + sub.field + ", sub.getField() = " + sub.getField() + ", sub.getSupperFiled() = " + sub.getSupperField());
}
}

输出:

1
2
sup.field = 0, sup.getField() = 1
sub.field = 1, sub.getField() = 1, sub.getSupperFiled() = 0

分析:
在Java中,成员变量是静态绑定因为Java不允许对成员变量执行多态操作。
当Sub对象转型为Super引用时,任何域访问操作都将由编译器解析,因此不是多态的(多态是在运行过程中进行动态绑定)。在这个例子中,虚拟机为Super.field和Sub.field分配了不同的存储空间(在堆内存中)。这样,Sub实际上包含两个称为field的域。然而,在引用Sub中的field时所产生的默认域并非Super版本的域。因此,为了得到Super.field,必须显示的指明super.field。
总结:
以上这种访问方法无疑是非常易混淆的,因此实际开发中通常会将所有的域都设置成private。

另外一种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class StaticSupper {
public static String staticGet() {
return "Base staticGet()";
}
public String dynamicGet() {
return "Base dynamicGet()";
}
}
class StaticSub extends StaticSupper {
public static String staticGet() {
return "Derived staticGet()";
}
public String dynamicGet() {
return "Derived dynamicGet()";
}
}
public class StaticPolymorphism {
public static void main(String[] args) {
StaticSupper sup = new StaticSub();
System.out.println(sup.staticGet());
System.out.println(sup.dynamicGet());
}
}

输出:

1
2
Base staticGet()
Derived dynamicGet()

分析:基类中的staticGet()方法是静态方法,dynamicGet是非静态方法,从结果中可以看出子类并没有覆盖基类中的静态方法。
总结:静态方法是与类,而并非与单个的对象相关联的。

构造器和多态

构造器不具有多态性,因为它们实际上是static方法,只不过该static声明式隐式的。

构造器的调用顺序

基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用。例:

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
class Meal {
Meal() {
System.out.println("Meal");
}
}

class Bread {
Bread() {
System.out.println("Bread");
}
}

class Cheese {
Cheese() {
System.out.println("Cheese");
}
}

class Lettuce {
Lettuce() {
System.out.println("Lettuce");
}
}

class Lunch extends Meal {
Lunch() {
System.out.println("Lunch");
}
}

class PortableLunch extends Lunch {
PortableLunch() {
System.out.println("PortableLunch");
}
}

public class Sandwich extends PortableLunch{
Sandwich() {
System.out.println("Sandwich");
}
private Bread b = new Bread();
private Cheese c = new Cheese();
private Lettuce l = new Lettuce();

public static void main(String[] args) {
new Sandwich();
}
}

输出:

1
2
3
4
5
6
7
Meal
Lunch
PortableLunch
Bread
Cheese
Lettuce
Sandwich

分析&总结:从输出中验证结论,调用构造器要遵照下面的顺序:

  1. 调用基类构造器。这个步骤会不断地反复地递归下去,首先是构造这种层次结构的根,然后是下一层导出类,等等,直到最低层的导出类。
  2. 按声明顺序调用成员的初始化方法。
  3. 调用导出类构造器的主体。

Q. What is the difference between compile-time polymorphism and runtime polymorphism?

There are two types of polymorphism in java:
1) Static Polymorphism also known as compile time polymorphism
2) Dynamic Polymorphism also known as runtime polymorphism

Example of static Polymorphism

Method overloading is one of the way java supports static polymorphism. Here we have two definitions of the same method add() which add method would be called is determined by the parameter list at the compile time. That is the reason this is also known as compile time polymorphism.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SimpleCalculator
{
int add(int a, int b) {
return a + b;
}
int add(int a, int b, int c) {
return a + b + c;
}
}
public class Demo
{
public static void main(String args[]) {

SimpleCalculator obj = new SimpleCalculator();
System.out.println(obj.add(10, 20));
System.out.println(obj.add(10, 20, 30));
}
}

Output:

1
2
30
60

Runtime Polymorphism (or Dynamic polymorphism)

It is also known as Dynamic Method Dispatch. Dynamic polymorphism is a process in which a call to an overridden method is resolved at runtime, thats why it is called runtime polymorphism.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ABC {

public void myMethod() {
System.out.println("Overridden Method");
}
}
public class XYZ extends ABC {

public void myMethod() {
System.out.println("Overriding Method");
}
public static void main(String args[]) {
ABC obj = new XYZ();
obj.myMethod();
}
}

Output:

1
Overriding Method

Q. Can you achieve Runtime Polymorphism by data members?

No, we cannot achieve runtime polymorphism by data members. Method is overridden not the data members, so runtime polymorphism can’t be achieved by data members.

Q. Overloading vs Overridden

Overloading occurs when two or more methods in one class have the same method name but different parameters. Overriding means having two methods with the same method name and parameters (i.e., method signature). One of the methods is in the parent class and the other is in the child class.