图源:
在上篇中,提到了向上转型,子类对象在被当做父类对待时,依然可以正常调用子类实例的方法,实际上这就是多态,或者说方法的多态调用。
方法绑定
之所以编程语言中通过方法名加括号,就可以在程序运行时在合适的时机执行相应的方法,这是因为编译器会对方法调用的相关语句进行方法绑定。
事实上通常所说的方法绑定都是在编译时完成的,因为编译时编译器就可以知晓方法调用对应的方法定义,但有种例外,就是多态:
package ch5.polymorphism;
import java.util.Random;
class Shape {
public void display() {
}
}
class Rectangle extends Shape {
public void display() {
super.display();
System.out.println("Rectangle is displayed.");
}
}
class Triangle extends Shape {
public void display() {
super.display();
System.out.println("Triangle is displayed.");
}
}
class Circle extends Shape {
public void display() {
super.display();
System.out.println("Circle is displayed.");
}
}
class ShapeFactory {
private static Random random = new Random();
public static Shape getRandomShape() {
switch (random.nextInt(3)) {
case 0:
return new Rectangle();
case 1:
return new Circle();
default:
return new Triangle();
}
}
}
public class Main {
public static void main(String[] args) {
Shape[] shapes = new Shape[5];
for (int i = 0; i < shapes.length; i++) {
shapes[i] = ShapeFactory.getRandomShape();
}
for (Shape shape : shapes) {
shape.display();
}
// Triangle is displayed.
// Rectangle is displayed.
// Rectangle is displayed.
// Circle is displayed.
// Rectangle is displayed.
}
}
在编译时,对于shape.display()
这样的多态调用,编译器是无法分辨的。尤其在上边这个例子中,shapes
的元素还是随机生成的。所以只有在运行时才能真正确定shapes
元素的具体类型,以及应当调用哪个类的display
方法。这被称作后期绑定,或者“运行时绑定”,有时也称作“动态绑定”。
相应的,编译时的方法绑定被称作“前期绑定”,或者“静态绑定”。
属性和静态方法
但对于属性和静态方法,就不存在类似的问题,所以它们是“前期绑定”,换句话说,它们不具备多态的特性:
package ch5.attr;
class Parent {
public String attr = "Parent.attr";
public static void test() {
System.out.println("Parent.test() is called.");
}
}
class Child extends Parent {
public String attr = "Child.attr";
public static void test() {
System.out.println("Child.test() is called.");
}
}
public class Main {
public static void main(String[] args) {
Child c = new Child();
System.out.println(c.attr);
Parent p = c;
System.out.println(p.attr);
// Child.attr
// Parent.attr
c.test();
p.test();
// Child.test() is called.
// Parent.test() is called.
}
}
没错,静态方法是可以通过对象调用的,这或许和有些人的习惯相违背,但的确可以这样做。因为对象必然属于某个类,所以自然可以通过对象“间接”调用类的静态方法。
上面的例子或许和很多人的直觉相反(我也是如此),但类属性(无论是否静态)的确都是前期绑定,不具备多态行为。它们的调用结果取决于当前句柄的类型,而非对象的真实类型。
这个结果告诉我们,最好不要让类属性是public
的,而且在子类中命名同名属性,这样就会在外部代码调用时产生上面的问题。
构造器和多态
如果构造函数中涉及多态调用,就会出现一些奇怪的问题:
package ch5.constructor;
class Parent {
private String attr = "Parent.attr";
public Parent() {
System.out.println("Parent's constructor start.");
displayAttr();
System.out.println("Parent's Constructor end.");
}
public void displayAttr() {
System.out.println("Parent.displayAttr:" + attr);
}
}
class Child extends Parent {
private String attr = "Child.attr";
public Child() {
super();
System.out.println("Child's constructor start.");
displayAttr();
System.out.println("Child's constructor end.");
}
public void displayAttr() {
System.out.println("Child.displayAttr:" + attr);
}
}
public class Main {
public static void main(String[] args) {
Child c = new Child();
// Parent's constructor start.
// Child.displayAttr:null
// Parent's Constructor end.
// Child's constructor start.
// Child.displayAttr:Child.attr
// Child's constructor end.
}
}
正如在中说的那样,涉及继承的对象在初始化时,必须由内向外进行,所以Parent
的构造函数先调用,而该函数会调用displayAttr
这个方法,这个方法实际上是一个“多态方法”,也就是说,当前this.displayAttr()
实际上是和一个Child
实例绑定的,自然会调用Child
的displayAttr
方法,但是这又有一个问题,你应该还记得,在内部类的构造函数调用时,外部类实际上只完成了静态属性的初始化,普通属性是并没有初始化的,所以此时Child
类属性attr
实际上是null
,而不是字符串Child.attr
,所以输出结果中才会出现Child.displayAttr:null
这样的结果。
所以在中所说的初始化顺序并非全部,完整的Java对象初始化顺序应当是:
-
加载所有涉及的类定义,并将所有的静态和非静态属性设置为0值。
-
从内向外初始化静态属性。
-
从内向外完成对象初始化,这包含两个步骤,先初始化内部类的非静态属性,再调用内部类的构造函数,再对外部类执行相同的初始化工作,如此往复。
这种初始化的好处在于,即使出现上面示例中匪夷所思的现象,也不会出现C/C++中那样的脏数据,至少能保证所有数据都被初始化为0值。
Java中字符串因为是对象,所以其0值是
null
,这和某些编程语言是不同的。
对于这里讨论的问题,唯一所能给出的建议是:尽量不要在构造函数中调用可能被子类继承的public
和protected
方法,如果一定需要,可以将对应的方法声明为final
。
协变返回类型
就像在中说的那样,方法重写必须是完全相同的方法签名,实际上返回值也必须完全相同才行。
有种例外——“协变返回类型”:
package ch5.override1;
class Tank {
public String toString() {
return "Tank()";
}
}
class LightTank extends Tank {
public String toString() {
return "LightTank()";
}
}
class TankFactory {
public Tank constructTank() {
return new Tank();
}
}
class LightTankFactory extends TankFactory {
public LightTank constructTank() {
return new LightTank();
}
}
public class Main {
public static void main(String[] args) {
TankFactory tf1 = new TankFactory();
TankFactory tf2 = new LightTankFactory();
System.out.println(tf1.constructTank());
System.out.println(tf2.constructTank());
// Tank()
// LightTank()
}
}
从形式上看,这似乎违反了“is原则”,LightTankFactory
的constructTank
方法与父类TankFactory
的同名方法并不完全相同,但是因为两者的返回值LightTank
与Tank
本身就是继承关系,且LightTank
是Tank
的子类,也就是说LightTank
可以当做Tank
看待,所以在某种程度上来说,这并不会违反里氏替换原则(Liskov Substitution Principle,LSP)。
关于LSP的详细说明可以阅读。
谢谢阅读。
文章评论