Java之多态

本文详细解释了Java中的多态概念,包括为什么使用多态,多态的基础(动态绑定),以及重写、向上转型和向下转型的原理、规则和应用场景。通过实例展示了如何在代码中实现多态,讨论了其优点和缺点,以及在构造方法中调用重写方法的风险。
摘要由CSDN通过智能技术生成

一、多态前言

1.为什么要使用多态

Java中使用多态的主要目的是提高代码的可重用性和扩展性,使得代码更加灵活和易于维护。通过多态,我们可以将不同的对象看做是同一种类型,从而使得我们可以使用同一种接口来操作这些对象,而不必关心具体的实现细节。

2.多态概念

当父类的引用所指向的子类对象引用指向的对象不一样时。调用重写的方法,所表现出来的行为是不一样的,我们把这种思想叫做多态。上面所说的可能大家会觉得有点抽象,看到后面就懂了。
多态的基础是动态绑定,所以要了解多态前提我们还要了解动态绑定。
要想实现动态绑定,需要满足以上几个条件:
1.要发生向上转型
2.要发生重写
3.使用父类对象的引用去调用重写方法
完成了这三部分,就会发生动态绑定,而在这里,出现了重写以及向上转型这些概念。所以我们得先了解它们才能去了解动态绑定。进而了解多态。

二、重写

1.重写的概念

重写 (override) :也称为覆盖。将父类的方法重新在子类中使用。 返回值和形参都不能改变 即外壳不变,核心重写! 重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。

方法重写的规则:
1.子类在重写父类的方法时,必须与父类方法原型一致:即返回值、方法名、参数列表要完全一致
2.被重写的方法的访问修饰限定符在子类中要大于等于父类的。
3.父类中被static或private或final修饰的方法以及构造方法都不能被重写。 
4.在子类中重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验。

2.重写的作用

对于已经投入使用的类,尽量不要进行修改。最好的方式是:重新定义一个新的类,来重复利用其中共性的内容,并且添加或者改动新的内容。
例如:若干年前的手机,只能打电话,发短信,来电显示只能显示号码,而今天的手机在来电显示的时候,不仅仅可以显示号码,还可以显示头像,地区等。在这个过程当中,我们不应该在原来老的类上进行修改,因为原来的 类,可能还在有用户使用 ,正确做法是: 新建一个新手机的类,对来电显示这个方法重写就好了,这样就达到了我 们当今的需求了

三、向上转型

向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。
语法格式:父类类型 对象名
= new 子类类型 ()
Animal animal = Dog ( );
我们对以上代码进行实质化分析,以上的代码其实是省略化了,见以下代码

Dog dog = new Dog();
 Animal animal = dog;//该代码发生了向上转换,将Dog对象转换为Animal类型

通过向上转型后,就可以父类对象名来访问子类的方法了。使用animal.eat();这语句来访问
这个语句发生了动态绑定(在编译过程中调用的其实是父类的eat,但是在运行时换为调用子类的eat了)故实现了 创建一个子类对象,将其当成父类对象来使用。见以下代码

class Animal {

void sound() {

System.out.println("Animal makes a sound");

}

}

class Dog extends Animal {

 void sound() {

System.out.println("Dog barks");

}

 void fetch() {

System.out.println("Dog fetches a ball");

}

}

public class Main {

public static void main(String[] args) {

Dog dog = new Dog(); // 创建Dog对象

 Animal animal = dog; // 向上转型,将Dog对象转换为Animal类型

 animal.sound();//调用子类的覆盖方法

 // animal.fetch(); // 编译错误,因为Animal类中没有fetch方法

}

}

// 输出: Dog barks

通过以上代码发现一个问题不能调用到子类特有的方法(因为编译时调用的是父类的方法),我们可以通过向下转型来调用到子类特有的方法(后面介绍)
静态绑定:也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表函数重载。
动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法。当发生重写时,通过父类调用该方法时会发生动态绑定。

向上转型 使用场景
1. 直接赋值
2. 方法传参
3. 方法返回
见以下代码

