那么接下来我们就来看看向上转型的三种多态体现方法:
第一种:直接赋值->子类对象赋值给父类对象:
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 一个对象回来,然后你在那父类类型接收,从而也可以实现向上转型。
这就是多态的体现,如上代码,一个引用调用同一个方法,因为这个引用 引用的对象不一样,导致调用这个方法所表现的行为不一样,这种思想,就叫做多态!
- 向上转型的优点:让代码实现更简单灵活。
- 向上转型的缺点:不能调用子类特有方法。
3.3 向下转型
这个其实用的很少,而且不安全,这里我们简单了解下即可:
向下转型无非就是把一个父类引用放着子类对象,然后强转成对应的子类类型在赋值给子类引用,也就是将父类引用还原成子类对象:
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 关键字来判断下,这里也可以自己下来测试下。
4、通过练习进一步了解多态
4.1 打印指定不同图案(多态实现)
假设我们有这段代码:
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循环遍历这个数组就实现打印了,这两种方法最终实现效果都是一样的:
通过这个练习,我们可以看到多态部分的优点:
- 可以有效降低代码复杂度,避免出现多个 if else
- 可扩展性更强,如果要新增打印,也更简单
当然,多态也有一定的缺点:
- 属性没有多态性,当父类属性和子类属性同名,通过父类引用只能调用父类自己的成员属性
- 构造方法也没有多态性
4.2 避免在构造方法中调用重写的方法
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 修饰的方法