Java类和对象(四)—— 多态

发布于:2024-05-19 ⋅ 阅读:(151) ⋅ 点赞:(0)

多态

字面上理解就是同一件事物,不同的人去做会有不一样的表现,例如:同样是学习,有人理解能力很好就很快掌握知识点,有人理解能力一般,掌握的程度没有这么好~~

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

在Java中要实现多态的条件:
必须在继承体系下
子类有对父类重写的方法
必须在是通过父类的引用来调用重写的方法

class A{
    public void eat(){
        System.out.println("A正在吃饭");
    }
}

class B extends A{
    public void eat() {
        System.out.println("B");
    }
}

class C extends A{
    public void eat() {
        System.out.println("C");
    }
}

public class Test {
    public static void main(String[] args) {
        A b = new B();
        A c = new C();
        b.eat();
        c.eat();
    }
}

在这里插入图片描述

向上转型

在Java中创建一个子类对象,将其当成父类对象来使用,这就是向上转型,其实就是大范围向小范围进行转变~~

在这里插入图片描述

三种向上转型的方式

直接赋值

就是直接创建:

创建格式:
父类类名 对象名 = new 子类类名();

class A{
    //...
}

class B extends A{
    //...
}

public class Test {
    public static void main(String[] args) {
        A a = new B();
    }
}

传参

class A{
	//...
}

class B extends A{
	//...
}

public class Test {
    public static void func(A a){
		//...
    }

    public static void main(String[] args) {
        B b = new B();
        func(b);
    }
}

返回值

class A{
	//...
}

class B extends A{
	//...
}

public class Test {
    public static A func(B b){
        return b;
    }

    public static void main(String[] args) {
        B b = new B();
        A a = func(b);
    }
}

向上转型的优点:让代码实现更简单灵活。
向上转型的缺陷:不能调用到子类特有的方法

class A{
	//...
}

class B extends A{
    public void test(){
        System.out.println("test()......");
    }

}

public class Test {
    public static void main(String[] args) {
        A a = new B();
        A.test();//err 无法调用子类特有的方法
        B b = new B();
        b.test();//可以运行的
    }
}

向下转型

将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换。
在这里插入图片描述

class A{

}

class B extends A{
    public void test(){
        System.out.println("B::test()......");
    }

}

public class Test {
    public static void main(String[] args) {
        A a = new B();
        ((B) a).test();
    }
}

在这里插入图片描述

先通过向上转型,将子类的对象当成父类的对象使用时,我们需要使用子类特有的对象,这就需要向下转型,将父类引用还原为子类引用,然后调用子类的方法即可~~

如果有多个子类继承父类,刚好你需要向下转型,一旦转错子类,就会抛出异常:ClassCastException

class A{

}

class B extends A{
    public void test(){
        System.out.println("B::test()......");
    }

}

class C extends A{
    public void test(){
        System.out.println("C::test()......");
    }
}

public class Test {
    public static void main(String[] args) {
        A a = new B();

        ((C) a).test();
    }
}

在这里插入图片描述


instanceof

为了避免向下转型抛出异常,我们可以使用instanceof来检查判断:

public class Test {
    public static void main(String[] args) {
        A a = new B();
        if(a instanceof C){
            ((C) a).test();
        }else{
            ((B) a).test();
        }
    }
}

在这里插入图片描述
简单来说,instanceof会检查 a 是不是由 C 向上转型过来的,如果是就返回true,否则返回false

重写

重写(override):也称为覆盖,覆写
重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。

class Animal{
    protected String name;
    public void eat() {
        System.out.println(name + "正在吃饭");
    }
}

class Dog extends Animal{
    public void eat() {
        System.out.println(name + "正在吃狗粮");
    }

}

同样是吃饭这个行为,具体到吃什么,不同的动物会有不同的食物,在不破坏父类代码的情况下,我们会进行方法的重写,来自己定义一些特殊的功能~~


规则

重写的方法的方法名要一样,参数列表要一样,返回值可以不一样(返回的可以是父类或者子类对象,其他情况下必须一样)
当父类使用的是自己的类名作为放回置时,子类重写可以用自己的类名作为返回值,如果父类使用的是子类的类名进行返回的话,子类重写这个方法就必须用子类自己的类名作为放回了,不过一般情况下,父类是用自己的作为返回值,因为它不止有一个子类,继承是要有代码的复用性~~

