面向对象——多态之超详细讲解

多态是继封装、继承之后,面向对象的第三大特性。

1、格式

父类类型 变量名 = 子类对象;

父类类型:指子类对象继承的父类类型,或者实现的父接口类型。

例如:

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();
	}
}

2、编译时类型与运行时类型不一致问题

  • 编译时,看“父类”,只能调用父类声明的方法,不能调用子类扩展的方法;

  • 运行时,看“子类”,一定是执行子类重写的方法体;

代码如下:

定义父类:

public class Animal {  
    public void eat(){
        System.out.println("吃~~~");
    }
}  ```
定义子类:

```java
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();               
    }  
}

3、多态的应用

1、多态应用在形参实参

父类类型作为方法形式参数,子类对象为实参。

代码如下:

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,只能看到父类中有的方法
    }

}
2、多态应用在数组

数组元素类型声明为父类类型,实际存储的是子类对象

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,只能看到父类中有的方法
		}
	}
}

3、多态应用在返回值

方法的返回值类型声明为父类的类型,实际返回值是子类对象

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;
    }
}

4、向上转型与向下转型

首先,一个对象在new的时候创建是哪个类型的对象,它从头至尾都不会变。即这个对象的运行时类型,本质的类型用于不会变。这个和基本数据类型的转换是不同的。

但是,把这个对象赋值给不同类型的变量时,这些变量的编译时类型却不同。

class Animal {  
    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 void watchHouse() {  
        System.out.println("看家");  
    }  
}

class Test{
    public static void main(String[] args){
        Cat a = new Cat();//a编译时类型是Cat
        Animal b = a;//b编译时类型是Animal
        Object c = a;//c编译时类型是Object
        
        //运行时类型
        System.out.println(a.getClass());
        System.out.println(b.getClass());
        System.out.println(c.getClass());
        //以上输出都一样,都是Cat类型
        
       	//a,b,c的编译时类型不同
    	//通过a能调用Cat中所有方法,包括从父类继承的,包括自己扩展的
    	//通过b只能调用Animal类及它的父类有的方法,不能调用Cat扩展的方法
    	//通过c只能调用Object类才有的方法
    }
}

为什么要类型转换呢?

因为多态,就一定会有把子类对象赋值给父类变量的时候,这个时候,在编译期间,就会出现类型转换的现象。

但是,使用父类变量接收了子类对象之后,我们就不能调用子类拥有,而父类没有的方法了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做类型转换。

  • 向上转型:当左边的变量的类型(父类) > 右边对象/变量的类型(子类),我们就称为向上转型

    • 此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法了
    • 但是,运行时,仍然是对象本身的类型
    • 此时,一定是安全的,而且也是自动完成的
  • 向下转型:当左边的变量的类型(子类)<右边对象/变量的类型(父类),我们就称为向下转型

    • 此时,编译时按照左边变量的类型处理,就可以调用子类特有的变量和方法了
    • 但是,运行时,仍然是对象本身的类型
    • 此时,不一定是安全的,需要使用(类型)进行强制类型转换
    • 不是所有通过编译的向下转型都是正确的,可能会发生ClassCastException,为了安全,可以通过isInstanceof关键字进行判断

废话不多说上代码:

public class Test {
    public static void main(String[] args) {
        // 向上转型  
        Animal a = new Cat();  
        a.eat(); 				// 调用的是 Cat 的 eat

        // 向下转型  
        Cat c = (Cat)a;       
        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
    }  
}

为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验,只要用instanceof判断返回true的,那么强转为该类型就一定是安全的,不会报ClassCastException异常。

变量名/对象 instanceof 数据类型 

那么,哪些instanceof判断会返回true呢?

  • 对象/变量的编译时类型 与 instanceof后面数据类型是直系亲属关系才可以比较
  • 对象/变量的运行时类型<= instanceof后面数据类型,才为true

5、多态引用时关于成员变量与成员方法引用的原则

1、成员变量:只看编译时类型

如果直接访问成员变量,那么只看编译时类型

/*
 * 成员变量没有重写,只看编译时类型
 */
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;
}
2、非虚方法:只看编译时类型

在Java中的非虚方法有三种:

1、由invokestatic指令调用的static方法,这种方法在编译时确定在运行时不会改变。

