目录
今天的内容有一点点抽象,但是不难,内容也不多。
不知道各位小伙伴在看到多态这两个字的时候会有什么样的想法,如果我从字面上的意思来理解的话,无非就是一个事物有多种表示形态,就好比你打游戏,一个角色能切换多种形态,那如果放在一门面向对象的编程语言中,如何理解多态呢?
简单来说,也就是有多种形态, 当不同的对象去完成一件事时,会产生不同的状态,就好比,猫跟狗都要吃饭,而猫吃饭的时候吃的是猫粮,狗吃饭的时候吃的是狗粮,他们都在做吃饭这个行为的时候,但是吃的东西是不同的,就可以理解成多态。
简而言之:同一种行为,发生在不同对象的身上,他们的产生的结果也就不同。
这里有几点想实现多态的硬性要求:
最后一条如何理解呢?也就是说,我用父类引用了一个子类的对象,也就导致代码在运行的时候,当父类引用子类对象的不同,也就会调用对应子类中重写的方法。
在正式开始学习多态之前,我们需要先了解重写和向上和向下转型:
重写(override) :如果从字面通俗的理解,虽然我继承了你的类,但我感觉你里面的一个成员放方法不满足我类的需求,我就要进行你类中成员方法的重写,既然是重写,方法名(有例外),返回值,形参都不能改变,我只需要对方法体进行改变。重写的好处是什么呢?在于子类可以根据需要,定义特定自己的行为,也就是说子类能够根据需要实现父类的方法。
那这里我们就来简单见一见重写:
- public class Animal {
- private String name;
- private int age;
-
- public void eat() {
- System.out.println(this.name + "正在吃");
- }
- }
- class Dog extends Animal {
- //这里是方法的重写
- @Override
- public void eat() {
- System.out.println(super.getName() + "正在吃狗粮");
- }
- }
由上代码可以看到,Animal 类中有一个 eat 方法,但是在 Dog 继承了 Animal 之后,重写了一个 eat 方法,你可以理解成,我认为狗不应该吃饭,不够准确,所以我要重写成吃狗粮。
这也算是面试中会被问到一道题,请简单说一下重写的重载的区别:
- 参数列表:重写的参数列表一定不能修改,重载的参数列表必须求改
- 返回类型:重写的返回类型必须与父类一致(除非构成父子关系),重载的返回类型不做要求
- 访问限定符:重写访问限定符必须大于等于父类,重载的访问限定符不做要求
总而言之:重写是子类与父类的一种多态性表现,重载则是一个类的多态性的表现。
重写的设计原则其实是,对于已经投入使用的类,我们尽量不要进行修改,最好是重新定义一个类,来重复利用其中共性的内容,增加或者改动新的内容,举一个很简单的例子,在这个通讯飞速发展的时代,人人都离不开手机,而手机的更新迭代是非常快的,系统更新也是很快,假设我有一个新品,他支持一个新的特性,要在原有特性上做修改,那我们首先得保证之前老款手机得不受影响,而新手机也得在原有的特性上进行优化和增加功能,这里就可以进行重写了。你可以想一想,如果在原有的基础上更新,那些老款不支持新的功能怎么办?
静态绑定:也称之为早期绑定,就是在编译的时候,已经根据用户所传递的实参类型确定了调用哪个方法,典型代表就是方法重载
动态绑定:也称之为后期绑定,就是在编译的时候,还不确定调用哪个方法,需要等到程序运行的时候,才能具体确认调用哪个类的方法,典型代表就是多态(往后看,你就明白了)
这个其实前面我们有简单提到过,但没有用代码实现和深入研究,那么在这里我们用代码来见识下什么是向上转型:
向上转型,简而言之就是创建一个子类的对象,把他当作父类的对象来使用,就拿我们上面的 Animal 和 Dog 来举例:
Animal animal = new Dog();
这里 Animal 是 Dog 的父类,而我我们创建了一个子类对象,却把他用父类来引用着,也就是把他当作成父类的对象来使用了,这就是向上转型,如何画图理解呢:
在简单一点,animal 是父类类型,但是可以引用一个子类对象,因为是从小范围向大范围的转换,就好比说,猫跟狗都是动物,因此将猫和狗子类对象转化为父类引用是合理的,大范围是可以囊括小范围的,是安全的!
前面讲一大堆,其实都是在为多态而作铺垫,现在我们来利用上我们前面学习的重写和向上转型,实现一个简单的多态代码:
- public class Animal {
- private String name;
- private int age;
- public Animal(){} //提供无参构造方法
-
- //带参数的构造方法
- public Animal(String name, int age) {
- this.name = name;
- this.age = age;
- }
- public void eat() {
- System.out.println(this.name + "正在吃");
- }
- public String getName() {
- return name;
- }
- }
-
- class Dog extends Animal {
- public Dog(){}
- //带参数的构造方法
- public Dog(String name, int age) {
- super(name, age);
- }
- //这里是方法的重写
- @Override
- public void eat() {
- System.out.println(super.getName() + "正在吃狗粮");
- }
- }
-
- class Cat extends Animal {
- public Cat(){}
- //带参数的构造方法
- public Cat(String name, int age) {
- super(name, age);
- }
- //这里是方法的重写
- @Override
- public void eat() {
- System.out.println(super.getName() + "正在吃猫粮");
- }
- }
我们细读上面代码,其实有把我们前面的知识串联起来,比如使用了 super 调用了父类的方法来获取被 private 修饰的 name,同时 Dog 和 Cat 类都继承了 Animal ,也实现了父类方法的重写,以及我们自己写了几个带参数的构造方法,因为编译器只有在一个类中没有任何构造方法才会帮我们自动添加一个无参的构造方法,但是我们现在写了带参数的,为了保险起见,我们自己又添加了无参的构造方法,同时也实现了要实例化子类对象,要在子类构造方法中调用父类构造方法,也就是 super(),上面代码都有,可以细细阅读一下。
那么接下来我们就来看看向上转型的三种多态体现方法:
第一种:直接赋值->子类对象赋值给父类对象:
- class TestAnimal {
- public static void main(String[] args) {
- Animal animal1 = new Dog("旺财", 5);
- animal1.eat();
- Animal animal2 = new Cat("小咪", 2);
- animal2.eat();
- }
- }
结合上面的学习:同一种行为,发生在不同对象的身上,他们的产生的结果也就不同,自然调用的就是对应对象的 eat 方法。
结果也很符合我们刚开始的结论,果然同一种行为,不同对象产生的结果是不同的!
第二种:方法传参,形参作为父类类型引用,可以接收任意子类对象
- class TestAnimal {
- //用户自己写的方法
- public static void eatFood(Animal animal) {
- animal.eat(); //直接通过方法可以进行实现多态,根据传递的参数不同
- }
- public static void main(String[] args) {
- eatFood(new Dog("小黑", 4));
- eatFood(new Cat("小咪", 1));
- }
- }
这里可能在传参的地方有些不理解,其实在这我们是传了一个匿名对象过去,在后期也会讲解,如果不理解,也可也先 new 一个对象出来,在把对应的引用传进去,也是一样的。
第三种:通过返回值,返回任意子类对象
- class TestAnimal {
- //通过返回值实现向上转型
- public static Animal buyAnimal(String animal) {
- if ("狗".equals(animal)) {
- return new Dog("狗", 2);
- } else if("猫".equals(animal)) {
- return new Cat("猫", 3);
- } else {
- return null;
- }
- }
- public static void main(String[] args) {
- Animal animal1 = buyAnimal("狗");
- animal3.eat();
- Animal animal2 = buyAnimal("猫");
- animal4.eat();
- }
- }
equals 是字符串比较方法,相等返回 true 不相等返回 false,上面这种用法其实还是比较少的,本质就是你给我传递什么,我对应给你 new 一个对象回来,然后你在那父类类型接收,从而也可以实现向上转型。
这就是多态的体现,如上代码,一个引用调用同一个方法,因为这个引用 引用的对象不一样,导致调用这个方法所表现的行为不一样,这种思想,就叫做多态!
这个其实用的很少,而且不安全,这里我们简单了解下即可:
向下转型无非就是把一个父类引用放着子类对象,然后强转成对应的子类类型在赋值给子类引用,也就是将父类引用还原成子类对象:
- class TestAnimal {
- public static void main(String[] args) {
- //向下转型 -> 不安全
- //instanceof 判断animal引用的对象是不是Dog类型的,如果是则为 true 不是则为 false
- Animal animal = new Dog("王五", 12);
- if (animal instanceof Dog) {
- Dog dog = (Dog)animal;
- dog.eat();
- }
- }
- }
简单理解,如果 animal 里面的对象本来是狗,那么如果将 狗 还原成 猫 那么就是不安全的,所以我们就需要用到 instanceof 关键字来判断下,这里也可以自己下来测试下。
假设我们有这段代码:
- public class Drawing {
- public void draw() {
- System.out.println("画图");
- }
- }
- class DrawCircle extends Drawing {
- @Override
- public void draw() {
- System.out.print("⚪");
- }
- }
- class DrawFlower extends Drawing {
- @Override
- public void draw() {
- System.out.print("❀");
- }
- }
- class DrawTriangle extends Drawing {
- @Override
- public void draw() {
- System.out.print("▲");
- }
- }
要让你按找一个顺序打印:▲❀❀⚪❀▲,我们如何实现呢?
第一种方法:用 else if:
- class TestDraw {
- //方法一:使用 循环 + if else
- public static void draw1() {
- DrawTriangle triangle = new DrawTriangle();
- DrawFlower flower = new DrawFlower();
- DrawCircle circle = new DrawCircle();
- String[] shapes = {"Triangle", "Flower", "Flower",
- "Circle", "Flower", "Triangle"
- };
- //增强for循环遍历数组
- for (String s : shapes) {
- if (s.equals("Triangle")) {
- triangle.draw();
- } else if (s.equals("Flower")) {
- flower.draw();
- } else if (s.equals("Circle")) {
- circle.draw();
- }
- }
- }
- public static void main(String[] args) {
- draw1();
- }
- }
这样写代码太复杂了,也不利于阅读,如果对 for-each还不了解,可以看我前面写的程序控制文章。
第二种方法:使用向上转型->多态
- class TestDraw {
- public static void draw2() {
- Drawing[] shapes = { new DrawTriangle(), new DrawFlower(), new DrawFlower(),
- new DrawCircle(), new DrawFlower(), new DrawTriangle()
- };
- for (Drawing s : shapes) {
- s.draw();
- }
- }
- public static void main(String[] args) {
- draw2();
- }
- }
Drawing 是他们的父类,所以我们可以直接创建 Drawing 类型数组,元素放子类对象是没问题的,然后通过增强for循环遍历这个数组就实现打印了,这两种方法最终实现效果都是一样的:
通过这个练习,我们可以看到多态部分的优点:
当然,多态也有一定的缺点:
- class A {
- public A() {
- func();
- }
- public void func() {
- System.out.println("A.func()");
- }
- }
- class B extends A {
- private int num = 1;
- @Override
- public void func() {
- System.out.println("B.func() " + num);
- }
- }
- public class Test {
- public static void main(String[] args) {
- A b = new B();
- }
- }
这里 B 继承了 A 类,所以在实例化B 的时候,因为我们没有写构造方法,肯定会默认调用编译器给我们提供的无参构造方法,而要想实例化子类必须先调用父类的构造方法,而父类 A 无参构造方法中,调用了 func 方法,但是这个方法在 B 类中重写了,我们来看打印结果:
通过测试可以发现:
- 构造 B 对象的时候,会调用 A 的构造方法
- A 的构造方法调用了func方法,此时会触发动态绑定,会调用 B 中的 func
- 此时 B 对象自身还没有构造,所以在 num 还未初始化的时候默认值为 0,如果具备多态性,num 的值应该是1
- 因此在构造方法内部,尽量不要去实例方法,除了 final 和 private 修饰的方法
下期预告:【Java SE】抽象类和接口