什么是多态
面向对象的三大特性:封装(encapsulation)、继承(Inheritance)和多态(polymorphism)。从一定角度来看,封装和继承几乎都是为多态而准备的。
多态的定义:指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用)实现多态的技术称为:动态绑定(dynamic binding),是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。
多态的作用:消除类型之间的耦合关系。现实中,关于多态的例子不胜枚举。比方说按下 F1 键这个动作,如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;如果当前在 Word 下弹出的就是 Word 帮助;在 Windows 下弹出的就是 Windows 帮助和支持。同一个事件发生在不同的对象上会产生不同的结果。
多态的两种表现形式
多态有两种表现形式:重载和覆盖。
重载(Overload)
首先说重载(overload),是发生在同一类中。与什么父类子类、继承毫无关系。
void foo(String str);
void foo(int number);
标识一个函数除了函数名外,还有函数的参数(个数和类型)。也就是说,一个类中可以有两个或更多的函数,叫同一个名字而他们的参数不同。
他们之间毫无关系,是不同的函数,只是可能他们的功能类似,所以才命名一样,增加可读性,仅此而已!
覆盖/重写(Override)
再说覆盖(Override),是发生在子类中!也就是说必须有继承的情况下才有覆盖发生。
class Parent {
void foo() {
System.out.println("Parent foo()");
}
}
class Child extends Parent {
void foo() {
System.out.println("Child foo()");
}
}
我们知道继承一个类,也就有了父类了全部方法,如果你感到哪个方法不爽,功能要变,那就把那个函数在子类中重新实现一遍。
这样再调用这个方法的时候,就是执行子类中的过程了。父类中的函数就被覆盖了(当然,覆盖的时候函数名和参数要和父类中完全一样,不然你的方法对父类中的方法就不起任何作用,因为两者是两个函数,毫不关系)。
例子
多态,用一句话来说,就是事物在运行过程中存在不同的状态。多态的存在有三个前提:
- 要有继承关系
- 子类要重写父类的方法
- 父类类型的引用指向子类
我们先定义一个父类Animal
class Animal{
int age = 20;
public void eat(){
System.out.println("动物在吃饭");
}
public void eat2(){
System.out.println("动物在吃饭2");
}
public static void sleep(){
System.out.println("static 动物在睡觉");
}
public void run(){
System.out.println("动物在奔跑");
}
}
再定义一个子类Cat
class Cat extends Animal{
int age = 40;
String name = "Tom";
public void eat(){
System.out.println("猫在吃饭");
}
public void eat2(){
System.out.println("猫在吃饭2");
}
public stastic void sleep(){
System.out.println("static 猫在睡觉");
}
public void catchMouse(){
System.out.println("猫在抓老鼠");
}
}
再来一个测试
class Demo_Test{
public stastic void main(String[] args){
Animal am = new Cat();
am.eat();
am.eat2();
am.sleep();
am.run();
System.out.println(am.age);
//am.catchMouse(); //编译不能通过,于是注释掉了
//System.out.println(am.name); //编译不能通过,于是注释掉了
}
}
先来看看上面是否满足三个条件:
- 子类Cat继承了父类Animal;
- 子类重写了父类的
eat()
、sleep()
方法,其中前者是非静态,后者是静态方法; - 在堆内存中实例化了一个子类Cat对象,并把存在于栈内存中的Animal引用类型变量指向这个对象。
运行结果
猫在吃饭
猫在吃饭2
动物在睡觉
动物在奔跑
20
我们发现:
- 子类重写了父类的非静态成员方法,对应
am.eat()
的输出结果是“猫在吃饭”; - 子类重写了父类的非静态成员方法,且在子类重写这个非静态成员方法时,增加了
@Override
关键字,对应am.eat2()
的输出结果是“猫在吃饭2”; - 子类重写了父类的静态成员方法,对应
am.sleep()
的输出结果是“动物在睡觉”; - 未被子类重写的方法,对应
am.run()
的输出结果是“动物在奔跑”; - 子类重写了父类的成员变量,对应
am.age
的值为 20(即为在父类中的赋值)。
规律
- 当子类重写了父类的非静态成员方法时,通过一个父类类型的引用变量来指向一个子类对象时,调用该重写的非静态成员方法,会执行子类中的重写方法;
- 当子类重写了父类的静态成员方法时,通过一个父类类型的引用变量来指向一个子类对象时,调用该重写的静态成员方法,会执行父类中的重写方法;
- 当子类重写了父类的非静态成员方法时,加不加上
@Override
关键字并不会影响执行结果,但是加上后,编译器会帮我们检查重写方法声明的正确性; - 当子类重写了父类的静态成员方法时,通过一个父类类型的引用变量来指向一个子类对象,无法调用到子类中新定义的方法和字段;
- 当子类重写了父类的成员变量时,通过一个父类类型的引用变量来指向一个子类对象,获取到的该成员变量的值,为该值在父类中赋的值。
多态的缺点
多态也存在缺点,也就是无法尝试调用子类特有的方法,如上面被注释掉的 am.catchMouse(); System.out.println(am.name);
两行代码会编译时报错,因为这是子类特有的成员方法和成员属性。
如果我们需要调用子类的特有成员方法和成员属性,那么应该将这个父类强制转换为子类类型,如
class Demo_Test {
public static void main(String[] args) {
Animal am = new Cat();
am.eat();
am.sleep();
am.run();
// am.catchMouse();
// System.out.println(am.name);
System.out.println(am.num);
System.out.println(am.age);
System.out.println("------------------------------");
Cat ct = (Cat)am;
ct.eat();
ct.sleep();
ct.run();
ct.catchMouse();
}
}
执行强制类型转换Cat ct = (Cat)am
后,这时ct就指向堆里最先创建的Cat对象了,自然能使用Cat类的一切成员方法和成员属性了。这也是多态的魅力,为了使用子类的某些方法不需要重新再开辟内存。以上就是多态中的向下转型。
Reference
- 什么是多态 - https://www.jianshu.com/p/2860bf584c8e
- Polymorphism of Java - http://frankchen.xyz/2017/01/13/Polymorphism-of-Java/