public class TestAnimal {

// 2. 方法传参:形参为父类型引用,可以接收任意子类的对象

public static void eatFood(Animal a){ //因为主方法的原因使用静态方法

a.eat();  //方法传参向上转型

}

// 3. 作返回值:返回任意子类对象的实例

public static Animal buyAnimal(String var){

return new Dog();

}

public static void main() {

Animal cat = new Cat("元宝",2); // 1. 直接赋值:子类对象赋值给父类对象

Dog dog = new Dog("小七", 1);

animal.eat();  //直接赋值向上转型

eatFood(cat);  //两种传参方式都可

eatFood(dog); 

Animal animal = buyAnimal();  

animal.eat();//方法返回向上转型

}

}

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

四、多态的实现

多态具体点就是去完成某个行为时,当不同的对象去完成同一件事时(调用eat方法)会产生出不同的状态。代码如下:

class Animal {

    public void eat(){

        System.out.println( "吃饭");

    }

}

 class Cat extends Animal{

    @Override //注解

    public void eat(){

        System.out.println("吃鱼~~~");

    }

}

 class Dog extends Animal {

    @Override

    public void eat(){

        System.out.println("吃骨头~~~");

    }

}

public class TestAnimal {

    public static void eat(Animal a){

        a.eat(); //两次调用该方法,但是结果却不一样

    }

    public static void main(String[] args) {

        Cat cat = new Cat();

        Dog dog = new Dog();

        eat(cat);

        eat(dog);

    }

}

//输出结果

吃鱼~~~
吃骨头~~~

此时在上述代码中当父类的引用所指向的子类对象引用指向的对象不一样时。调用重写的方法(eat),所表现出来的行为是不一样的(输出结果不一样),我们把它叫做多态。

五、向下转型

将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时可以实例化子类,然后调用子类方法即可。我们其实还可以将父类引用再还原为子类对象即可,即 向下转型
语法格式:子类类型 对象名
= (强制转换)父类对象名

 Dog myDog = (Dog) animal;

那么以上代码为什么要强制类型转换呢?向上转型可以不用,因为是从小范围向大范围的转换。(可以类比整型里面的强制转换),我们现在提出一个问题:什么时候都可以向下转型吗?
答案是不,在Java中,向下转型(将父类引用转换为子类引用)一般需要先进行向上转型
见以下代码

class Animal {

    void sound() {

        System.out.println("Animal的sound");

    }

    void sun() {

        System.out.println("Animal特有的sun");

    }

}

class Dog extends Animal {

    void sound() {

        System.out.println("Dog的sound");

    }

    void fetch() {

        System.out.println("Dog特有的fetches ");

    }

}

public class Mainn {

    public static void main(String[] args) {

        Animal animal = new Dog(); // 向上转型

        Dog myDog = (Dog) animal; // 向下转型

        myDog.sound(); // 输出: Dog barks,调用子类的覆盖方法

        myDog.fetch(); // 输出: Dog fetches a ball,调用子类特有的方法

        myDog.sound(); // 输出: Dog barks,调用子类的覆盖方法

    }

}

如果上面的代码没有 Animal animal = new Dog();,向下转型将报错,同时注意必须确保父类引用所指向的对象确实是子类的实例。如果父类引用所指向的对象不是子类的实例,那么即使进行了向上转型,向下转型也是不安全的:见以下代码

class Parent {}
class Child extends Parent {}
class AnotherChild extends Parent {}

public class Main {
public static void main(String[] args) {
Parent parent = new AnotherChild(); // 向上转型
 // 这里如果尝试向下转型为Child,编译器将会报错
 // Child child = (Child) parent;//不安全的向下转型
}
}

//为了演示方便,这个代码是不完整的

因此,向下转型之前,你需要确保父类引用所指向的对象确实是你要转型的子类的实例。这通常通过instanceof操作符来检查:用来判断parent是否为Child的实例,若是,返回true,否则返回false


if (parent instanceof Child) {
    Child child = (Child) parent; //
安全的向下转型
} else {
   ......                         // 不能转换为Child
}

