@[toc]
为什么要有多态性
package 封装继承多态.多态性练习基本使用;
// 多态性的使用举例
public class AnimalTest {
public static void main(String[] args) {
AnimalTest test = new AnimalTest();
test.func(new Dog());
test.func(new Cat());
}
// 如果使用了多态性,就只用写这一个方法
// 每个对象的eat、shout方法的不同取决于new的子类的差异
// 如:Animal animal = new Dog();
// 如果Dog()换成Cat(),则下面eat、shout方法执行的结果就又不一样了
public void func(Animal animal){
animal.eat();
animal.shout();
}
// 如果没有多态性,就会写很多如下的方法,去调用
// 因为这些形参是写死的,是啥类型就得写啥
public void func(Dog dog){
dog.eat();
dog.shout();
}
// 要想减少这样冗余的代码,就要用多态
public void func(Cat cat){
cat.eat();
cat.shout();
}
}
class Animal{
public void eat(){
System.out.println("动物,进食");
}
public void shout(){
System.out.println("动物:叫");
}
}
class Dog extends Animal{
public void eat(){
System.out.println("狗吃骨头");
}
public void shout() {
System.out.println("汪!汪!汪!");
}
}
class Cat extends Animal{
public void eat(){
System.out.println("猫吃鱼");
}
public void shout() {
System.out.println("喵!喵!喵!");
}
}
多态性的理解与使用
理解多态性
可以理解为一个事物的多种状态。
何为多态性
对象的多态性:父类
的引用指向子类
的对象(子类
的对象赋给父类
的引用)
多态的使用(又叫虚拟方法调用)
有了对象的多态性后,我们在编译期
,只能调用父类
中声明的方法;但在运行期
,我们实际执行的是子类
重写父类的方法。
总结:编译,看左边;运行,看右边。
多态性的使用前提
- 类的继承关系
没有继承关系就没有子父类,进而没有多态性。
- 方法的重写
不重写就相当于造了个新方法,跟多态没关系。
用代码直观理解
现在我们已经造好三个类了,分别是:Person类、Man类、Woman类
对象的多态性(再次强调!)
子类的对象赋给父类的引用(这句好理解);父类的引用指向子类的对象
正常来说,new对象应该这样做:
Person p1 = new Person();
但多态性是这样:
Person p2 = new Man();
Person p3 = new Woman();
显然new Man()是创建了子类Man的对象,new Woman()是创建了子类Woman的对象。它们都分别赋给了父类的引用类型变量p2和p3。
这就是对象的多态性!
多态的使用:
当调用子父类同名同参数的方法时,实际执行的是子类重写父类的方法 ——这又叫虚拟方法调用。
p2.eat();
p2.walk();
- 编译看左:在写代码时,当写出
p2.
时,点儿出来的方法全是父类中的方法。即 写代码(编译)时,看等号左边的父类结构来写程序。 - 运行看右:程序运行时,真正执行的是子类重写后的方法。即 运行时,看等号右边的子类结构来判断执行结果。
多态性的范围
对象的多态性,只适用于方法,不适用于属性 (编译和运行都看左边)
多态性不适用于属性
之前多态性说的都是关于方法,为什么不说属性呢?因为属性不在多态性使用范围内!!!
重写:
- 方法:若子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中。
- 属性:对于属性则不存在这样的现象,即使子类里定义了与父类完全相同的属性,这个属性依然不可能覆盖父类中定义的属性。
编译运行时:
- 方法:编译看左,运行看右
- 属性:编译运行都看左
虚拟方法调用
正常的方法调用
等号左右两边类相同
Person p = new Person();
p.walk();
Student s = new Student();
s.walk();
虚拟方法调用(多态情况下)
子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法调用,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。
Person p = new Student();
p.walk(); // 调用Student类(子类)的walk方法
编译时类型和运行时类型
编译时 p为 Person类型(等号左边父类),而方法的调用是在运行时确定的,所以调用的是Student类(等号右边子类)的walk方法。—— 动态绑定
方法的重载与重写
1、二者的定义细节
看我之前的文章!
重载:Java学习笔记(七):面向对象② —— 类与对象的结构
2、从编译和运行的角度看
重载,是指允许存在多个同名方法,而这些方法的参数不同。编译器根据方法们不同的参数列表,对同名的方法进行区分。对于编译器而言,这些同名方法就成了不同的方法。它们的调用地址在编译期就绑定了。Java的重载是可以包括父类和子类的,即子类可以重载父类的同名但不同参数的方法。
所以,对于重载而言,在方法调用之前,编译器就已经确定了所要调用的方法,这称为早绑定
或静态绑定
;
而对于多态,只有等到方法调用的那一刻,解释运行器才会确定所要调用的具体方法,这称为晚绑定
或动态绑定
。
引用一句Bruce Eckel的话:“不要犯傻,如果它不是晚绑定,它就不是多态。”
强制类型转换(父类数据类型转子类数据类型)
为什么要有强制类型转换
有了对象的多态性后,内存中实际上是加载了子类特有的属性和方法,但是由于变量声明为父类类型,导致编译时,只能调用父类中声明的属性和方法。子类特有的属性和方法不能调用。
Person p = new Man();
此时 p对象在编译时只能写 Person父类的方法,子类Man特有的方法,如:站着尿尿,不能调用。
如何才能调用子类特有的属性和方法?
向下转型:使用强制类型转换
// 把 Person类型的p,强制转换成了Man类型
Man m = (Man) p;
这样就能用Man子类特有的方法,如:m.站着尿尿();了。
异常情况:
之前刚才的强转,p 已由Person类型转为Man类型,如果再强转
Woman w = (Woman)p;
会出现 ClassCastException异常
这是由于强转只能发生在父类与子类之间,同级别的两个子类间不能强转。
instanceof关键字
instanceof关键字就是用来防止上面说的ClassCastException异常的。
格式:
a instanceof A
判断对象a是否是类A的实例对象或者A子类的实例对象。如果是,返回true;如果不是,返回false。
如果a所属的类与A类连子父类关系都没有,那编译就报错了。
使用情境:
为了避免在向下转型时出现 ClassCastException的异常,我们在向下转型之前,先进行 instanceof 的判断,一旦返回true,就进行向下转型;如果返回false,不进行向下转型。
举一反三:
如果 a instanceof A 返回true,且类B是类A的父类,则a instanceof B也返回true。
代码举例:
// if判断为false,不能强转
if (p instanceof Woman) {
// 这不会执行
Woman w = (Woman) p;
w.goShopping();
System.out.println("**********Woman*********");
}
// if判断为true,可以强转
if (p instanceof Man) {
// 会执行
Man m = (Man) p;
m.earnMoney();
System.out.println("*********Man************");
}