多态的引入
多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
比如你回家发现桌上有几个杯子里面都装了白酒,从外面看我们不知道这是些什么酒,只有喝了之后才能猜出来是什么酒。你一喝,是剑南春、再喝是五粮液、再喝是酒鬼酒….我们可以描述成如下:
酒 wine1 = 剑南春
酒 wine2 = 五粮液
酒 wine3 = 酒鬼酒
上面表现的的就是多态。剑南春、五粮液、酒鬼酒都是酒的子类,我们通过酒这一父类就能够引用不同的子类,这就是多态。
要理解多态我们就必须要明白什么是“向上转型”。在上面喝酒的例子中,酒(Wine)是父类,剑南春(JNC)、五粮液(WLY)、酒鬼酒(JGJ)是子类。我们定义如下代码:
JNC wine1 = new JNC();
对于这个代码我们非常容易理解就是实例化了一个剑南春的对象,但是这样呢?
Wine wine1 = new JNC();
在这里我们这样理解,这里定义了一个Wine 类型的wine1,它指向JNC对象实例。由于JNC是继承与Wine,所以JNC可以自动向上转型为Wine,所以wine1是可以指向JNC实例对象的。
但是向上转型存在一些缺憾,那就是它必定会导致一些方法和属性的丢失,而导致我们不能够获取它们。所以父类类型的引用可以调用父类中定义的所有属性和方法,对于只存在子类中的方法和属性它就不能使用(只能使用父类有的方法(也就是子类重写父类的方法,子类扩展的、特有的属性方法不能被向上转型的父类引用使用))。
public class Wine {
public void fun1(){
System.out.println("Wine 的 Fun1...");
fun2();
}
public void fun2(){
System.out.println("Wine 的 Fun2...");
}
}
public class JNC extends Wine{
/**
* 子类 重载 父类方法
*
* 属于 JNC子类 特有方法 向上转型后的wine不能调用
*
* 父类中不存在该方法,向上转型后,父类是不能引用该方法的
*/
public void fun1(String string){
System.out.println("JNC 的 Fun1...");
fun2();
}
/**
* 子类 重写 父类方法
*
* 指向子类的父类引用调用fun2时,必定是调用该方法
*/
public void fun2(){
System.out.println("JNC 的 Fun2...");
}
}
public class Test {
public static void main(String[] args) {
Wine wineJNC = new JNC();
wineJNC.fun1();
}
}
-------------------------------------------------
输出结果:
Wine 的 Fun1...
JNC 的 Fun2...
从程序的运行结果中发现,wineJNC.fun1()首先是运行父类Wine中的fun1().然后再运行子类JNC中的fun2()。
分析:在这个程序中子类 JNC 重载 了父类 Wine 的方法fun1(),重写 fun2(),而且 重载 后的fun1(String string)与 fun1()不是同一个方法,由于父类中没有该方法,向上转型后会丢失该方法,所以执行JNC的Wine类型引用wineJNC是不能引用fun1(String string)方法。而子类JNC重写了fun2() ,那么指向JNC的wineJNC引用会调用JNC中fun2()方法。
多态的实现
Java实现多态有三个必要条件:继承、重写、向上转型。
- 继承:在多态中必须存在有继承关系的子类和父类。
- 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
- 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。
对于Java而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。
基于继承的多态
基于继承的实现机制主要表现在父类和继承该父类的一个或多个子类对某些方法的重写,多个子类对同一方法的重写可以表现出不同的行为。
public class Wine {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Wine(){
}
public String drink(){
return "喝的是 " + getName();
}
/**
* 重写toString()
*/
public String toString(){
return null;
}
}
class JNC extends Wine{
public JNC(){
setName("JNC");
}
/**
* 重写父类方法,实现多态
*/
public String drink(){
return "喝的是 " + getName();
}
/**
* 重写toString()
*/
public String toString(){
return "Wine : " + getName();
}
}
class JGJ extends Wine{
public JGJ(){
setName("JGJ");
}
/**
* 重写父类方法,实现多态
*/
public String drink(){
return "喝的是 " + getName();
}
/**
* 重写toString()
*/
public String toString(){
return "Wine : " + getName();
}
public static void main(String[] args) {
//定义父类数组
Wine[] wines = new Wine[2];
//定义两个子类
JNC jnc = new JNC();
JGJ jgj = new JGJ();
//父类引用子类对象,向上转型
wines[0] = jnc;
wines[1] = jgj;
for(int i = 0 ; i < 2 ; i++){
System.out.println(wines[i].toString() + "--"
+ wines[i].drink());
}
}
}
-------------------------------------
输出结果:
Wine : JNC--喝的是 JNC
Wine : JGJ--喝的是 JGJ
在上面的代码中JNC、JGJ继承Wine,并且重写了drink()、toString()方法,程序运行结果是调用子类中方法,输出JNC、JGJ的名称,这就是多态的表现。不同的对象可以执行相同的行为,但是他们都需要通过自己的实现方式来执行,这是因为向上转型。
我们都知道所有的类都继承自超类Object,toString()方法也是Object中方法,当我们这样写时:
Object object = new JGJ();
System.out.println(object.toString());
输出的结果是Wine : JGJ。
Object、Wine、JGJ三者继承链关系是:JGJ—>Wine—>Object。所以我们可以这样说:当子类重写父类的方法被调用时,只有对象继承链中的最末端的方法才会被调用。但是注意如果这样写:
Object object = new Wine();
System.out.println(object.toString());
输出的结果应该是Null,因为JGJ并不存在于该对象继承链中。
基于继承实现的多态总结如下:对于引用子类的父类类型,在处理该引用时,它适用于继承该父类的所有子类,子类对象的不同,对方法的实现也就不同,执行相同动作产生的行为也就不同。
如果父类是抽象类,那么子类必须要实现父类中所有的抽象方法,这样该父类所有的子类一定存在统一的对外接口,但其内部的具体实现可以各异,这样我们就可以使用顶层类提供的统一接口来处理该层次的方法。
基于接口实现的多态
在接口的多态中,指向接口的引用必须是指定这实现了该接口的一个类的实例程序,在运行时,根据对象引用的实际类型来执行对应的方法。
继承都是单继承,只能为一组相关的类提供一致的服务接口。但是接口可以是多继承多实现,它能够利用一组相关或者不相关的接口进行组合与扩充,能够对外提供一致的服务接口。所以它相对于继承来说有更好的灵活性。
向上转型
父类对象指向(引用)子类对象称为向上转型。通俗地说就是是将子类对象转为父类对象。此处父类对象可以是接口。
public class Animal {
public void eat(){
System.out.println("animal eatting...");
}
}
public class Cat extends Animal{
public void eat(){
System.out.println("我吃鱼");
}
}
public class Dog extends Animal{
public void eat(){
System.out.println("我吃骨头");
}
public void run(){
System.out.println("我会跑");
}
}
public class Main {
public static void main(String[] args) {
// 向上转型
Animal animal = new Cat();
animal.eat();
// 向上转型
animal = new Dog();
animal.eat();
}
}
--------
输出结果:
我吃鱼
我吃骨头
--------
这就是向上转型,Animal animal = new Cat();将子类对象Cat转化为父类对象Animal。这个时候animal这个引用调用的方法是子类方法。
注意:
- 向上转型时,子类单独定义的方法会丢失。比如上面Dog类中定义的run方法,当animal引用指向Dog类实例时是访问不到run方法的,animal.run()会报错。
- 子类引用不能指向父类对象。Cat c = (Cat)new Animal()这样是不行的。
向上转型的好处:
- 减少重复代码,使代码变得简洁。
- 提高系统扩展性
向下转型
//还是上面的animal和cat dog
Animal animal1 = new Cat();
Animal animal2 = new Animal();
Cat cat1 = ((Cat) animal1);
cat1.eat(); //输出 我吃鱼
Dog dog = ((Dog) animal1);
dog.eat(); // 报错 : java.lang.ClassCastException:com.chengfan.animal.Cat cannot be cast to com.chengfan.animal.Dog Animal a1 = new Animal();
Cat cat2 = ((Cat) animal2);
c1.eat(); // 报错 : java.lang.ClassCastException:com.chengfan.animal.Animal cannot be cast to com.chengfan.animal.Cat
animal1本身就是Cat对象,所以它可以向下转型为Cat,也当然不能转为Dog,你见过一只猫能就变成一条狗吗?
animal2本身就是Animal对象,它也不能被向下转型为任何子类对象。比如猫是动物,但是动物一定是猫吗?也可以是狗,所以animal2不能转换成其他类,也就是不能向下转型。
注意:
- 向下转型的前提是父类对象指向的是子类对象(也就是说,在向下转型之前,它得先向上转型)
- 向下转型只能转型为本类对象(也就是说,向下转型的类只能是进行向上转型中的子类,猫是不能变成狗的)
- 进行向下转型后的引用就可以调用子类的特殊方法和属性了