前言❤️❤️
hello hello💕,这里是洋不写bug~😄,欢迎大家点赞👍👍,关注😍😍,收藏🌹🌹 多态是Java入门的一个重点,多态是 Java 做项目最核心、最高频、必用的特性之一,项目中基本上到处都是多态,这篇博客就会详细的解析多态的用法和注意事项
🎇个人主页:洋不写bug的博客 🎇所属专栏:Java学习之旅,从入门到进阶 🎇铁汁们对于Java的各种常用核心语法(不太常用的也有😆),都可以在上面的Java专栏学习,专栏正在持续更新中🐵🐵,有问题可以写在评论区或者私信我哦~
1,多态的简介
多态属于是JavaSE中最难的语法 多态、继承、封装,属于是Java的三大特征
多态简单来说,就是一个对象的引用,具备多种形态 就比如说大明家里养了一只狗叫大黄,大明叫他儿子 大明还有个真正的儿子,小明 有一次,大明的老婆给大明说,给你儿子整点吃的,那这时候究竟谁给狗整吃的,还是给小明整吃的,就要结合上下文的语境来进行确定了
代码如下所示:
package package1;
public class Son {
public void eat(){
System.out.println("Son吃东西");
}
}
package package1;
public class XiaoMing extends Son{
public void eat(){
System.out.println("小明吃炒米饭");
}
}
package package1;
public class Dog extends Son{
public void eat(){
System.out.println("大黄吃狗粮");
}
}
package package1;
public class Test {
public static void main(String[] args) {
Son son = new Son();
son.eat();
son = new XiaoMing();
son.eat();
son = new Dog();
son.eat();
}
}
在Test类中,使用父类的引用指向子类实例,执行结果如下所示: 
son这个引用,可能有多种形态,具体是哪种形态,就需要结合上下文来看了,不同形态执行son.eat(),执行的是不同的逻辑
多态也有其他的例子,如下所示:
package package1;
public class Test {
public static void run(Son son){
son.eat();
}
public static void main(String[] args) {
run(new Son());
run(new XiaoMing());
run(new Dog());
}
}

这样看起来是在调用父类Son的eat方法,但是实际上执行的逻辑还是取决于animal实际指向的对象是哪个类型的
2,动态绑定
当代码运行到son.eat()的时候,JVM就会在运行过程中,分析son真实指向的对象是啥类型的,进一步调用匹配类型的方法
这样的一个过程就可以称为动态绑定,也就是eat方法执行哪个版本,不是在程序编译时(Java程序在运行前先编译成字节码,编译完成的字节码可以反复执行多次,也可以拷贝大其他机器上执行)就确定的,而是在运行时确定的
和编译操作相关的动作/过程,一般称为是静态的 和运行相关的动作/过程,一般称为是动态的 Java的多态,就是基于动态绑定机制来实现的
3,多态使用的注意事项
在Java中,要实现多态,需要注意以下几点:
返回值类型相容,这是个比较冷门的语法,一般也用不到,这里铁汁们了解下即可 如下所示,这里子类Cat重写父类Animal的eat方法,子类eat方法返回的是Cat类型的,父类eat方法返回的是Animal类型的,子类方法返回的类型是父类方法返回类型的子类,因此这样也是可行的,如下所示:
package package2;
public class Animal {
public Animal eat(){
System.out.println("Animal is eating");
return this;
}
}
package package2;
public class Cat extends Animal{
public Cat eat(){
System.out.println("Cat is eating");
return this;
}
}
package package2;
public class Test {
public static void main(String[] args) {
Animal animal = new Animal();
animal.eat();
animal = new Cat();
animal.eat();
}
}

