【Java SE】多态 一篇带你拿下多态,逐步详解

1. 多态的概念

多态顾名思义,就是多种形态,不同对象去完成某个相同的行为,产生的结果也不同

比如同样都是画画,不同的人画出来的都不一样,同样都是吃饭,猫猫吃猫粮,狗狗吃狗粮。
在这里插入图片描述

2. 多态的实现条件

  1. 必须在继承体系下
  2. 子类必须要对父类中方法进行重写
  3. 父类的引用调用重写的方法

多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法。

我们之前写过的代码里,写过Animal类和Cat类,在里面实现了eat()方法。我们再来回顾一下:

class Animal {
    String name;
    int age;

    public void eat() {
        System.out.println(this.name + "呲饭饭!");
    }
}

class Cat extends Animal {
    String hair;    
    public void eat() {
        System.out.println(this.name + "吃猫粮!");
    }
}

class Dog extends Animal {
    String breed;
    public void eat() {
        System.out.println(this.name + "吃狗粮!");
    }
}
public class Test {
    public static void main(String[] args) {
        Animal cat = new Cat();
        Animal dog = new Dog();
        cat.eat();
        dog.eat();
    }
}

运行结果:
在这里插入图片描述
我们把Cat类和Dog类的对象给了Animal类型的引用,那么引用cat和dog都是Animal类的引用,按理来说Animal类的引用调用eat方法,会调用Animal的eat方法,但是我们看到它分别调用了Cat和Dog类的eat方法。

这是因为在程序运行时,编译器才会知道这个引用具体是哪个子类实例化出来的,也就是到底new的是哪个类的对象,然后在运行时动态地调用相应的方法,我们称之为动态绑定

动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法。

不过在实际开发中,我们不是这么用的,我们需要搞清楚我们的身份,是类的实现者,还是类的调用者

作为类的实现者,我们在写Animal类的eat方法时,我们并不知道未来调用者会调用哪个子类的eat方法,而且,每个子类都有自己的eat方法,那我们就可以把Animal方法的eat方法定义为抽象方法,不写他的具体实现,这样这个类就变成了抽象类。具体抽象类我们下节细讲。

但是,不管是哪个类,他们都是继承自Animal,所有都能用Animal类来接收,所以我们站在调用者的调度,这样实现下动态绑定:

//将Animal实现为抽象类
abstract class Animal {
    String name;
    int age;

    public abstract void eat();
}

class Cat extends Animal {
    String hair;
    public void eat() {
        System.out.println(this.name + "吃猫粮!");
    }
}

class Dog extends Animal {
    String breed;
    public void eat() {
        System.out.println(this.name + "吃狗粮!");
    }
}

public class Test {
    public static void eat(Animal animal) {
        animal.eat();
    }
    public static void main(String[] args) {
        Cat cat = new Cat();
        Dog dog = new Dog();
        eat(cat);
        eat(dog);
    }
}

运行结果:
在这里插入图片描述
来分析一下:

这样我们站在调用者的角度,实现eat方法,含有一个Animal类的参数,用来接收各种Animal的子类的引用,这个过程称为向上转型。然后再调用这个对象的eat方法,由父类引用调用重写的方法,发生动态绑定,运行时调用相应的子类重写的方法,这个过程就是多态的实现。
在这里插入图片描述

这样写,就不用像上一个代码一样直接把Cat类对象给一个Animal类的引用,感觉怪怪的,一般不会这样写。

3. 重写

3.1 重写的概念

重写:又叫覆盖

可以进行重写的方法:

重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写。

重写方法的方法名,返回类型,形参都不能变。

一句话解释就是:外壳相同,核心不同

注意事项:
1.重写方法的访问权限必须大于等于父类的方法比如:父类是protected修饰,子类重写方法可以是protected和public修饰,如果父类是public修饰,子类重写方法只能是public了,因为public是访问权限最大的了。

2.JDK7以后,也支持返回类型可以不同,但是必须是有父子关系的,比如:
父类方法返回Animal类型的引用,重写的子类方法返回Cat类引用。

3.重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写.

4.父类被static、private修饰的方法、构造方法都不能被重写。

3.2 重写和重载的区别

在这里插入图片描述
Overloading重载,是一个类的多态性体现,传入的参数不同则调用相应的方法。

Overriding重写,是子类与父类的多态性表现。

4.向上转型和向下转型

向上转型

概念:创建一个子类对象,将其当成父类对象来使用。
向上转型的实现方法:

1.直接赋值
我们上面写过的代码里:

Animal cat = new Cat();

就属于直接赋值,把Cat类的对象赋给Animal类引用来使用。
2.方法传参

    public static void eat(Animal animal) {
        animal.eat();
    }
    public static void main(String[] args) {
        Cat cat = new Cat();
        Dog dog = new Dog();
        eat(cat);
        eat(dog);
    }

这里我们把Cat类和Dog类对象作为参数传给eat方法,由父类引用Animal来接收,就是方法传参实现向上转型。
3.方法返回

    public static Animal func(int x) {
        if(x == 0) {
            return new Cat();
        } else {
            return new Dog();
        }
    }

返回值是子类对象,返回类型是父类对象,也相当于父类引用接收子类对象。

向上转型的优点:使代码实现更灵活,简化代码
向上转型的缺点:不能调用子类特有的方法,只能调用子类重写了父类的方法。

向下转型

概念:将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转型。

简单说就是:子类转为父类再转回子类。

