多态
向上转型
-
当左边的变量的类型(父类) > 右边对象/变量的类型(子类),我们就称为向上转型
-
此时,编译时按照左边变量**(父类)的类型处理,就只能调用父类中有的变量和方法**,不能调用子类特有的变量和方法了
-
但是,运行时,仍然是对象本身的类型
-
此时,一定是安全的,而且也是自动完成的
-
-
定义
父类类型 变量名 = 子类对象;
父类类型:指子类对象继承的父类类型,或者实现的父接口类型。
例如:
class Person{
private String name;
private int age;
Person(String name, int age){//有参构造
this.name = name;
this.age = age;
}
public void speak(){
System.out.println(name + "说:我今年" + age);
}
}
class Man extends Person{
Man(String name, int age){
super(name,age);
}
}
class Woman extends Person{
Woman(String name, int age){
super(name,age);
}
}
class Test{
public static void main(String[] args){
Person[] arr = new Person[2];
arr[0] = new Man("张三",23);
arr[1] = new Woman("如花",18);
for(int i=0; i<arr.length; i++){
arr[i].speak();
}
System.out.println("------------------------");
show(new Man("张三",23));
show(new Woman("如花",18));
}
public static void show(Person p){
p.speak();
}
}
编译时类型与运行时类型不一致问题
- 编译时,看“父类”,只能调用父类声明的方法,不能调用子类扩展的方法;
- 运行时,看“子类”,一定是执行子类重写的方法体;
多态实例
代码如下:
定义父类:
public class Animal {
public void eat(){
System.out.println("吃~~~");
}
}
定义子类:
class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
public void catchMouse(){
System.out.println("抓老鼠");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
}
定义测试类:
public class Test {
public static void main(String[] args) {
// 多态形式,创建对象
Animal a1 = new Cat();
// 调用的是 Cat 的 eat
a1.eat();
//a1.catchMouse();//错误,catchMouse()是子类扩展的方法,父类中没有
/*
多态引用,编译时,看“父类”,只能调用父类声明的方法;
运行时,看“子类”,一定是执行子类重写的方法体;
*/
// 多态形式,创建对象
Animal a2 = new Dog();
// 调用的是 Dog 的 eat
a2.eat();
}
}
多态的应用
多态应用在形参实参
父类类型作为方法形式参数,子类对象为实参。
代码如下:
public class Test01 {
public static void main(String[] args) {
showAnimalEat(new Dog()); //形参 Animal a,实参new Dog()
//实参给形参赋值 Animal a = new Dog() 多态引用
showAnimalEat(new Cat());//形参 Animal a,实参new Cat()
//实参给形参赋值 Animal a = new Cat() 多态引用
}
/*
* 设计一个方法,可以查看所有动物的吃的行为
* 关注的是所有动物的共同特征:eat()
* 所以形参,设计为父类的类型
* 此时不关注子类特有的方法
*/
public static void showAnimalEat(Animal a){
a.eat();
// a.catchMouse();//错误,因为a现在编译时类型是Animal,只能看到父类中有的方法
}
}
多态应用在数组
数组元素类型声明为父类类型,实际存储的是子类对象
public class Test02 {
public static void main(String[] args) {
/*
* 声明一个数组,可以装各种动物的对象,看它们吃东西的样子
*/
Animal[] arr = new Animal[2]; //此时不是new Animal的对象,而是new Animal[]的数组对象
//在堆中开辟了长度为5的数组空间,用来装Animal或它子类对象的地址
arr[0] = new Cat();//多态引用 左边arr[0] 是Animal类型,右边是new Cat()
//把Cat对象,赋值给Animal类型的变量
arr[1] = new Dog();
for (int i = 0; i < arr.length; i++) {
arr[i].eat();
// arr[i].catchMouse();错误,因为arr[i]现在编译时类型是Animal,只能看到父类中有的方法
}
}
}
多态应用在返回值
方法的返回值类型声明为父类的类型,实际返回值是子类对象
public class Test03 {
public static void main(String[] args) {
Animal c = buy("猫咪");
System.out.println(c.getClass());
c.eat();
}
/*
* 设计一个方法,可以购买各种动物的对象,此时不确定是那种具体的动物
*
* 返回值类型是父类的对象
*
* 多态体现在 返回值类型 Animal ,实际返回的对象是子类的new Cat(),或new Dog()
*/
public static Animal buy(String name){
if("猫咪".equals(name)){
return new Cat();
}else if("小狗".equals(name)){
return new Dog();
}
return null;
}
}
向上转型与向下转型
-
向上转型:当左边的变量的类型(父类) > 右边对象/变量的类型(子类),我们就称为向上转型
- 此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法了
- 但是,运行时,仍然是对象本身的类型
- 此时,一定是安全的,而且也是自动完成的
-
向下转型:当左边的变量的类型(子类)<右边对象/变量的类型(父类),我们就称为向下转型
- 此时,编译时按照左边变量的类型处理,就可以调用子类特有的变量和方法了
- 但是,运行时,仍然是对象本身的类型
- 此时,不一定是安全的,需要使用(类型)进行强制类型转换
- 不是所有通过编译的向下转型都是正确的,可能会发生ClassCastException,为了安全,可以通过isInstanceof关键字进行判断
-
举例
public class Test { public static void main(String[] args) { // 向上转型 Animal a = new Cat(); a.eat(); // 调用的是 Cat 的 eat // 向下转型 Cat c = (Cat)a; //告诉JVM我是一个Cat类对象 c.catchMouse(); // 调用的是 Cat 的 catchMouse // 向下转型 //Dog d = (Dog)a; //这段代码可以通过编译,但是运行时,却报出了ClassCastException //这是因为,明明创建了Cat类型对象,运行时,当然不能转换成Dog对象的。这两个类型并没有任何继承关系, //不符合类型转换的定义。 //d.watchHouse(); // 调用的是 Dog 的 watchHouse Animal a2 = new Animal(); // Dog d2 = (Dog)a2;//这段代码可以通过编译,但是运行时,却报出了ClassCastException // d2.watchHouse(); // 调用的是 Dog 的 watchHouse } }
多态引用时关于成员变量与成员方法引用的原则
1、成员变量:只看编译时类型
如果直接访问成员变量,那么只看编译时类型
package com.gec.test05;
/*
* 成员变量没有重写,只看编译时类型
*/
public class TestExtends {
public static void main(String[] args) {
Son s = new Son();
System.out.println(s.a);//2,因为son的编译时类型是Son
System.out.println(((Father)s).a);//1 ((Father)son)编译时类型,就是Father
Father s2 = new Son();
System.out.println(s2.a);//1 son2的编译时类型是Father
System.out.println(((Son)s2).a);//2 ((Son)son2)编译时类型,就是Son
}
}
class Father{
int a = 1;
}
class Son extends Father{
int a = 2;
}
非虚方法:只看编译时类型
在Java中的非虚方法有三种:
- static方法、私有方法、构造方法、父类方法(子类没有)、final修饰的方法
1、由invokestatic指令调用的static方法,这种方法在编译时确定在运行时不会改变。
javap -v .\Test.class
2、由invokespecial指令调用的方法,这些方法包括私有方法,实例构造方法和父类方法,这些方法也是在编译时已经确定,在运行时不会再改变的方法
3、由final关键字修饰的方法。虽然final方法是由invokevirtual指令进行调用的,但是final修饰的方法不能够进行在子类中进行覆盖,所以final修饰的方法是不能够在运行期进行动态改变的。在java语言规范中明确规定final方法就是非虚方法。
package com.gec.test09;
public class Test {
public static void main(String[] args) {
Father f = new Son();
f.test();//只看编译时类型
f.method();
}
}
class Father{
public static void test(){
System.out.println("Father.test");
}
public void method(){
System.out.println("Father.method");
fun();//看运行时类型
other();//看编译时类型
}
public void fun(){
System.out.println("Father.fun");
}
private void other(){
System.out.println("Father.other");
}
}
class Son extends Father{
public static void test(){
System.out.println("son");
}
public void fun(){
System.out.println("Son.fun");
}
private void other(){
System.out.println("Son.other");
}
}
小贴士:
静态方法不能被重写
调用静态方法最好使用“类名.”
虚方法:静态分派与动态绑定
在Java中虚方法是指在编译阶段和类加载阶段都不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法。
当我们通过“对象.方法”的形式,调用一个虚方法,我们要如何确定它具体执行哪个方法呢?
(1)静态分派:先看这个对象的编译时类型,在这个对象的编译时类型中找到最匹配的方法
最匹配的是指,实参的编译时类型与形参的类型最匹配
(2)动态绑定:再看这个对象的运行时类型,如果这个对象的运行时类重写了刚刚找到的那个最匹配的方法,那么执行重写的,否则仍然执行刚才编译时类型中的那个方法
(1)示例一:没有重载有重写
abstract class Animal {
public abstract void eat();
}
class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
}
public class Test{
public static void main(String[] args){
Animal a = new Cat();
a.eat();
}
}
如上代码在编译期间先进行静态分派:此时a的编译时类型是Animal类,所以去Animal类中搜索eat()方法,如果Animal类或它的父类中没有这个方法,将会报错。
而在运行期间动态的在进行动态绑定:a的运行时类型是Cat类,而子类重写了eat()方法,所以执行的是Cat类的eat方法。如果没有重写,那么还是执行Animal类在的eat()方法
(2)示例二:有重载没有重写
class MyClass{
public void method(Father f) {
System.out.println("father");
}
public void method(Son s) {
System.out.println("son");
}
public void method(Daughter f) {
System.out.println("daughter");
}
}
class Father{
}
class Son extends Father{
}
class Daughter extends Father{
}
public class TestOverload {
public static void main(String[] args) {
Father f = new Father();
Father s = new Son();
Father d = new Daughter();
MyClass my = new MyClass();
my.method(f);//father
my.method(s);//father
my.method(d);//father
}
}
如上代码在编译期间先进行静态分派:因为my是MyClass类型,那么在MyClass类型中寻找最匹配的method方法。
而在运行期间动态的在进行动态绑定:即确定执行的是MyClass类中的method(Father f)方法,因为my对象的运行时类型还是MyClass类型。
有些疑问,不是应该分别执行method(Father f)、method(Son s)、method(Daughter d)吗?
因为此时实参f,s,d编译时类型都是Father类型,因此method(Father f)是最合适的。