javap -v .\Test.class

2、由invokespecial指令调用的方法,这些方法包括私有方法,实例构造方法和父类方法,这些方法也是在编译时已经确定,在运行时不会再改变的方法

3、由final关键字修饰的方法。虽然final方法是由invokevirtual指令进行调用的,但是final修饰的方法不能够进行在子类中进行覆盖,所以final修饰的方法是不能够在运行期进行动态改变的。在java语言规范中明确规定final方法就是非虚方法。

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");
    }
}
3、虚方法:静态分派与动态绑定

在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)是最合适的。

(3)示例三:有重载没有重写
class MyClass{
	public void method(Father f) {
		System.out.println("father");
	}
	public void method(Son s) {
		System.out.println("son");
	}
}
class Father{
	
}
class Son extends Father{
	
}
class Daughter extends Father{
	
}
public class TestOverload {
	public static void main(String[] args) {
		MyClass my = new MyClass();
		Father f = new Father();
		Son s = new Son();
		Daughter d = new Daughter();
		my.method(f);//father
		my.method(s);//son
		my.method(d);//father
	}
}

如上代码在编译期间先进行静态分派:因为my是MyClass类型,那么在MyClass类型中寻找最匹配的method方法。

而在运行期间动态的在进行动态绑定:即确定执行的是MyClass类中的method(Father f)方法,因为my对象的运行时类型还是MyClass类型。

此时实参f,s,d编译时类型分别是Father、Son、Daughter,而Daughter只能与Father参数类型匹配

(4)示例四:有重载没有重写
class MyClass{
	public void method(Father f) {
		System.out.println("father");
	}
	public void method(Son s) {
		System.out.println("son");
	}
}
class MySub extends MyClass{
	public void method(Daughter d) {
		System.out.println("daughter");
	}
}
class Father{
	
}
class Son extends Father{
	
}
class Daughter extends Father{
	
}
public class TestOverload {
	public static void main(String[] args) {
		MyClass my = new MySub();
		Father f = new Father();
		Son s = new Son();
		Daughter d = new Daughter();
		my.method(f);//father
		my.method(s);//son
		my.method(d);//father
	}
}

如上代码在编译期间先进行静态分派:因为my是MyClass类型,那么在MyClass类型中寻找最匹配的method方法。

而在运行期间动态的在进行动态绑定:即确定执行的是MyClass类中的method(Father f)方法,因为my对象的运行时类型还是MyClass类型。

  • my变量在编译时类型是MyClass类型,那么在MyClass类中,只有method(Father f),method(Son s)方法,

  • f,s,d变量编译时类型分别是Father、Son、Daughter,而Daughter只能与Father参数类型匹配

  • 而在MySub类中并没有重写method(Father f)方法,所以仍然执行MyClass类中的method(Father f)方法

(5)示例五:有重载有重写
class MyClass{
	public void method(Father f) {
		System.out.println("father");
	}
	public void method(Son s) {
		System.out.println("son");
	}
}
class MySub extends MyClass{
	public void method(Father d) {
		System.out.println("sub--");
	}
    public void method(Daughter d) {
		System.out.println("daughter");
	}
}
class Father{
	
}
class Son extends Father{
	
}
class Daughter extends Father{
	
}
public class TestOverloadOverride {
	public static void main(String[] args) {
		MyClass my = new MySub();
		Father f = new Father();
		Son s = new Son();
		Daughter d = new Daughter();
		my.method(f);//sub--
		my.method(s);//son
		my.method(d);//sub--
	}
}

如上代码在编译期间先进行静态分派:因为my是MyClass类型,那么在MyClass类型中寻找最匹配的method方法。

而在运行期间动态的在进行动态绑定:即确定执行的是MyClass类中的method(Father f)方法,因为my对象的运行时类型还是MyClass类型。

  • my变量在编译时类型是MyClass类型,那么在MyClass类中,只有method(Father f),method(Son s)方法,

  • f,s,d变量编译时类型分别是Father、Son、Daughter,而Daughter只能与Father参数类型匹配

  • 而在MySub类中重写method(Father f)方法,所以执行MySub类中的method(Father f)方法

你可能感兴趣的:(多态,面向对象)