用代码实现如下:

    public static void eat(Animal animal) {
        animal.eat();
    }
    public static void main(String[] args) {
        Animal cat = new Cat();
        //向上转型
        Animal dog = new Dog();
        //向上转型
        cat = (Cat)cat;
        //向下转型(强制类型转换),将Animal类强转为Cat类
        dog = (Dog)dog;
        //向下转型(强制类型转换),将Animal类强转为Dog类
        eat(cat);
        eat(dog);
    }

运行结果:
在这里插入图片描述
但是,如果我们把本来是Cat类的对象向上转型为Animal后再向下强转为Dog类会怎么样呢? 我们能不能调用Dog类特有的方法 bark() ?

class Dog extends Animal {
    String breed;
    public void eat() {
        System.out.println(this.name + "吃狗粮!");
    }
    public void bark() {
        System.out.println(this.name + "汪汪!!");
    }
}

public class Test {
    public static void main(String[] args) {
        Animal cat = new Cat();
        //向上转型
        cat = (Dog)cat;
        //向下转型
        ((Dog) cat).bark();
    }
}

在这里插入图片描述
编译报错了,出现类型转换异常,将Cat类强转为Animal再转为Cat是安全的,可以正常使用,因为cat本来就是猫。而Cat类强转为Animal再转为Dog是不安全的,因为cat是猫不是狗。

所以,向下转型是不安全的,一旦向下转错了,就会抛出异常。Java中为了提高向下转型的安全性,引入了instanceof 操作符,如果左边的引用本来是右边的类实例化而来的,则表达式为true,则可以安全转换。使用方法如下:

 public static void main(String[] args) {
        Animal animal = new Cat();
        //向上转型
        if(animal instanceof Cat) { //判断animal是否本来是Cat类的对象
            animal = (Cat)animal;
            animal.eat();
        }
    }

在这里插入图片描述
运行成功!

5. 多态的优缺点

优点

  1. 能够降低代码的 “圈复杂度”, 避免使用大量的 if - else

什么叫 “圈复杂度” ?
圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解。 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.

因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”.

如果一个方法的圈复杂度太高, 就需要考虑重构.
不同公司对于代码的圈复杂度的规范不一样,一般不会超过 10 .

这段代码用多态实现是这样的:

    public static void main(String[] args) {
    	//写一个animals数组,存放三个不同的对象,都属于Animals类的子类
        Animal[] animals = {new Cat(), new Dog(), new Bird()};
        //调用每个对象的eat方法
        for(Animal animal : animals) {
        	animal.eat();
        	//发生动态绑定,调用各自的eat方法
        }
    }

如果没有多态,代码将变成这样:

    public static void main(String[] args) {
    	String[] animals = {"cat", "dog", "bird"};
    	for(String animal : animals) {
    		if(animal.equals("cat")) {
				Cat.eat();
			} else if(animal.equals("dog")) {
				Dog.eat();
			} else if(animal.equals("bird")) {
				Bird.eat();
			}
    	}
	}

这样代码就复杂多了。

  1. 可拓展性强
    如果要增加一种动物比如马,那么只需要添加Horse类继承Animal,然后在数组中添加 new Animal() 即可,但如果没有多态,就得大量重改代码,添加else if

避免在构造方法中调用重写的方法

在说这个之前,我们先来回顾一下成员变量的初始化

成员初始化再详谈(组合类 就地初始化)

类的成员的创建和初始化(赋初值),是在类加载时完成,其实际执行顺序是:

构造方法执行之前,创建成员变量并赋初值,接下来,如果有就地初始化的成员,则执行就地初始化的语句,最后,才会执行构造方法的内容,为其他成员进行赋值。

我们通过调试这段代码会让你彻底明白成员初始化。

class A1 {
    int a;
    int b = 10;
    A1() {
        a = 45;
        System.out.println("A1的构造方法");
    }
}

class B1 {
    int c;
    int d = 22;
    int e = 10;
    A1 exe = new A1();
    B1() {
        c = 12;
        System.out.println("B1的构造方法");
    }
}
public class Test2 {
    public static void main(String[] args) {
        B1 b = new B1();

    }
}

打断点,开始调试!
在这里插入图片描述
在这里插入图片描述
这一步,就是成员的创建和赋初值。接着走!
在这里插入图片描述

成员创建并赋初值结束,下一步,就该到我们的就地初始化了。
在这里插入图片描述
现在d和e都不再是0,而是就地初始化之后的结果,继续,我们进行exe对象的初始化。
在这里插入图片描述
实例化exe对象,跟前面的B1对象的实例化类似,也是先创建成员并初始化为0(引用为null),然后执行就地初始化,再执行构造方法。
在这里插入图片描述
在这里插入图片描述
exe初始化完毕,接下来才是B1的构造方法。
在这里插入图片描述
构造方法执行结束,才算真正实例化了B1对象!!


构造方法中调用重写方法典例分析

来看这段代码,我们来创建两个类A和B。在B中重写A的func方法,在B的构造方法中调用func。

class A {
    public A() {
        func();
    }
    public void func() {
        System.out.println("A的func ");
    }
}
class B extends A {
    int num = 1;
    @Override
    public void func() {
        System.out.println("B的func " + num);
    }
}
public class Test {
    public static void main(String[] args) {
        B b = new B();
    }
}

运行结果:
在这里插入图片描述

依据上面的知识我们来分析代码,要实例化B的对象,就要进入B的构造方法,我们没写B的构造方法,编译器会默认帮我们加上,因为继承了A类,所以第一行语句是super(),即

public B() {
	super();
}

来调试看看。
在这里插入图片描述
在这里插入图片描述
然后,就该调用我们的func方法了。
在这里插入图片描述
这是因为A的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 B 中的 func 方法。

所以,总结下:

尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题.


码字不易,点个赞再走吧,收藏不迷路,持续更新中~

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值