面向对象特征-多态
# 前言
[TOC]
面向对象的三大特征:
封装 (
Encapsulation
)继承 (
Inheritance
)多态 (
Polymorphism
)
接下来我们学习多态,为了方便通过代码学习,先定义三个类。
package polymorphism;
public class Person {
String name;
int age;
public void eat() {
System.out.println("人:吃饭");
}
public void walk() {
System.out.println("人:走路");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
package polymorphism;
public class Man extends Person {
boolean isSmoking;
public void earnMoney() {
System.out.println("男人负责挣钱养家");
}
@Override
public void eat() {
System.out.println("男人多吃肉,长肌肉");
}
@Override
public void walk() {
System.out.println("男人走路,六亲不认");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package polymorphism;
public class Woman extends Person {
boolean isBeauty;
@Override
public void eat() {
System.out.println("女人少吃,为了减肥");
}
@Override
public void walk() {
System.out.println("女人窈窕的走路");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
多态是面向对象编程语言中,继数据抽象和继承之外的第三个重要特性。
# 什么是多态?
多态是同一个行为具有多个不同表现形式或形态的能力。
多态就是同一个接口,使用不同的实例而执行不同操作。
多态性,是面向对象中最重要的概念,在Java中的体现:
什么是多态?
对象的多态性:父类的引用指向子类的对象
可以直接应用在抽象类和接口上。
//对象的多态性:父类的引用指向子类的对象
Person p2 = new Man();
Person p3 = new Woman();
2
3
# 虚拟方法调用
多态的使用:其实就是虚拟方法调用(
Virtual Method Invocation
)
当调用子父类同名同参数的方法时,实际执行的是子类重写父类的方法。也叫虚拟方法调用。
//对象的多态性:父类的引用指向子类的对象
Person p2 = new Man();
Person p3 = new Woman();
p2.eat();
p3.eat();
2
3
4
5
男人多吃肉,长肌肉
女人少吃,为了减肥
2
有了对象的多态性以后,我们在编译期,只能调用父类中声明的方法,但在运行期,实际执行的是子类重写父类的方法。
子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。
# 为什么要有多态?
假设存在一个公共的方法
class Test{
public static void main(String[] args){
func(new Man());
func(new Woman())
}
public static void func(Person p){
p.eat();
p.walk();
}
}
2
3
4
5
6
7
8
9
10
11
实际执行的是子类重写父类的方法。
思考:这样做有什么好处呢?
假设存在n种人,如果不使用多态,我必须重载n个func方法,每个方法参数一种数据类型。
但是有了多态之后,我只需要一个func方法就可以了,参数列表填他们的父类。这就是多态的好处。
在Java API中,equals,也是使用多态,只要是Object的子类,都可以使用。
多态的作用是:消除类型之间的耦合,提高了代码的通用性,常称作接口重用。
# 注意
一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法
先来看看方法的例子:
Man
类明明有一个earnMoney()
的方法,但是却不能使用,那是因为编译器认为我们所写的Person
类,而不是Man
类,但是运行时实际执行还是Man
类中的方法。
Java引用变量有两个类型:编译时类型和运行时类型。
编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。
简称:编译时,看左边;运行时,看右边
若编译时类型和运行时类型不一致,就出现了对象的多态性(Polymorphism)
多态情况下:
- “看左边”:看的是父类的引用(父类中不具备子类特有的方法)
- “看右边”:看的是子类的对象(实际运行的是子类重写父类的方法)
对象的多态性,只适用于方法,不适用于属性
再来看看属性的例子:
1、先去Person类中添加一个属性
int id = 1001;
2、再去Man类中添加一个属性
int id = 1002;
3、再去使用多态访问id,看看打印的是多少?
Person p2 = new Man();
System.out.println(p2.id);
2
结果是1001。那是因为
一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法。
一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象是什么意思?
声明一个父类的引用 = 实例化子类对象。
父类 变量名 = new 子类();
对象的多态性,只适用于方法,不适用于属性。
这个2个例子很好的反证了这两句话。
# 方法调用绑定 (opens new window)
将一个方法调用和一个方法主体关联起来称作绑定。若绑定发生在程序运行前(如果有的话,由编译器和链接器实现),叫做前期绑定。你可能从来没有听说这个术语,因为它是面向过程语言不需选择默认的绑定方式,例如在 C 语言中就只有前期绑定这一种方法调用。
上述程序让人困惑的地方就在于前期绑定,因为编译器只知道一个 Instrument 引用,它无法得知究竟会调用哪个方法。
解决方法就是后期绑定,意味着在运行时根据对象的类型进行绑定。后期绑定也称为动态绑定或运行时绑定。当一种语言实现了后期绑定,就必须具有某种机制在运行时能判断对象的类型,从而调用恰当的方法。也就是说,编译器仍然不知道对象的类型,但是方法调用机制能找到正确的方法体并调用。每种语言的后期绑定机制都不同,但是可以想到,对象中一定存在某种类型信息。
# 动态绑定
//对象的多态性:父类的引用指向子类的对象
Person p = new Man();
p.eat();
2
3
我们已经知道,上面这两行代码实际执行的是子类重写的方法。
因为编译器只知道一个引用
Person p;//是创建一个引用,而且还Person类引用,具体指向哪个对象,编译器不知道。
编译时p为Person类型,而方法的调用是在运行时确定的,所以调用的是Man类的eat()方法。——动态绑定
# 面试题1
多态是编译时行为还是运行时行为?
如何证明?
//多态是运行时行为
Person p = new Man();
p.eat();
2
3
子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的
只有运行起来,才能确定执行的具体哪种方法。
# 面试题2
add方法是重写吗?输出结果是?
package polymorphism;
public class InterviewTest2 {
public static void main(String[] args) {
Base base = new Sub();
base.add(1, 2, 3);
}
}
class Base {
public void add(int a, int... arr) {
System.out.println("base");
}
}
class Sub extends Base {
public void add(int a, int[] arr) {
System.out.println("sub");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
是重写,因为他们本质上是同一个方法,只不过声明方式不同而已。
JavaSE 5.0 中提供了Varargs
(variable number of arguments)机制,允许直接定义能和多个实参相匹配的形参。从而,可以用一种更简单的方式,来传递个数可变的实参。
//JDK 5.0以前:采用数组形参来定义方法,传入多个同一类型变量
public static void test(int a ,String[] books);
2
//JDK5.0:采用可变个数形参来定义方法,传入多个同一类型变量
public static void test(int a ,String...books);
2
结果是sub
。
从内存中解释
因为实例化的是Sub
类,所以肯定是输出sub
。
从多态解释
子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。
# 面试题3
输出结果是?
package polymorphism;
public class InterviewTest2 {
public static void main(String[] args) {
Base base = new Sub();
base.add(1, 2, 3);
}
}
class Base {
public void add(int a, int... arr) {
System.out.println("base");
}
}
class Sub extends Base {
@Override
public void add(int a, int[] arr) {
System.out.println("sub_1");
}
public void add(int a, int b, int c) {
System.out.println("sub_2");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
结果还是sub_1
,因为在动态情况下,会调用子类同名同参数的方法。
子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。
# 面试题4
下面输出结果是什么?为什么?
package polymorphism;
public class InterviewTest2 {
public static void main(String[] args) {
Base base = new Sub();
Sub s = (Sub)base;
s.add(1,2,3);
}
}
class Base {
public void add(int a, int... arr) {
System.out.println("base");
}
}
class Sub extends Base {
@Override
public void add(int a, int[] arr) {
System.out.println("sub_1");
}
public void add(int a, int b, int c) {
System.out.println("sub_2");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
输出结果是sub_2
,因为强转类型后,不是多态的情况了,而且从方法优先级来判断,第二个方法已经确定了参数数量,第一个方法还没确定参数个数,所以会优先调用第二个方法。
# 方法的重载与重写
从编译和运行的角度看:
重载,是指允许存在多个同名方法,而这些方法的参数不同。编译器根据方法不同的参数表,对同名方法的名称做修饰。对于编译器而言,这些同名方法就成了不同的方法。它们的调用地址在编译期就绑定了。Java的重载是可以包括父类和子类的,即子类可以重载父类的同名不同参数的方法。
所以:对于重载而言,在方法调用之前,编译器就已经确定了所要调用的方法,这称为“早绑定”或“静态绑定”;而对于多态,只有等到方法调用的那一刻,解释运行器才会确定所要调用的具体方法,这称为“晚绑定”或“动态绑定”。
引用一句Bruce Eckel的话:“不要犯傻,如果它不是晚绑定,它就不是多态。”
Bruce Eckel:《C++编程思想》作者。
# 总结
多态的作用
消除类型之间的耦合,提高了代码的通用性,常称作接口重用。
多态存在的三个必要条件
- 类的继承关系
- 方法重写
- 父类引用指向子类对象:
Father f = new Child();
成员方法
编译时:要查看引用变量所声明的类中是否有所调用的方法。
运行时:调用实际new的对象所属的类中的重写方法。
成员变量
不具备多态性,只看引用变量所声明的类。