认识多态(Java)

1.什么是多态

多态是java面向对象编程的一个重要特性,通俗来讲就是一个事物,多种形态。具体来讲就是同一种事情,作用在不同的对象上时,会得到不一样的结果。

1.1 多态的实现条件

Java中要实现多态,需要满足以下条件。

  • 必须在继承体系下
  • 子类必须对父类中的方法进行重写
  • 通过父类的引用调用重写的方法

现在我们可以先给个例子简单了解一下多态的实现。
Animal类(父类)

public class Animal {

    public String name;
    public int age;

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

    public void eat(){

        System.out.println(this.name + " 正在吃饭....");
    }

}

Dog类(子类)

public class Dog extends Animal{

    public Dog(String name,int age){
        super(name,age);
    }

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

    public void bark(){
        System.out.println(this.name + "正在汪汪叫....");
    }
}

Cat类(子类)

public class Cat extends Animal{

    public Cat(String name,int age){
        super(name,age);
    }

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

    public void mew(){
        System.out.println(this.name + "正在喵喵叫....");
    }
}

多态实现

	public static void main(String[] args) {

        Animal animal1 = new Dog("大黄",3);
        animal1.eat();
        System.out.println("---------");
        Animal animal2 = new Cat("咪咪",1);
        animal2.eat();

    }

此时分别调用eat()方法的结果为
在这里插入图片描述
animal1引用的是Dog的对象,animal2引用的是Cat的对象,两个引用都调用了eat()方法,但是所得到的结果不一样,这是因为父类引用了不一样的对象。那么这种通过父类引用调用不同的子类对象中对父类重写的方法,得到不一样的结果,这种思想我们就称之为多态。

那么满足这些条件需要我们了解重写、向上转型以及动态绑定。

2. 重写

2.1 什么是重写

所谓重写就是子类对父类中非静态、非private修饰,非final修饰,非构造方法等方法的内部进行重新编写,要求方法名一样、参数(数据类型、参数个数、参数顺序)一样、返回值一样,那么称父类和子类中这两个方法构成了重写。例如上面代码中的重写:
在这里插入图片描述

重写的规则:

  • 子类在重写父类的方法时,⼀般必须与父类方法原型⼀致: 返回值类型 方法名 (参数列表) 要完
    全⼀致
  • 被重写的方法返回值类型可以不同,但是必须是具有父子关系的(一般是一样的,不同时要遵循父子关系)
  • 访问权限不能比父类中被重写的方法的访问权限更低。
  • 重写的方法, 可以使用 @Override 注解来显式指定。它可以帮助我们判断重写的方法是否满足重写的规则,若不满足就会编译报错,无法构成重写。(例如我们将父类的eat()方法重写时错误的写成了ate(),此时方法名不一样,就会提示编译报错)

2.2 不同访问修饰限定符下的重写规则

  1. 子类当前方法的访问修饰限定符 要大于等于 父类的权限
    (权限大小:private < 包访问权限 < protected < public (! 被private修饰的方法比较特殊))
  2. 被private修饰的成员方法,不能被重写
    (被private修饰的方法是私有方法,只能在本类中调用,即使拥有父子关系也无法进行访问调用,因此更不能进行重写了)
  3. 被static修饰的成员方法,不能被重写
  4. 被final修饰的成员方法,不能被重写

2.3 重写与重载的区别

区别重写重载
参数列表一定不能修改(类型,个数,顺序)必须修改
返回类型一定不能修改(或是构成父子关系)可以修改
访问限定符子类的访问权限大于等于父类可以修改

3. 向上转型和向下转型

3.1 向上转型

所谓向上转型,就是创建一个子类对象,然后通过父类引用,将其当作父类对象来使用,由此可以知道向上转型必须发生在继承下。(并且这只是类型的转换,不考虑父子类内部的成员变量以及成员方法)。

它的语法格式:父类类型 对象名 = new 子类类型();可以传参构造

Animal animal1 = new Dog("大黄",3);

例如,Animal作为父类,Dog作为子类,animal1引用了子类的对象,是一个从小范围到大范围的转换,是向上转型。

3.1.1 向上转型的使用场景
  1. 直接赋值
    Animal animal1 = new Dog("大黄",3);这就是直接赋值。
  2. 方法传参

    此时就是将子类对象作为实参,父类引用变量作为形参,在方法传参的过程中进行了向上转型。
    代码:
	public static void func(Animal animal){
        animal.eat();
    }

    public static void main(String[] args) {

        Dog dog = new Dog("旺财",1);
        func(dog);
        Cat cat = new Cat("肥波",2);
        func(cat);

        //更为简洁的方法传参
        func(new Dog("大白",2));
    }
  1. 作为方法返回值
	public Animal func1(){
        
        Dog dog = new Dog("旺财",2);
        return dog;
        
        //简洁形式
        //return new Dog("旺财",2);
    }

将父类类型作为返回类型,return时返回子类的对象。

此时又有疑问了,向上转型不是只能执行父类的方法吗,为什么当父类引用子类对象时调用的方法执行了子类重写方法的结果呢,这就是发生了动态绑定,下面会介绍。

3.2 向下转型

通过向上转型我们只能访问父类的成员方法,无法对子类方法调用;而有时候需要用到子类特有的方法,此时将父类引用还原为子类对象即可,也就是把父类类型给到子类类型,这就是向下转型。
例如:

public class Dog extends Animal{

