Java中的多态
前言
本篇博客的主要内容为Java中关于多态的基础概念,特性,使用方法等。包括了重写和向上转型,向下转型等内容
多态
多态的概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同 的状态。
比如:
我们要画一个图形,这个图形可以画圆形,也可以画方形,也可以画梯形等等等。
动物吃东西,猫吃猫粮,狗吃狗粮。
总的来说:同一件事情,发生在不同对象身上,就会产生不同的结果
多态的实现条件
在实现多态之前,需要满足三个条件。
这三个条件分别是:继承,重写,向上转型(通过父类的引用调用重写的方法)
多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法。
继承这里就不再说了,我们主要看重写和向上转型。
这里建议先了解向上转型,再去理解重写和下面这段代码,会更容易理解一些。
先看一段已经实现好多态的代码,后面关于向上转型和重写都会根据这段代码来演示。
public class Animal {
public String name;
public int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
//注意看这个eat方法,和下面Dog类和Cat类的eat方法。
public void eat() {
System.out.println(name+"正在吃东西");
}
}
//
class Dog extends Animal{
public Dog(String name, int age) {
super(name, age);
}
// 狗这里是吃狗粮
@Override
public void eat() {
System.out.println(name+"正在吃狗粮");
}
public void bark() {
System.out.println(name+"正在汪汪叫");
}
}
class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
// 猫吃猫粮
@Override
public void eat() {
System.out.println(name+"正在吃猫粮");
}
public void mew() {
System.out.println(name+"正在喵喵叫");
}
}
//上方为类的实现者编写
//======================================================================================================================//
//下方为类的调用者编写
class TestAnimal {
public static void eat(Animal animal) {
animal.eat();
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog("小黑", 3);
Cat cat = new Cat("小橘", 2);
eat(dog);
eat(cat);
}
}
当类的调用者在编写 eat 这个方法的时候, 参数类型为 Animal (父类), 此时在该方法内部并不知道, 也不关注当前的animal 引用指向的是哪个类型(哪个子类)的实例. 此时 animal 这个引用调用 eat方法可能会有多种不同的表现(和 animal 引用的实例相关), 这种行为就称为 多态.
比如这个代码中,针对dog和cat对象都调用了eat方法,结果是一个是吃了狗粮,另外一个是吃了猫粮,这就是Java中多态的体现。
向上转型
这里我们先理解向上转型,会更方便的理解我们的重写。
向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。
语法格式:父类类型 对象名 = new 子类类型()
直接看代码
Animal animal = new Cat("大橘", 3);
animal是父类类型,引用了一个子类对象,这就是向上转型。是小范围向大范围转换。
这就是向上转型。
向上转型的使用场景
场景1:直接赋值:子类对象赋值给父类对象
Animal animal = new Cat("大橘", 3);
场景2:用于方法传参,形参作为父类类型引用,就可以接受任何子类的对象。
public static void eat(Animal animal) {
animal.eat();
}
场景3:用于返回值,可以返回任何子类对象
public static Animal buyAnimal() {
return new Cat("大橘", 2);
}
向上转型的优缺点
优点:可以让代码实现的更加灵活
缺点:向上转型不能调用子类的特有方法
这里的animal(父类)无法调用Cat类型(子类)里的mew方法。
关于向下转型
有向上转型就有向下转型,这里由于篇幅原型加上因为安全原因导致向下转型用的较少,这里就不再赘述,感兴趣的朋友可以在社区中找一下其他大佬写的关于向下转型的内容。
重写
重写的概念
重写(override):也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!
重写的规则
1:子类再重写父类的方法的时候,一般必须和父类的的方法原型一致,方法名和参数列表必须一致。
//父类 animal的eat方法
public void eat() {
System.out.println(name+"正在吃东西");
}
//子类 Dog的eat方法
@Override
public void eat() {
System.out.println(name+"正在吃狗粮");
}
//子类Cat的eat方法
@Override
public void eat() {
System.out.println(name+"正在吃猫粮");
}
注:这里重写的方法可以发现有一个@Override的注解,一般编译器会帮我们自动加上,这个注解的意思就是这个方法是被注解的,如果语法错误或者拼写错误,编译器会报错。
2:被重写的方法返回值可以不同,但是必须是有继承关系的。
//父类 animal的eat方法
public Animal eat() {
System.out.println(name+"正在吃东西");
return null;
}
//子类 Dog的eat方法
@Override
public Dog eat() {
System.out.println(name+"正在吃狗粮");
return null;
}
上面这种情况仍然可以认为这两个方法构成了重载,但是这两个返回值类型必须要有继承关系。
3:访问权限不能比父类中被重写的方法的访问权限更低。
重写的情况下,父类中的方法,访问权限必须大于等于子类的方法,比如如果父类被protected修饰,那么子类重写改方法就必须是protected或者public。
4:父类被static、private修饰的方法、构造方法都不能被重写。
重写和重载的区别
重写的设计原则
在实际项目中,对于已经投入使用的类,尽量不要进行修改,否则很容易会造成一些bug
最好的方式是:重新定义一个新的类,来重复利用其中共性的内容,并且添加或者改动新的内容。对其中的方法进行重写。
比如:若干年前的手机,只能打电话,发短信,来电显示只能显示号码,而今天的手机在来电显示的时候,不仅仅可以显示号码,还可以显示头像,地区等。在这个过程当中,我们不应该在原来老的类上进行修改,因为原来的类,可能还在有用户使用,正确做法是:新建一个新手机的类,对来电显示这个方法重写就好了,这样就达到了我们当今的需求了。
多态的优缺点
我们先来看一段代码
public class Shape {
public void draw(){
System.out.println("画图形");
}
}
class Rect extends Shape {
@Override
public void draw() {
System.out.println("■");//画方形
}
}
class Cycle extends Shape {
@Override
public void draw() {
System.out.println("⚪");//画圆形
}
}
class Flower extends Shape {
@Override
public void draw() {
System.out.println("❀");//画花
}
}
使用多态的好处
1:能够降低代码的 “圈复杂度”, 避免使用大量的 if - else
什么叫 “圈复杂度” ?
圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解。
而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂。
因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”。
如果一个方法的圈复杂度太高, 就需要考虑重构。
不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10
这个时候如果我们要一次性打印 ⚪■⚪■❀,如果不用多态该怎么做?
答案是先把要打印的图形放入一个数组中,使用for循环进行遍历,然后通过if - else进行比较,然后打印出相应的形状。
public static void drawShape(){
//先创建对象
Rect rect = new Rect();
Cycle cycle = new Cycle();
Flower flower = new Flower();
//建立数组
String[] shape = {"rect" , "cycle","rect" , "cycle", "flower" };
//循环遍历,然后通过if判断进行打印
for (String s: shape) {
if (s.equals("rect")) {
rect.draw();
} else if (s.equals("cycle")) {
cycle.draw();
} else if (s.equals("flower")) {
flower.draw();
}
}
}
如果使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单.
public static void drawShape() {
Rect rect = new Rect();
Cycle cycle = new Cycle();
Flower flower = new Flower();
//这里用到了向上转型,Rect和Cycle,Flower都是Shape的子类。
Shape[] shapes = {rect, cycle, rect, cycle ,flower};
for (Shape shape : shapes) {
shape.draw();
}
}
2:可扩展能力更强
如果想要加上一种新的形状,直接再继承一个子类就可以了
class Triangle extends Shape {
@Override
public void draw() {
System.out.println("▲");
}
}
而且对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低.
而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高.
多态的缺陷
1:代码的运行效率降低。
2:属性没有多态性:
当父类和子类都有同名属性的时候,通过父类引用,只能引用父类自己的成员属性。
3:构造方法没有多态性,看下面一段有坑的代码:
我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用 func
class B {
public B() {
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
@Override
public void func() {
System.out.println("D.func() " + num);
}
}
public class Test {
public static void main(String[] args) {
D d = new D();
}
}
构造 D 对象的同时, 会调用 B 的构造方法.
B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func
此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0. 如果具备多态性,num的值应该是1.
所以在构造函数内,尽量避免使用实例方法,除了final和private方法。
尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题。
总结
多态是面向对象语言种的三大特性之一,而且和继承息息相关,对于继承和多态,加上类和对象,就像C语言中的指针一样,需要深刻理解。本篇博客还有许多不足之处,因为我也是在学习,这篇是我对于多态内容的复习。如果有问题希望大佬多多指正,非常感谢~