我们最后思考一个问题:向上转型的缺陷是不能调用到子类特有的方法,那么向下转型可以调用父类特有的方法吗?是可以的,同时向下转型后不会影响向上转型的操作。见以下代码

class Animal {

    void sound() {

        System.out.println("Animal的sound");

    }

    void sun() {

        System.out.println("Animal特有的sun");

    }

}

class Dog extends Animal {

    void sound() {

        System.out.println("Dog的sound");

    }

    void fetch() {

        System.out.println("Dog特有的fetches ");

    }

}

public class Mainn {

    public static void main(String[] args) {

        Animal animal = new Dog(); // 向上转型

        Dog myDog = (Dog) animal; // 向下转型

        myDog.sound(); // 输出: Dog barks,调用子类的覆盖方法

        myDog.fetch(); // 输出: Dog fetches a ball,调用子类特有的方法

    

        myDog.sound(); // 输出: Dog barks,调用子类的覆盖方法

        myDog.sun();   //观察到向下转型过程中可以调用父类的特有的方法

        animal.sun();   //观察到向下转型后不会影响向上转型的操作

        animal.sound();

    }

}

//输出结果

Dog的sound
Dog特有的fetches 
Dog的sound
Animal特有的sun
Animal特有的sun
Dog的sound

六、多态的优缺点

如我们现在需要打印的不是一个形状了 , 而是多个形状 . 如果不基于多态 , 实现代码如下

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("❀");

    }

}

public class Mainn {

    public static void main(String[] args) {

            Rect rect = new Rect();

            Cycle cycle = new Cycle();

            Flower flower = new Flower();

            String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};

            for (String shape : shapes) {

                if (shape.equals("cycle")) {

                    cycle.draw();

                } else if (shape.equals("rect")) {

                    rect.draw();

                } else if (shape.equals("flower")) {

                    flower.draw();

            }

        }

    }

}

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

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("❀");

    }

}

public class Mainn {

    public static void main(String[] args) {

            Shape[] shapes = {new Cycle(), new Rect(), new Cycle(),

                    new Rect(), new Flower()};

            for (Shape shape : shapes) {

                shape.draw();

            }

        }

            }

如果要新增一种新的形状 , 使用多态的方式代码改动成本也比较低 .见以下代码
//公共部分
class Triangle extends Sjx {
@Override
public void draw() {
System.out.println("△");
}
}
//If lese 改动方式

Sjx sjx=new Sjx();

            String[] shapes = {"cycle", "rect", "cycle", "rect", "flower", "sjx"};

            for (String shape : shapes) {

                if (shape.equals("cycle")) {

                    cycle.draw();

                } else if (shape.equals("rect")) {

                    rect.draw();

                } else if (shape.equals("flower")) {

                    flower.draw();

                }else if(shape.equals("sjx"){

                        sjx.draw();

                    }

            }

        }

}

//多态改动方式

public class Mainn {

    public static void main(String[] args) {

        Shape[] shapes = {new Cycle(), new Rect(), new Cycle(),

                new Rect(), new Flower(),new Sjx()};

        for (Shape shape : shapes) {

            shape.draw();

                    }

            }

        }

对于类的调用者来说 (drawShapes 方法 ), 只要创建一个新类的实例就可以了 , 改动成本很低 .
而对于不用多态的情况 , 就要把 drawShapes 中的 if - else 进行一定的修改 , 改动成本更高 .
多态缺陷:
1. 属性没有多态性
当父类和子类都有同名属性的时候,通过父类引用,只能引用父类自己的成员属性
2. 构造方法没有多态性
见如下代码 ~

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

class B {
public B () {
// do nothing
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 . func () 0 // 此时子类对象还没构造完成,故num的值为0
结论:尽量不要在构造器中调用方法 ( 如果这个方法被子类重写 , 就会触发动态绑定, 但是此时子类对象还没构造完成 ), 可能会出现一些隐藏的但是又极难发现的问题 .
评论 104
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值