多态在Java中是一个很重要的概念,其成立的条件有三个:
- 子类继承父类;
- 子类重写父类的方法;
- 父类引用指向子类对象。
前两条大家都清楚,第三条父类引用指向子类对象我多说一句。Java中数据分为基本类型和引用类型,引用类型有引用和对象,存放在栈中的为引用或者有时候叫句柄,存放在堆中的我们称为对象。
下面我们用例子来说明Java中多态的概念:
我们定义两个类,Person 和 Student 类,让Student 继承Person类
public class Person { int var = 100; public void a() { System.out.println("class Person , method a()"); } }
public class Student extends Person { int var = 200; /* * public void a() { * System.out.println("class Student , method a()"); } */ public void b() { System.out.println("class Student , method b()"); } }
这里我们先不重写父类的 a() 方法,接下来我们让父类(Person)的引用指向子类(Student)的对象
public class Test { public static void main(String[] args) { Person p = new Student(); // 相当于基本类型的隐式转换 p.a(); } }
在这里我们用 p 调用 a() 方法时,执行的显然是父类的 a() 方法,因为子类没有 a() 方法,输出
class Person , method a()
如果我们将Student类中 a() 方法的注释去掉(第5行至第8行),在 p 调用 a() 方法的时候执行的为子类重写之后的 a() 方法,输出
class Student , method a()
但是,如果这里我们在 main() 方法中调用 p.b() 方法,答案是编译器会报错。那为什么 p 可以调用子类重写的 a() 方法,却不能调用子类的 b() 方法呢?我们可以用天一时代老师的一个形象的例子来说明这个问题。
我们开头说引用类型分为引用和对象两个部分,可以把引用比喻成电视的遥控器,把对象就比作成电视节目。那么 Person p = new Student(); 这句话是不是就相当于用一个父类的或者说是先前的,旧的遥控器来控制一台子类的或者说是新的电视啊。对,就是这样。这么一来,如果说原来的旧电视只有一个 a() 节目的话,我们旧的遥控器就只需要一个按钮就可以控制这台旧电视了,如果说在新电视中不去更新 a() 这个旧的节目,即不重写 a() 方法,那么显然遥控器一打开旧的电视看到的就是旧的 a() 节目,如果说新电视更新了旧的 a() 节目那么自然父类的 a() 节目就被取代了。这时,如果这里我们在新的电视中增加了一个节目 b() 节目,我们想看 b() 节目,能不能看呢,当然不能,因为我们用的是旧的遥控器啊,它只有一个按钮,换不了台啊!
那现在如果我们一定要看新增加的节目怎么办呢?答案就是我们需要换新的遥控器,即 Student s = (Student)p; 这样我们把旧遥控器换成了新遥控器,当然可以看新增加的节目了。
public class Test { public static void main(String[] args) { Person p = new Student(); // 相当于基本类型的隐式转换 // p.a(); Student s = (Student) p; // 向下转型(强制转换) s.b(); } }
现在,我们增加一个类
public class TestBinding { public void test(Person p) { p.a(); } }
这样我们在 main() 方法中这样调用时,能否知道执行的 a() 方法是父类的方法还是子类的方法?
public class Test { public static void main(String[] args) { TestBinding t = new TestBinding(); t.test(p); } }
答案是不知道,因为我们不知道传进去的是父类的对象还是子类的对象(不知道是新电视还是旧电视)。这里,如果我们传进去父类对象
public static void main(String[] args) { TestBinding t = new TestBinding(); t.test(new Person()); }
执行的就是父类的 a() 方法,因为是旧电视,如果传进去的是子类对象,则执行子类的 a() 方法。就是说我们要用哪个方法不是在编译期的时候决定的,而是在运行期间决定的,这种运行期绑定就叫动态绑定,或者叫后期绑定,或者叫多态!
最后,我们来简单说一下另一种绑定------前期绑定。我们在TestBinding类中增加一行代码,来打印类中的属性
public class TestBinding { public void test(Person p) { p.a(); System.out.println(p.var); // 新增 } }
那么在main() 方法中传入Person对象和Student对象的时候打印出来的属性值会是多少呢?试过之后我们知道打印的都是 100, 即父类的属性值。原因是Java中属性和静态方法都是在编译期绑定的,即前期绑定。至于静态方法的情况,读者可以自己验证一下。