【OOP】什么是多态

Posted by 西维蜀黍 on 2019-07-03, Last Modified on 2023-07-18

什么是多态

面向对象的三大特性:封装(encapsulation)、继承(Inheritance)和多态(polymorphism)。从一定角度来看,封装和继承几乎都是为多态而准备的。

多态的定义:指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用)实现多态的技术称为:动态绑定(dynamic binding),是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。

多态的作用:消除类型之间的耦合关系。现实中,关于多态的例子不胜枚举。比方说按下 F1 键这个动作,如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;如果当前在 Word 下弹出的就是 Word 帮助;在 Windows 下弹出的就是 Windows 帮助和支持。同一个事件发生在不同的对象上会产生不同的结果。

多态的两种表现形式

多态有两种表现形式:重载和覆盖。

重载(Overload)

首先说重载(overload),是发生在同一类中。与什么父类子类、继承毫无关系。

void foo(String str);
void foo(int number);

标识一个函数除了函数名外,还有函数的参数(个数和类型)。也就是说,一个类中可以有两个或更多的函数,叫同一个名字而他们的参数不同。

他们之间毫无关系,是不同的函数,只是可能他们的功能类似,所以才命名一样,增加可读性,仅此而已!

覆盖/重写(Override)

再说覆盖(Override),是发生在子类中!也就是说必须有继承的情况下才有覆盖发生。

class Parent {
    void foo() {
        System.out.println("Parent foo()");
    }
}
class Child extends Parent {
    void foo() {
        System.out.println("Child foo()");
    }
}

我们知道继承一个类,也就有了父类了全部方法,如果你感到哪个方法不爽,功能要变,那就把那个函数在子类中重新实现一遍。

这样再调用这个方法的时候,就是执行子类中的过程了。父类中的函数就被覆盖了(当然,覆盖的时候函数名和参数要和父类中完全一样,不然你的方法对父类中的方法就不起任何作用,因为两者是两个函数,毫不关系)。

例子

多态,用一句话来说,就是事物在运行过程中存在不同的状态。多态的存在有三个前提:

  1. 要有继承关系
  2. 子类要重写父类的方法
  3. 父类类型的引用指向子类

我们先定义一个父类Animal

class Animal{
  int age = 20;

  public void eat(){
    System.out.println("动物在吃饭");
  }

  public void eat2(){
    System.out.println("动物在吃饭2");
  }

  public static void sleep(){
    System.out.println("static 动物在睡觉");
  }

  public void run(){
    System.out.println("动物在奔跑");
  }
}

再定义一个子类Cat

class Cat extends Animal{
  int age = 40;
  String name = "Tom";

  public void eat(){
    System.out.println("猫在吃饭");
  }

  public void eat2(){
    System.out.println("猫在吃饭2");
  }
  
  public stastic void sleep(){
    System.out.println("static 猫在睡觉");
  }

  public void catchMouse(){
    System.out.println("猫在抓老鼠");
  }
}

再来一个测试

class Demo_Test{
  public stastic void main(String[] args){
    Animal am = new Cat();
    am.eat(); 
    am.eat2();
    am.sleep();
    am.run();
    System.out.println(am.age);

    //am.catchMouse(); //编译不能通过,于是注释掉了
    //System.out.println(am.name); //编译不能通过,于是注释掉了
  }
}

先来看看上面是否满足三个条件:

  1. 子类Cat继承了父类Animal;
  2. 子类重写了父类的 eat()sleep() 方法,其中前者是非静态,后者是静态方法;
  3. 在堆内存中实例化了一个子类Cat对象,并把存在于栈内存中的Animal引用类型变量指向这个对象。

运行结果

猫在吃饭
猫在吃饭2
动物在睡觉
动物在奔跑
20

我们发现:

  • 子类重写了父类的非静态成员方法,对应am.eat() 的输出结果是“猫在吃饭”;
  • 子类重写了父类的非静态成员方法,且在子类重写这个非静态成员方法时,增加了@Override 关键字,对应am.eat2() 的输出结果是“猫在吃饭2”;
  • 子类重写了父类的静态成员方法,对应 am.sleep() 的输出结果是“动物在睡觉”;
  • 未被子类重写的方法,对应 am.run() 的输出结果是“动物在奔跑”;
  • 子类重写了父类的成员变量,对应 am.age 的值为 20(即为在父类中的赋值)。

规律

  • 当子类重写了父类的非静态成员方法时,通过一个父类类型的引用变量来指向一个子类对象时,调用该重写的非静态成员方法,会执行子类中的重写方法
  • 当子类重写了父类的静态成员方法时,通过一个父类类型的引用变量来指向一个子类对象时,调用该重写的静态成员方法,会执行父类中的重写方法
  • 当子类重写了父类的非静态成员方法时,加不加上 @Override 关键字并不会影响执行结果,但是加上后,编译器会帮我们检查重写方法声明的正确性;
  • 当子类重写了父类的静态成员方法时,通过一个父类类型的引用变量来指向一个子类对象,无法调用到子类中新定义的方法和字段;
  • 当子类重写了父类的成员变量时,通过一个父类类型的引用变量来指向一个子类对象,获取到的该成员变量的值,为该值在父类中赋的值。

多态的缺点

多态也存在缺点,也就是无法尝试调用子类特有的方法,如上面被注释掉的 am.catchMouse(); System.out.println(am.name);两行代码会编译时报错,因为这是子类特有的成员方法和成员属性。

如果我们需要调用子类的特有成员方法和成员属性,那么应该将这个父类强制转换为子类类型,如

class Demo_Test {
	public static void main(String[] args) {

	Animal am = new Cat();
	am.eat();
	am.sleep();
	am.run();
//	am.catchMouse();
//	System.out.println(am.name);
	System.out.println(am.num);
	System.out.println(am.age);

	System.out.println("------------------------------");
	Cat ct = (Cat)am;
	ct.eat();
	ct.sleep();
	ct.run();
	ct.catchMouse();
	}        
}

执行强制类型转换Cat ct = (Cat)am后,这时ct就指向堆里最先创建的Cat对象了,自然能使用Cat类的一切成员方法和成员属性了。这也是多态的魅力,为了使用子类的某些方法不需要重新再开辟内存。以上就是多态中的向下转型

Reference