接下来是注解,当子类重写父类方法的时候,最好在上面加上个@Override注解,标注这个方法是重写的,对方法做一些合法性的校验 例如这里的子类的方法名打错了,这里并不会报错,因为这样只是相当于没有进行方法重写 
程序运行结果如下图,这时候子类中没有检测出eat方法,就会自动使用父类中的eat方法,就不是我们想调用的效果 
加了@Override后,这里就会报错,我们看报错信息,这个方法没有重写它的超类(父类又称超类)的方法,就会提醒我们 
另外,方法重写时,需要我们尽量遵守开闭原则,也就是对于修改关闭,对于扩展开放
开闭原则的核心就是:对于已经投入的类,尽量不要进行修改,最好的方式是重新定义一个类,来重复其中共性的内容,并且添加或者改动新的内容
就比如手机通信,现在的新手机都普遍显示地区,但早期的手机只显示号码,那在这个过程中,就不该在原来老的类上进行修改,因为可能还有使用老式手机的人,它的手机上没有这个功能,盲目修改类,就会导致bug 应该新建一个手机类,重写方法,新手机来电显示这个重写的方法即可 
4,向上转型
向上转型:父类引用指向子类实例
这里是以对象本体来看的,对象是子类对象,本来应该使用子类引用的,使用了父类引用,所以说就是向上转型
前面提到的多态其实都是向上转型 平时使用最多的就是下面这种:
package package1;
public class Test {
public static void main(String[] args) {
Son son = new Son();
son.eat();
son = new XiaoMing();
son.eat();
son = new Dog();
son.eat();
}
}
前面提到的这种写法也是多态(使用父类引用来表示子类对象)
package package1;
public class Test {
public static void run(Son son){
son.eat();
}
public static void main(String[] args) {
run(new Son());
run(new XiaoMing());
run(new Dog());
}
}
包括这种写法,也是向上转型,只要父类引用指向子类对象的,都是向上转型
public class Test {
public static Animal createAnimal(){
return new Cat();
}
public static void main(String[] args) {
Animal animal = createAnimal();
}
}
5,向下转型
接着是向下转型,向下转型是依赖向上转型的,平常也比较少用,要求父类的引用实际上指向的是子类的实例,并且需要加上显示的转换 向下转型说白了就是父类引用赋值给子类引用
public class Test {
public static void main(String[] args) {
Animal animal = new Cat();
Cat cat = (Cat) animal;
}
}
下面这种情况就必须使用向下转型,这个铁汁们了解下即可,一般很少这样写代码,我们可以借此了解下编译器的检查规则
铁汁们看下面这几段代码,猜下这个会报错吗?
package package2;
public class Animal {
public void eat(){
System.out.println("Animal is eating");
}
}
package package2;
public class Cat extends Animal{
public void eat(){
System.out.println("Cat is eating");
}
public void meow(){
System.out.println("喵喵喵");
}
}
package package2;
public class Test {
public static void main(String[] args) {
Animal animal = new Cat();
animal.meow();
}
}
其实是会报错的,如下图,这里显示在Animal类里面找不到这个meow()方法 
可能有的铁汁就会说:不对呀,前面提到的动态规划中说JVM在运行到这里时,会分析这个引用真实指向的对象类型,这里真实指向的对象类型是Cat呀,那为什么会报错在Animal类中找不到这个方法呢?
其实,这个错误是在编译器编译阶段报的错误,在编译阶段,看到animal就是父类类型的引用,于是尝试在父类类型中找meow(),找不到,就报错了 编译器只做最基本的语法检查,不知道代码的逻辑是怎么样的,也不知道代码是如何执行的,无法确定animal的真是类型,只能把animal当作父类的引用
那么编译器报错,代码就无法执行了,动态绑定是在代码运行起来进行的
解决这个问题,就可以依靠向下转型,将父类引用赋值给子类引用,如下所示:
public class Test {
public static void main(String[] args) {
Animal animal = new Cat();
Cat cat = (Cat)animal;
cat.meow();
}
}
另外,向下转型的时候,需要确保父类引用确实是指向子类实例的,否则就算强转了,也无法调用里面的方法,如下所示,会报出一个ClassCastException(类型转化异常)
public class Test {
public static void main(String[] args) {
Animal animal = new Animal();
Cat cat = (Cat)animal;
cat.meow();
}
}

向下转型如果想要避免报出类型转化异常,可以写个if判断一下,利用instaceof判断下animal引用指向的对象是不是Cat类型的,如果是,才进行向下转型 如下所示
public class Test {
public static void main(String[] args) {
Animal animal = new Animal();
if(animal instanceof Cat){
Cat cat = (Cat)animal;
cat.meow();
}else{
System.out.println("类型不匹配");
}
}
}

