Java多态

本文详细探讨了Java中参数传递的值传递特性,通过示例展示了如何使用对象引用实现值交换。接着,解释了多态的概念,包括其三个必要条件、动态绑定以及向上转型的场景。同时,文章还分析了方法重写、静态与动态绑定的区别,并讨论了向下转型及其安全性。最后,阐述了多态在代码复杂度控制和可扩展性方面的优势和潜在问题。
摘要由CSDN通过智能技术生成
  1. 关于引用的进一步理解(交换值)
    因为Java方法在传递参数的时候都是值传递,那么如何通过方法实现2个数的值交换?
    明确:在传引用的时候,到底拿引用干了个啥
class Value {
    public int a;
}

public class Test {

    public static void swap(Value value1, Value value2) {
        int tmp = value1.a;
        value1.a = value2.a;
        value2.a = tmp;
    }

    public static void main(String[] args) {
        //交换值,如果只是简单的定义a和b不太能做到,因为他们在栈上,我们要想办法把他们放到堆上
        Value value1 = new Value();
        Value value2 = new Value();
        value1.a = 10;
        value2.a = 20;
        swap(value1, value2);
        System.out.println(value1.a);
        System.out.println(value2.a);
    }

在这里插入图片描述
将属性a的权限改成private,此时在类外a是被封装起来的,不能够直接赋值了,可以提供get和set方法来实现交换值

class Value {
    private int a;

    public int getA() {
        return a;
    }

    public void setA(int a) {
        this.a = a;
    }
}

public class Test {

    public static void swap(Value value1, Value value2) {
//        int tmp = value1.a;
        int tmp = value1.getA();
//        value1.a = value2.a;
        value1.setA(value2.getA());
//        value2.a = tmp;
        value2.setA(tmp);
    }

    public static void main(String[] args) {
        //交换值,如果只是简单的定义a和b不太能做到,因为他们在栈上,我们要想办法把他们放到堆上
        Value value1 = new Value();
        Value value2 = new Value();
        //value1.a = 10;
        value1.setA(10);
        //value2.a = 20;
        value2.setA(20);
        swap(value1, value2);
        System.out.println(value1.getA());
        System.out.println(value2.getA());
    }

    public static void main1(String[] args) {
        Derived derived = new Derived();
        derived.fun();
    }
}

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

  1. 什么是多态?
    发生多态的3个条件①在继承的条件下②发生向上转型③方法重写
    多态是一种思想,父类引用引用不同对象的时候,表现出来的行为是不一样的。【这就叫做多态】
    多态的前提是:动态绑定
    【一个父类引用 指向的对象不一样,调用重写的方法,会表现出不同的行为。】

  2. 向上转型
    分别定义动物类、狗类、鸟类

class Aniaml {
    public String name;
    public int age;

    public void eat() {
        System.out.println(name + "吃东西");
    }
}

class Dog extends Aniaml {
    public void wangwnag() {
        System.out.println(name + "汪汪汪");
    }
}

class Bird extends Aniaml {
    public String wing;
    public void miaomiao() {
        System.out.println(name + "喵喵喵");
    }
}

public class Test1 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.name = "小狗";
        dog.eat();
        dog.wangwnag();
        System.out.println("===分割===");
        Bird bird = new Bird();
        bird.name = "小鸟";
        bird.eat();
        bird.miaomiao();
    }
}

运行结果:
在这里插入图片描述
如果修改main方法中的实例化对象语句为:

        Aniaml aniaml1 = new Dog();
        aniaml1.name = "小狗";
        aniaml1.eat();
//        aniaml1.wangwang();  //报错,因为只能访问Aniaml类中有的成员
        System.out.println("===分割===");
        Aniaml aniaml2 = new Bird();
        aniaml2.name = "小鸟";
        aniaml2.eat();

此时 Aniaml aniaml1 = new Dog();,左边是动物类,右边是狗类,发生了向上转型,父类引用指向了子类对象。
理论上:等号两边的数据类型必须一致,否则赋值出错。
当发生向上转型之后,此时通过父类的引用只能访问父类自己的成员,不能访问到子类特有的成员,也就是说,只animal能调用Animal类中的方法和属性,不能再访问Dog类和Bird类子类中独有的方法和属性了。

  1. 向上转型的3种场景
    ①直接赋值
        Aniaml aniaml1 = new Dog();
        aniaml1.name = "小狗";
        aniaml1.eat();

