(一)多态的相关定义
-
一个对象变量可以指示多种数据类型的现象称为多态,是同一个行为具有不同的表现现象的能力。
比如玩游戏:按W键 (只是打个比方)
1:在英雄联盟里,是英雄使用第二个技能。
2:在绝地求生里,是让人物前进。 -
在运行时能自动地选择调用哪个方法的现象称为动态绑定。
(二)多态存在的三个条件及优缺点
-
三个条件
1.要有继承关系。
2.子类要重写父类的方法。
3.父类引用指向子类对像。 -
优点
1.消除类型之间的耦合关系
2.可替换性
3.可扩充性
4.接口性
5.灵活性
6.简化性 -
缺点
无法直接访问子类特有成员
(三)案例
- 父类
class Employee {
String name = "员工";
static int age = 25;
public void work() {
System.out.println("员工的工作");
}
public static void sleep() {
System.out.println("员工睡觉了");
}
public void holiday(){
System.out.println("员工放假了");
}
}
- 子类
class Programmer extends Employee { //子类继承了父类
String name = "程序员";
static int age = 90;
String myname = "小明";
public void work() { //重写了方法
System.out.println("小明在敲代码");
}
public static void sleep() {
System.out.println("小明在加班");
}
public void noholiday() { //子类特有属方法
System.out.println("小明在公司度假");
}
}
- 测试类
class Test {
public static void main(String[] args) {
Employee p = new Programmer(); //父类引用指向子类对象
p.work();
p.sleep();
p.holiday();
//以下俩个方法调用就是多态的缺点,无法直接访问子类特有成员
//p.noholiday();
//System.out.println(p.myname);
System.out.println(p.name);
System.out.println(p.age);
}
}
(四)理解方法调用
1.子类Pgrammer重写了父类Employee的非静态成员方法p.work(); 输出结果为:小明在敲代码。
2.子类重写了父类的静态成员方法p.sleep(); 输出结果为:员工睡觉了
3.未被子类重写的父类方法p.holiday();输出结果为:员工放假了
(2)终极分析
- 下面假设调用x.f(param),隐式参数x声明为B类的对象
-
编译器查看对象的声明类型和方法名
有可能存在多个名为f,但参数类型不一样的方法,如f(int)和方法f(String),编译器将会一一列举所有B类中名为f的方法和其超类A中为可访问属性且名为f的方法。
至此编译器获得所有可能被调用的候选方法。 -
接下来查看调用方法时提供的参数类型
如果在所有名为f的方法中存在一个与提供类型完全匹配,就选择这个方法。这个过程被称为重载解析。例如,对于调用x.f(“Hello”)来说,编译器将会挑选f(String),而不是f(int)。由于允许类型转换(int可以转换成double,Programmer可以转换成Employee等等),所有过程很复杂。如果编译器没有找到与参数类型匹配的方法,或者转换后有多个方法与之可以匹配就会报错。
至此编译器已获得需要调用的方法名字和参数类型。 -
.
如果是private,static,final方法或者是构造器,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式为静态绑定。与之对应的是调用的方法依赖x的实际类型,并且在运行中实现动态绑定。上面例子中,p的实际类型为Pragrammer,编译器采用动态方法的方式生成一条调用方法的指令。
-
程序运行,且采用动态绑定调用方法时
虚拟机一定调用与x所引用对象的实际类型最合适的那个方法。假设x的实际类型是B,B是A的子 类,如果B定义方法f(String),就直接调用它;否则在B的超类中寻找f(String),以此类推。
每次调用方法都要进行搜索,时间开销相当大。因此虚拟机预先为每个类创建一个方法表,其中列出了所有方法的签名(签名是方法的名字与参数列表)与实际调用的方法,在调用方法时,虚拟机仅查这个表就行了
(3)调用总结
-
那么我们可以根据以上情况总结出多态成员访问的特点:
1.成员变量
编译看左边(父类),运行看左边(父类)
2.成员方法
编译看左边(父类),运行看右边(子类)。动态绑定
3.静态方法
编译看左边(父类),运行看左边(父类)。
(静态和类相关,算不上重写,所以,访问还是左边的)
只有非静态的成员方法,编译看左边,运行看右边
(五)类型转换
- 向上转型
Employee p = new Programmer(); //向上转型
子类对象Programmer转化为父类对象Employee,向上转型时,这个时候Programmer这个引用 调用的方法是子类方法。子类单独定义的方法会丢失。比如上面Programmer类中定义的noholiday方法,当Employee类引用指向Programmer类实例时是访问不到noholiday方法的,Employee.noholiday会报错。
子类引用不能指向父类对象。Cat c = (Cat)new Animal()这样是不行的。
向上转型的好处:
1.减少重复代码
2.使代码变得简洁
3.提高系统扩展性。
- 向下转型
//向下转型 正确示范
Employee p = new Programmer();
Programmer t = (Prpgrammer)p;
~~~~
//假设Hr类(招聘官)也是继承了Employee,和Programmer一样是Employee类的子类
//向下转型 错误示范
Employee p = new Programmer();
Hr t = (Hr)p;
//向下转型 错误示范
Employee p = new Employee();
Programmer t = (Prpgrammer)p;
第一个不报错是因为Programmer(程序员)本来就是Employee(员工),可以向下转型成员。
第二个报错是因为Hr和Programmer都是同一超类下的不同子类,程序员这种肥宅(逃)怎么可能转换成Hr
第三个报错是因为 p 为Employee对象,不能进行向下转型
想要向下转型,就得先向上转型
这个时候t就能访问到属于自己的专属成员了 如t.noholiday(); t.myname
- 综上所述
只能在继承内层内进行类型转换
可以用instanceof运算符检测能否在转换成功 if(Programmer instanceof Hr)