6,多态的优缺点分析
多态的主要优点如下:
第一点前面已经提到过了,便于代码扩展,这里就主要解析下第二点
“圈复杂度”就是衡量一个代码/方法,复杂程度的一种指标,把一段代码中出现分支的个数,称为圈复杂度 如果代码是从上到下执行完的,就是最简单的代码,圈复杂度就是1 如果代码中的条件、循环特别多,而且还进行嵌套,那这个代码圈复杂度也就很高,理解起来就有难度
一般在公司中,要求最好圈复杂度在5以下,最多不超过10
那如何使用多态,来降低圈复杂度呢,示例如下(Windows计算机可以按win 加上 >键来获取图案):
package package3;
public class Shape {
public void draw(){
System.out.println("打印形状");
}
}
package package3;
public class Rect extends Shape{
@Override
public void draw(){
System.out.println("□");
}
}
package package3;
public class Circle extends Shape{
@Override
public void draw(){
System.out.println("〇");
}
}
package package3;
public class Flower extends Shape {
@Override
public void draw(){
System.out.println("🌹");
}
}

注:在Java中字符串类型就是String(后面会出一期博客来详细解析这个类型),判断字符串是否相同不能用“==”,需要用equals方法来判断
package package3;
public class Test {
public static void main(String[] args) {
String[] shapes = {"circle","rect","rect","flower"};
for(String shapeName : shapes){
if(shapeName.equals("circle")){
Shape shape = new Circle();
shape.draw();
}else if(shapeName.equals("rect")){
Shape shape = new Rect();
shape.draw();
}else if(shapeName.equals("flower")){
Shape shape = new Flower();
shape.draw();
}
}
}
}
打印结果如下: 代码的圈复杂度就是4,共有4个分支,进行if判断 
利用多态,就可以把圈复杂度降为1,代码如下所示: 直接搞一个对象数组,然后再遍历整个数组即可,多态是不是非常牛
public class Test {
public static void main(String[] args) {
Shape[] shapes = {new Circle(),new Rect(),new Rect(),new Flower()};
for(Shape shape : shapes){
shape.draw();
}
}
}
其实多态解决的核心问题,本质上是在封装的基础上更进一步了 封装是使用者不知道类的内部实现,就能使用,降低使用者的成本 多态是使用者不光不需要知道类的内部实现,甚至都不需要知道类的真实类型,就可以使用
那多态有什么缺点呢? 那就是程序在运行过程中,消耗的时间和耗费的空间越来越多,因为动态绑定是需要一定的开销的,但是现在机器的硬件配置相比以前有了很大的提高 因此,这里的缺点基本上可以忽略(像C++就是把效率发挥到极致的语言)
7,初始化问题
铁汁们看这样一个程序: 子类重写父类的func()方法,父类在无参构造方法中调用了func()方法,接着在main方法中创建子类对象
package package4;
public class Parent {
public Parent(){
func();
}
public void func(){
System.out.println("Parent.func()");
}
}
package package4;
public class Child extends Parent{
public Child(){
}
private int num = 10;
public void func(){
System.out.println("Child.func() : num = " + num);
}
}
package package4;
public class Test {
public static void main(String[] args) {
Child child = new Child();
}
}
那这里执行main方法后,会打印什么 创建子类对象,在执行子类的构造方法之前,会先执行父类的构造方法,执行 父类的构造方法时,就会触发多态,实际上执行子类的版本 执行结果如下: 
这里执行的是子类的func方法没错,但是打印出的num的值却是0,这是为什么呢?
因为此处的打印,是在Parent的构造中调用的,这时候Child里面的属性还没有进行初始化呢,此处看到的0,就是JVM申请内存时,自动填充的0,那这里打印的结果就是错误的
使用多态的时候,要注意,不要在父类的构造方法中去触发多态,因为父类构造方法执行的时候,子类的属性还没有初始化,此时执行子类的方法,使用子类的属性,也就处于“未初始化”的状态中
结语💕💕
到现在,Java的三大特征(多态,封装,继承)已经全部解析完了,这部分东西可能有的东西稍微有点抽象,学起来不知道有什么实用价值,这是很正常的 后面写完抽象类和接口的博客后,博主会写一个简单的图书管理系统的解析博客,在里面会使用各种JavaSE的基础内容,最好的理解方法不是硬记概念,而是在项目代码中理解🐵
以上就是今天的所有内容啦~完结撒花~🥳🎉🎉
网硕互联帮助中心




评论前必须登录!
注册