②方法传参

    public static void fun(Aniaml aniaml) {
        aniaml.eat();
    }

    public static void main(String[] args) {
        Dog dog = new Dog("小狗");
        fun(dog);
    }

③方法返回值

    public static Aniaml func() {
        return new Dog();
    }

向上转型的优点:让代码实现更简单灵活。(animal能够引用它,说明它一定是个animal的子类,它一定是一个动物)
向上转型的缺点:不能访问子类特有的方法。【除非发生了动态绑定,可以调用子类重写的方法】(animal只能调用自己特有的属性和方法)

  1. 方法重写的引入
    在上述例子种,狗和鸟都是吃饭,按照常理,狗应该吃狗粮,鸟应该吃鸟粮,此时就应该在狗类和鸟类种重写父类的eat方法。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    public static void main(String[] args) {
        Aniaml aniaml1 = new Dog("小狗");
        aniaml1.eat();
        Aniaml aniaml2 = new Bird("小鸟");
        aniaml2.eat();
    }

运行结果:
在这里插入图片描述
分析:在这个过程中,【父类不是只能调用父类的方法吗?这里怎么调用了重写的方法呢?】因为这里发生了动态绑定,编译的时候确实调用的是Animal的eat方法,但是在运行的时候发生了动态绑定,调用了重写的方法。

  1. 动态绑定需要满足的3个条件
    ①向上转型②重写③通过父类引用调用这个父类和子类重写的方法
    【动态绑定是多态的基础】
    动态绑定:也称为后期绑定(晚绑定),在编译的时候不能确定方法的行为,需要等到程序运行的时候,才能确定具体调用哪个类的方法
    静态绑定:也称为前期绑定(早绑定),在编译的时候,根据传入的参数,就能够确定调用哪个方法,典型代表方法重载。

  2. 方法重写的注意点
    重写:是子类对父类非private修饰、非static修饰、非final修饰、非构造方法等进行重新编写。【重写的好处:子类可以根据需要,定义特定于自己的行为,子类可以根据需要实现父类的方法。】
    ①被private修饰的方法不能被重写:因为private权限范围只能在当前类内使用
    ②被static修饰的方法不能被重写:因为static属于类方法,如果被重写,则一个属于动物类,一个属于狗类,和对象毫无关系,分别属于各自所在的类。
    ③被final修饰的方法不能被重写:也叫密封方法
    ④构造方法不能被重写:因为子类的构造方法名字不可能和父类的构造方法的名字一样。
    ⑤子类的访问修饰限定权限要大于等于父类的权限
    private<默认<protected<public

  3. 重写和重载的对比
    重写:①方法名相同 ②参数列表相同③返回值类型相同【当返回值类型不同的时候必须是 父子关系
    重载:①方法名相同②参数列表不同③与返回值类型无关
    【重写的参数列表 一定不能修改,重载的参数列表 必须修改】

  4. 向下转型【不安全】
    将一个子类对象经过向上转型之后当成父类方法使用,这也再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时需要将父类引用再还原为子类对象。
    例如,将Dog和Bird向上转型成Animal后,无法调用Dog中特有的wangwang方法以及Bird中的zhazha方法,此时需要将Animal再次向下转型成Dog和Bird进行调用子类特有的方法。

    public static void main(String[] args) {
        Aniaml aniaml1 = new Dog("小狗");
        Dog dog = (Dog) aniaml1;
        dog.eat();
        Aniaml aniaml2 = new Bird("小鸟");
        Bird bird = (Bird) aniaml2;
        bird.eat();
    }

运行结果:
在这里插入图片描述
分析:animal1向下转型:本来是狗,还原为狗,安全;animal2向下转型:本来是鸟,还原为鸟,安全。
如果本来是狗,向下转型为鸟,则不安全:

    public static void main(String[] args) {
        Aniaml aniaml = new Dog("小狗");
        Bird bird = (Bird) aniaml;
        bird.eat();
    }

编译没有报错,但是在运行的时候出现类型转换异常
在这里插入图片描述
分析:程序可以通过编程,但是运行时抛出异常,本来是狗,现在要强制还原为猫,无法正常还原,不安全。
Java中为了提高向下转型的安全性,引入 instanceof关键字,如果表达式为true,则可以安全转换。

    public static void main(String[] args) {
        Aniaml aniaml = new Dog("小狗");
        if (aniaml instanceof Bird) {
            Bird bird = (Bird) aniaml;
            bird.eat();
        }else {
            Dog dog = (Dog) aniaml;
            dog.eat();
        }
    }

