Java 有一个比较容易忽略的小知识点,就是其动态绑定/静态绑定机制。如果不能理解这个机制,就有可能在执行上转型对象的重写方法时得到预料之外的结果。
首先了解上转型对象
上转型对象,至少和两个类有关,并且这两个类之间存在着继承关系。
比如说,苹果 Apple 和水果 Fruit 就是两个具备继承关系的类:
class Fruit {}
class Apple extends Fruit {}
此时,当声明了一个 Fruit 类的引用,该引用却指向了一个 Apple 类的实例时:
Fruit apple = new Apple();
此时的这个apple对象,我们就称之为上转型对象。大意为:一个子类实例被赋给了父类的引用。
Java中的属性是静态绑定的
我们用一个例子来说明这个问题,先声明两个具备继承关系的类,且它们内部有重名但不同值的属性value:
class Parent {
public int value = 10;
}
class Son extends Parent {
public int value = 20;
}
我们在主函数中声明两个 Parent 引用,但是它们分别指向 Son 实例和 Parent 实例:
Parent son = new Son();
System.out.println(son.value);
Parent parent = new Parent();
System.out.println(parent.value);
执行程序,我们可以发现,这两行输出的值一样,而不是10与20。
这是Java的静态绑定机制决定的。静态绑定的内容指在编译过程中就已经确定了的内容。
一句话来概括被静态绑定规则,就是这个引用本身是什么类型,访问的就是谁的内容,程序不会去考虑重写机制可能带来的歧义问题。
带入到这个例子中就是:指向了一个 Son 类实例的引用 son "理应" 得到 Son 类定义的默认 value 值,但该引用本身是一个 Person 类型,因此静态绑定机制使得程序只会去访问 Person 类中定义的value 值,而不是 Son 类的。(重写只是个比喻,对属性重写在 Java 中本身就属于伪命题)
既然属性无法重写,那么父类的同名属性何去何从?
class Super {
public int val = 200;
}
class Sub extends Super {
public int val = 300;
public void printThisValueAndSuperValue() {
System.out.println("this.value=" + this.val);
System.out.println("super.value=" + super.val);
}
}
如题。我们给出两种猜想(实际答案很明显):
- 第一种猜想,父类的同名成员因被覆盖而不再存在。
- 第二种猜想,父类的同名成员隐藏了起来,需要使用
super关键字来访问,但仍然存在。
实践出真知。我们在主函数中声明一个 Sub 类的实例,并调用printThisValueAndSuperValue方法,查看能否同时打印出子类和父类的两个同名属性:
new Sub().printThisValueAndSuperValue();
运行程序,没有任何的问题,并正确地打印了两个值(200,300)。这说明:当子类声明了与父类重名的属性时,父类的同名属性仍然被保留了下来,并可以使用super关键字将来访问。
话虽如此,在开发中,为了避免不必要的麻烦,不会在子类中声明与父类同名的属性。
有以下细节需要注意:
- 在 Java 中,对重写和重载的判定均不把 "访问修饰符是否一致" 考虑在内。
- 子类的重写方法的访问修饰符不可以比父类的更加严格。
- 子类不可以去重写在父类中被声明为
private的方法。
观察 Java 对方法的绑定机制
设现在有存在继承关系的两个类:Person 和 Son 类。它们满足以下条件:
- 具备相同的
getValue方法:都是返回本类的value值。 - 在父类声明了
valueAdd10()方法,方法内部通过getValue方法获取其value属性。 - Person类的方法都是公开(表示允许被Son类继承)的。
class Parent {
private int value = 10;
public void valueAdd10() {
System.out.println("parent:" + (getValue() + 10));
}
public int getValue() {
return value;
}
}
class Son extends Parent {
private int value = 20;
public int getValue() {
return value;
}
}
在主程序中创建一个上转型对象,运行valueAdd10方法。
Parent son = new Son();
son.valueAdd10();
由于valueAdd10是声明在父类的方法,所以它可能会造成一种错觉:其内部的getValue返回的也是父类的值。但实际上,屏幕打印的内容却是:parent:30(这说明执行的是子类的getValue方法)。
这是 Java 的动态绑定机制导致的现象:在运行到valueAdd10方法内的getValue方法时,由于son是一个上转型对象,并且它重写了getValue方法,因此程序在此处优先选择了子类的getValue方法。
我们随后进行一些改动,来让屏幕输出:parent:20。
- 将
valueAdd10做如下改造。
public void valueAdd10() {
System.out.println("parent:" + (value + 10));
}
刚才已经介绍了,Java中的属性是不可重写的。这个value不存在任何歧义:它就是指代父类的属性。
- 或者只将主函数的引用指向Person类的实例:
Parent parent = new Parent();
parent.valueAdd10();
这就是声明一个普通的Person对象,它执行的当然就都是属于自己的属性和方法了。
并不是所有的方法都是动态绑定
动态绑定,即运行时绑定,只有在运行时程序才可以确认究竟要执行那个方法。首先要知道何时Java会选择动态绑定:
- 存在向上转型的情况。
- 这个方法被允许被子类重写。
第二条的言外之意就是:如果一个方法不允许被重写,那就谈不上动态绑定。
private修饰的方法
接着用上一节的例子做一点文章:我们将父类的getValue方法使用private关键字来修饰,其它部分不动,然后运行程序。结果是在屏幕中打印出来:parent:20。
若尝试在子类的getValue方法上加上一个@Override注解,则会提示编译错误。这说明了编译器认为目前子类的public getValue()方法和父类的private getValue()是两码事。
本质原因是因为:父类被private修饰的getValue()方法被保护了起来,它没有被子类所继承,那就更谈不上重写。所以即便声明了一个上转型对象,由于只有父类内部可以调用这个方法(private方法不对外公开),VM 在执行代码时就不会考虑再动态地在子类中寻找重写的方法,因为此时子类的同名方法跟父类的同名方法并没有关系。
final修饰的方法
我们将父类的getValue修改回private或者protected,随后在前面加上final关键字来限定:
protected final int getValue() {return value;}
原因是final关键字禁止子类重写该方法,程序也就不会再考虑是否会有额外的重写方法了。
static修饰的方法
假如父类和子类都声明一个同名的静态getVal方法:
class Parent {
public static int getVal() {return 100;}
}
class Son extends Parent {
public static int getVal() {return 200;}
}
在该情况下,也不会发生动态绑定。因为:Java 规定子类可以继承父类的静态方法,但是不能重写父类的静态方法。而没有重写,就谈不上动态绑定。
绑定机制总结
综上所述,我们可以总结出两点:
- 凡是可被重写的方法,都是动态绑定的。在程序处理上转型对象时,总是会从子类开始,按照继承顺序向上级寻找可调用的方法。
- 而被声明(private, static, final)的方法,构造器方法和属性全都是静态绑定的。这个引用本身是什么类型的,程序则就访问该类下的方法/属性。