一、再论向上转型
在第七章中我们说过,对象引用既可以作为它自己本身的类型使用,也可以作为它的基类型使用,这种把某个类型引用作为它的基类型使用的做法被称为向上转型。
package com.chenxyt.java.practice;
class Instrument{
public Instrument() {
//---
}
public void print(){
System.out.println("Instrument-----:");
}
}
class Wind extends Instrument{
public void print(){
System.out.println("Wind-----:");
}
}
public class Music{
public static void play(Instrument i){
i.print();
}
public static void main(String[] args) {
Wind wind = new Wind();
play(wind);
}
}
我们可能觉得Music.play方法有点奇怪,既然要传递Wind引用,为什么不再写一个方法来传递Wind引用呢?当然,再写是可以的,只不过会使程序显得过为复杂而已,比如Instrument类有多个子类,那么就要写多个方法,显然没有这种单一的方法显得高效。因此只要我们记好这种特性那么就会使程序变得更加简洁。
二、转机
考虑一个新的问题,当我们有多个子类的时候,编译器是怎样知道我们传递给基类引用的参数是哪个子类呢?比如我们把刚才的程序做个修改,再增加一个子类:
package com.chenxyt.java.practice;
class Instrument{
public Instrument() {
//---
}
public void print(){
System.out.println("Instrument-----:");
}
}
class Wind extends Instrument{
public void print(){
System.out.println("Wind-----:");
}
}
class Rain extends Instrument{
public void print(){
System.out.println("Rain-----");
}
}
public class Music{
public static void play(Instrument i){
i.print();
}
public static void main(String[] args) {
Rain rain = new Rain();
play(rain);
}
}
运行结果如下:
上述代码中,不同的子类与基类都有相同的方法(返回值、方法名、参数列表都相同),但是方法体内部不相同,这种操作叫做方法的重写或者方法的覆盖,即子类覆盖了父类方法的实现,当参数传递为子类对象的引用时,虽然看似调用了父类的这个方法,但是实际上由于动态绑定调用了子类的方法,实现了不同的功能。这也就是面向对象编程中多态的意义所在。而对于调用方法来说,比如上述代码中的Music.play来说并没有什么变化,也就是子类方法重写的改动,对方法调用来说没有什么影响,换句话说就是多态是一项让程序员“将改变的事物与不变的事物分离开来”的重要技术!
package com.chenxyt.java.practice;
public class PrivateOverride{
private void f(){
System.out.println("private void f");
}
public static void main(String[] args) {
PrivateOverride po = new Derived();
po.f();
}
}
class Derived extends PrivateOverride{
public void f(){
System.out.println("public void f");
}
}
我们期望Derived类重写了f方法,但是实际结果并不是这样,因为f方法为private方法,因此它不能被重写,这种情况子类中的f方法被当做了新的方法。虽然编译器没有报错,但是实际结果也不是预期那样,解决这种问题的办法是避免子类中的方法与父类的方法重名。
实际上只有普通的方法调用可以是多态的,对于域和静态方法都不是多态的。域是在访问的时候编译期进行解析的。
package com.chenxyt.java.practice;
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 getSuperField(){
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.GetSuperField" + sub.getSuperField());
}
}
在本例中为sub.field域和super.filed域分配了不同的内存空间,也就是对于sub来说他有两个field值,一个是它本身的field值,另一个是来自super继承的值,而在sub引用域field时使用的并非是来自super的值,而是它自己本身的默认值。在实际工作中,这种问题基本不会发生,避免问题出现的有效做法是将基类与子类的域分别起不同的名字。
静态方法由于只与类有关,而不与对象牵连,因此它不存在多态的形式。
三、构造器和多态
构造器的调用顺序在第五章和第七章都已经讨论过了,构造器区别于其它普通的方法,它是隐式的static函数,所以它不存在多态性。这里我们再次说一下构造器的调用过程,尤其是在复杂的继承与多态的情况下。
package com.chenxyt.java.practice;
class First{
public First(){
System.out.println("First---");
}
}
class Second{
public Second(){
System.out.println("Second---");
}
}
class Third{
public Third(){
System.out.println("Third---");
}
}
class Fourty extends Third{
public Fourty(){
System.out.println("Fourty---");
}
}
class Fifty extends Fourty{
public Fifty(){
System.out.println("Fifty---");
}
}
public class Sixty extends Fifty{
public Sixty(){
System.out.println("Sixty---");
}
First first = new First();
Second second = new Second();
public static void main(String[] args) {
new Sixty();
}
}
前一章说到过,类的加载过程是从上向下的,也就是它会去找到最根部的那个类,然后进行加载。如上程序Sixty继承了Fifty类,Fifty类继承了Fourty类,Fourty类继承了Third类,所以最先加载的构造器是Third类,当所有构造器都加载完成后,会加载子类中的成员变量,然后最后加载子类的构造函数。
四、协变返回类型
package com.chenxyt.java.practice;
class Father{
public String toString(){
return "Father";
};
}
class Son extends Father{
public String toString(){
return "Son";
}
}
class Mill{
Father process(){
return new Father();
}
}
class wheatMill extends Mill{
Son process(){
return new Son();
}
}
public class Test{
public static void main(String[] args) {
Mill m = new Mill();
Father f = m.process();
System.out.println(f);
m = new wheatMill();
f = new Son();
System.out.println(f);
}
}
五、用继承进行设计
六、总结
多态是面向对象“封装”、“继承”、“多态”三大特性之一,理解多态的特性能够更好的设计程序,同时,掌握程序初始化加载的过程能够更好的理解程序,这也是重中之重。