运行结果:
在这里插入图片描述
分析:aniaml instanceof Bird,判断animal是不是引用了Bird对象,如果引用了就可以向下转型。

  1. 多态的优缺点
    例:定义类,完成画图(圆,矩形,花等)
    使用多态完成:
class Shape {
    public void draw() {
        System.out.println("画图形");
    }
}


class Cir extends Shape {
    @Override
    public void draw() {
        System.out.println("画圆");
    }
}

class Rex extends Shape {
    @Override
    public void draw() {
        System.out.println("画矩形");
    }
}

class Flow extends Shape {
    @Override
    public void draw() {
        System.out.println("画花");
    }
}


public class TestDemo {

    public static void func(Shape shape) {
        shape.draw();
    }

    public static void main(String[] args) {
        Cir cir = new Cir();
        Rex rex = new Rex();
        Flow flow = new Flow();
        
        func(cir);
        func(rex);
        func(flow);


    }
}

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

圈复杂度:是一种描述一段代码复杂程度的方式,可以简单粗暴的一段代码中条件语句和循环语句出现的个数,这个个数就称为“圈复杂度”。如果一个方法圈复杂度太高,就需要考虑重构,不同公司对于代码的圈复杂度的规范不一样,一般不会超过10。
假如不使用多态,则要使用到if-else语句等进行判断(此时圈复杂度会很高)。

public static void main(String[] args) {
        Cir cir = new Cir();
        Rex rex = new Rex();
        Flow flow = new Flow();
        String[] shapes = {"cir", "rex", "flow","cir", "rex"};

        for (String x: shapes) {
            if (x.equals("cir")) {
                cir.draw();
            }else if (x.equals("rex")) {
                rex.draw();
            }else if (x.equals("flow")) {
                flow.draw();
            }
        }
    }

如果使用多态,则不必写那么多if-else分支语句,代码更简单。

    public static void func(Shape shape) {
        shape.draw();
    }

    public static void main(String[] args) {
        Shape[] shapes = {new Cir(), new Rex(), new Flow(),new Rex(), new Rex()};
        for (Shape x: shapes) {
            func(x);
        }
    }

分析: Shape数组:意味着每个元素都是Shope子类,父类类型的数组可以放子类类型变量。
在func方法中,shape引用 引用饿子类对象不一样,调用的方法表现出来的行为也就不一样——这种思想叫做多态。

如果要增加一个打印三角形。
对于类的实现者:定义一个三角形的类,并重写方法。

class Tri extends Shape {
    @Override
    public void draw() {
        System.out.println("画三角形");
    }
}

对于类的调用者:创建一个三角形的实例。

        Tri tri = new Tri();
        func(tri);

改动成本很低
而对于不用多态的情况,就要把if-else进行一定的改动,改动成本更高。

因此多态的优势:
①能够降低代码的“圈复杂度”,避免使用大量的if-else。
②可扩展能力强,如果要新增一种新的形状,使用多态的方式代码改动成本也比较低。
多态的缺陷:
①属性没有多态性,只有方法有多态性:当父类和子类都有同名的属性的时候,通过父类的引用,只能引用父类自己的成员属性。
②构造方法没有多态性,构造方法也不能被重写
③多态代码运行效率降低

  1. 避免在构造方法中调用重写的方法
    有坑的代码,创建两个类,B是父类,D是子类,D中重写func方法。并且在父类B的构造方法中调用func。
class B {

    public B(){
        func();
    }
    public void func() {
        System.out.println("B的方法");
    }
}

class D extends B {
    int num;
    public D() {
        num = 1;
    }
    @Override
    public void func() {
        System.out.println(num + " C重写B的方法");
    }
}


public class TestDemo2 {
    public static void main(String[] args) {
//B是父类,D是子类,D中重写func方法。并且在父类B的构造方法中调用func。
        D d = new D();
    }
}

运行结果:
在这里插入图片描述
分析:在B的构造方法中调func,刻板印象会认为调用的是B的func方法,但是通过实际的运行结果发现调用的是D中的func方法,并且num的值为0,因为此时父类B的构造方法都还没执行完成,更别说子类D的构造了。
注意:当在父类的构造方法当中,去调用父类和子类重写的方法的时候,此时会调用子类的重写方法。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CodeKnightShuai

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值