    public Dog(String name,int age){
        super(name,age);
    }

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

    public void bark(){
        System.out.println(this.name + "正在汪汪叫....");
    }
}
//向下转型
    public static void main(String[] args) {

        Dog dog = new Dog("旺财",2);

        Animal animal = dog;
        Dog dog1 = (Dog) animal;
        dog1.bark();

    }

我们将子类dog对象向上转型为Animal类型,但是我们此时又想调用子类特有的bark()方法,那么我们将父类向下转型还原为子类对象,此时就可以调用了。

但是向下转型用的一般比较少,是因为其不安全,如果还原失败,运行时就会抛出异常。例如这个向下转型:

在这里插入图片描述
可以看到编译器并没有提示我们错误,因此如果在项目中这样写,一定是不安全的。那么此时我们可以借助关键字instanceof 来判断引用是否正确。

  • instanceof:
	public static void main(String[] args) {

        Animal animal = new Dog("旺财",1);
        
        if(animal instanceof Dog){
            Dog dog = (Dog) animal;
            dog.bark();
        }
        if (animal instanceof Cat){
            Cat cat = (Cat) animal;
            cat.mew();
        }
    }

instanceof 可以帮助判断被转型的父类是否是这个子类类型,如果是返回true,不是则返回false,可以提高代码的安全性,因此我们在使用向下转型时一定要使用instanceof 关键字。

4. 动态绑定

  • 静态绑定:也被称为前期绑定(早绑定),即在编译时,根据用户传递的实参类型就可以确定调用的是哪个方法。典型代表函数的重载。
  • 动态绑定:也被称为后期绑定(晚绑定),即在编译时,不能确定调用的是哪个方法,只有在运行时,才能确定具体调用的是哪个方法。
    在这里插入图片描述
    例如上面的animal.eat()通过.class字节码文件看到,在编译时认为使用的还是父类的eat()方法,但是在运行的时候,才知道我真正执行的是子类重写的方法,这就是动态绑定。

在了解了重载、向上转型与动态绑定后,回顾最开始的多态例子,是否理解的更清晰了一点。

  • 此外,属性是没有多态性的,当父类和子类都有同名属性时,通过父类引用,只能引用父类自己的成员属性。
  • 通过动态绑定执行的是子类重写的方法,那什么时候执行父类自己的方法呢。当子类没有对父类方法进行重写时,执行的是父类自己的方法,也就没有动态绑定;而当子类重写了父类方法时,执行的一定是子类重写方法。

5.多态实现的优缺点

使代码更加简洁,并能够降低代码的"圈复杂度",所谓圈复杂度就是if-else的个数,如果一个方法的圈复杂度太高就要考虑重构,一般不会超过10。
例如我们现在打印多个形状,如果不使用多态,那么实现方法是这样的。
首先我们给出父类与继承子类

public class Shape {
    public void drowmap(){
        System.out.println("打印一个图形...");
    }
}

class Cycle extends Shape{
    @Override
    public void drowmap() {
        System.out.println("打印一个圆形...");
    }
}

class Rect extends Shape{
    @Override
    public void drowmap() {
        System.out.println("打印一个矩形...");
    }
}

class Triangle extends Shape{
    @Override
    public void drowmap() {
        System.out.println("打印一个三角形...");
    }
}

class Flower extends Shape{
    @Override
    public void drowmap() {
        System.out.println("打印一朵花...");
    }
}

那么我们现在要分别打印一个圆形,一个矩形和一个三角形。

	public static void main(String[] args) {
        Cycle cycle = new Cycle();
        Rect rect = new Rect();
        Triangle triangle = new Triangle();

        String[] Shapes = new String[]{"cycle","rect","triangle"};

        for(String shape : Shapes){
            if(shape.equals("cycle")){
                cycle.drowmap();
            }else if (shape.equals("rect")){
                rect.drowmap();
            }else if(shape.equals("triangle")){
                triangle.drowmap();
            }
        }
    }

在方法中我们使用了三个if 条件判断,并且如果我们现在要多打印一个额外的图形,那么我们不仅要对数组添加,还要再加一个if-else 条件判断,太复杂,但如果使用的是多态:

	public static void main(String[] args) {

        Shape[] shapes = {new Cycle(),new Rect(),new Triangle(),new Flower()};

        for(Shape shape : shapes){
            shape.drowmap();
        }
    }

当我们使用了多态,代码不仅变得更加简洁,还增加了扩展性,如果我们要增加打印的图形,只需要在Shape类型的数组里添加就行。

多态缺点:不能访问子类特有的方法,只能通过向下转型来调用。

6. 不要在构造方法中使用多态

在构造方法中也会发生动态绑定

public class NumB {

    public NumB(){
        func();
    }

    public void func()
    {
        System.out.println("NumB.func() ");
    }
}

class NumD extends NumB{
    int num = 1;

    public NumD(){
        super();
    }

    @Override
    public void func() {
        System.out.println("NumD.func() " + num);
    }
}

public class Test {

    public static void main(String[] args) {

        NumD numd = new NumD();

    }
}
  1. 在构造NumD对象的同时,会先进行父类NumB的构造
  2. 而NumB的构造方法中调用了func()方法,此时会进行动态绑定,调用到NumD中的func()方法
  3. 此时NumD中num还未进行实例化,值默认为0,最终输出为0。

因此在构造方法中尽量避免使用实例方法,这个简单了解就可以。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值