不能重写父类的被private、static、fianl修饰的方法
如果子类重写父类的方法,该方法的访问权限要大于等于父类的~~

class Animal{
    protected String name;
    public Animal eat() {
        System.out.println(name + "正在吃饭");
        return null;
    }
    
    public Dog sleep(){
        System.out.println(name + "正在睡觉");
        return null;
    }
}

class Dog extends Animal{
    public Dog eat() {
        System.out.println(name + "正在吃狗粮");
        return null;
    }
    
    public Animal sleep(){  //报错,父类使用的是Dog进行返回的话,子类重写这个方法必须使用Dog
        System.out.println(name + "正在睡觉");
        return null;
	}
}

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

看一下下面的代码,思考运行结果:

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 Test2 {
    public static void main(String[] args) {
        D d = new D();
    }
}

在这里插入图片描述
在这里插入图片描述
先执行父类和子类的静态代码块,然后执行父类的实例化代码块和构造方法,在本题中父类的构造方法调用的是被重写的func方法,由于是创建子类的对象,而不是创建父类自己的对象,所以会发生向上转型去执行子类的func方法,子类的func方法中用到了num,由于还没有执行子类的实例化代码块(num = 1 的赋值操作),所以num此时是默认值为0,所以输出D.func()0

重写的原则

对于已经投入使用的类,尽量不要进行修改。最好的方式是:重新定义一个新的类,来重复利用其中共性的内容,并且添加或者改动新的内容。


重写的注解

重写的注解:@Override
可以使用 @Override 注解来显式指定。 有了这个注解能帮我们进行一些合法性校验。如果不是重写的方法,它会下面标红色的波浪线,给你提醒。
例如不小心将方法名字拼写错了 (比如写成 fanc),那么此时编译器就会发现父类中没有 fanc 方法,就会发生编译报错, 提示无法构成重写。

class A{
    public void func(){
        System.out.println("haha");
    }
}

class B extends A{
    @Override
    public void func(){
        System.out.println("hello");
    }
}

动态绑定

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

回到最开始的代码,本来是调用父类的eat方法,当程序运行时,调用的却是子类的eat方法,这就是动态绑定,也是多态发生的基础~~

静态绑定

也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表函数重载。

多态的优缺点

能够降低代码的 “圈复杂度”, 避免使用大量的 if - else
什么叫 “圈复杂度” ?
圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如
果有很多的条件分支或者循环语句, 就认为理解起来更复杂.
因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”.
如果一个方法的圈复杂度太高, 就需要考虑重构.
不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10

例如现在我们要分别输出不同的动物不同的吃饭行为,我们可以使用多态:

class Animal {
    protected String name;
    protected int age;

    public void eat(){
        System.out.println(name + "正在吃饭");
    }
}

class Dog extends Animal {
    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public void eat(){
        System.out.println(name + "正在吃狗粮");
    }
}

class Cat extends Animal {
    public Cat(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public void eat(){
        System.out.println(name + "正在吃猫粮");
    }
}

public class Test {
    public static void eat(Animal animal){
        animal.eat();
    }
    public static void main(String[] args) {
        Dog dog = new Dog("小黑",3);
        Cat cat = new Cat("小花",2);
        eat(dog);
        eat(cat);
    }
}

在这里插入图片描述

如果不使用多态,我们就要使用if else语句:

public class Test {
    public static void eat(Animal animal){
        if(animal instanceof Dog){
            ((Dog)animal).eat();
        }else if(animal instanceof  Cat){
            ((Cat)animal).eat();
        }else{
            System.out.println("nothing");
        }
    }
    public static void main(String[] args) {
        Dog dog = new Dog("小黑",3);
        Cat cat = new Cat("小花",2);
        eat(dog);
        eat(cat);
    }
}

多态还有一个优点:可扩展能力更强
如果要新增一种动物的吃饭行为,使用多态的方式代码改动成本也比较低。

多态缺陷:代码的运行效率降低。
1.属性没有多态性,当父类和子类都有同名属性的时候,通过父类引用,才能引用父类自己的成员属性。
2.构造方法没有多态性,例如上面提到的,避免在构造方法中调用重写的方法,这涉及到对象的初始化,如果在构造方法调用重写的方法可能无法发生你想要的效果。


网站公告

今日签到

点亮在社区的每一天
去签到