java中的多态


1、向上转型

1.1 什么叫向上转型

向上转型用一句话介绍就是:用父类的引用变量去引用子类实例

//父类
class Fruit{
    public void show() {
        System.out.println("this is a fruit");
    }
}

//子类
class Apple extends Fruit{
    public void test() {
        System.out.println("i am a apple");
    }
}

Fruit fruit = new Apple();

可以通过父类引用去调用父类方法,但不能调用子类方法,例如:
在这里插入图片描述
在这里插入图片描述

当向上转型之后,父类引用变量可以访问子类中属于父类的属性和方法,但是不能访问子类独有的属性和方法。上述代码中父类引用调用子类的test()方法则会报错

1.2 哪些情况下会发生向上转型

  1. 子类直接赋值给父类
    例如:
Fruit fruit = new Apple();
  1. 父类作为函数的参数,将子类作为参数进行调用
    例如:
public static void func(Fruit fruit) {
}
func(new Apple());
  1. 函数返回的类型是父类,实际返回子类
    例如:
public static Fruit func() {
	return (new Apple());
}

2、动态绑定

对前面的代码稍加修改, 给 Apple类也加上同名的 show 方法,并且在两个 show 中分别加上不同的日志信息
再去调用会出现什么情况呢?

//父类
class Fruit{
    public void show() {
        System.out.println("this is a fruit");
    }
}

//子类
class Apple extends Fruit{
    public void show() {
        System.out.println("this is a Apple");
    }
}

在这里插入图片描述

此时, 我们发现:

  • fruit1和 fruit2虽然都是Fruit类型的引用,但是 fruit1指向Fruit类型的实例, fruit2 指向Fruit类型的实例。
  • 针对 fruit1和 fruit2分别调用 show方法,发现 fruit1.show()实际调用了父类的方法, 而fruit2.show()实际调用了子类的方法

因此,在 Java 中,调用某个类的方法,究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) ,要看究竟这个引用指向的是父类对象还是子类对象。这个过程是程序运行时决定的(而不是编译期),因此称为动态绑定。

动态绑定必须满足两个条件:

  1. 父类引用去引用子类对象
  2. 通过这个父类引用调用父类或子类同名覆盖的方法。

子类和父类的同名覆盖方法又叫重写


3、方法重写

对于Apple类来说,show方法就是重写方法

重写方法必须满足的条件:

1.子类和父类的方法名相同
2.方法的参数列表相同(包括参数的个数以及参数的类型)

关于重写的注意事项

  1. 重写和重载完全不一样。不要混淆
  2. 普通方法可以重写,static 修饰的静态方法不能重写
  3. 重写中子类的方法的访问权限不能低于父类的方法访问权限
  4. 重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同,特殊情况除外(协变))
  5. private方法不能够重写
  6. 被final修饰的方法不能被重写

子类重写方法权限缩小问题示例:将子类的 show方法 改成 private
在这里插入图片描述
所以切记第三点:重写中子类的方法的访问权限不能低于父类的方法访问权限

另外, 针对重写的方法, 可以使用 @Override 注解来显式指定
在这里插入图片描述
有了这个注解能帮我们进行一些合法性校验。例如不小心将方法名字拼写错了 (比如写成 shwo),那么此时编译器就会发现父类中没有 aet 方法,就会编译报错,提示无法构成重写
在这里插入图片描述

因此,推荐在代码中进行重写方法时显式加上 @Override 注解


重载和重写的区别
在这里插入图片描述

4、理解多态

所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。

Java实现多态有三个必要条件:继承、重写、向上转型。

继承:在多态中必须存在有继承关系的子类和父类。

重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。

向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。

只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为

多态的实现:

class Fruit{
    public void show() {
        System.out.println("fl eat Fruit");
    }
}

class Apple extends Fruit{
    @Override
    public void show() {
        System.out.println("fl eat Apple");
    }
}

class Banana extends Fruit {
    @Override
    public void show() { System.out.println("fl eat Banana"); }
}

在这里插入图片描述
当类的调用者在编写 show这个方法的时候,参数类型为 Fruit(父类),此时在该方法内部并不知道,也不关注当前的 fruit引用指向的是哪个类型(哪个子类)的实例。此时fruit这个引用调用 show方法可能会有多种不同的表现(和fruit对应的实例相关),这种行为就称为多态

使用多态的好处是什么?

  1. 类调用者对类的使用成本进一步降低
    封装是让类的调用者不需要知道类的实现细节
    多态能让类的调用者连这个类的类型是什么都不必知道,只需要知道这个对象具有某个方法即可

  2. 能够降低代码的 “圈复杂度”,避免使用大量的 if - else

  3. 可扩展能力更强。如果要新增一种新的形状,使用多态的方式代码改动成本也比较低

5、向下转型

向上转型是子类对象转成父类对象,向下转型就是父类对象转成子类对象。相比于向上转型来说,向下转型没那么常见,但是也有一定的用途

class Fruit{
    public void show() {
        System.out.println("fl eat Fruit");
    }
}

class Apple extends Fruit{
    @Override
    public void show() {
        System.out.println("fl eat Apple");
    }
    public void eat() {
        System.out.println("eat Apple");
    }
}

class Banana extends Fruit {
    @Override
    public void show() { System.out.println("fl eat Banana"); }
}

在这里插入图片描述
因为父类的引用无法调用到子类的方法。

此时,我们可以使用向下转型,让父类的引用调用子类方法
在这里插入图片描述

但是这样的向下转型有时是不太可靠的。例如:
在这里插入图片描述
fruit本质上引用的是一个 Banana对象,是不能转成 Apple对象的。运行时就会抛出异常
也可以理解为:不是所有的水果都是苹果

所以,为了让向下转型更安全,我们可以先判定一下看看 Fruit 本质上是不是一个 Banana实例,再来转换
在这里插入图片描述
instanceof 可以判定一个引用是否是某个类的实例。如果是,则返回 true,这时再进行向下转型就比较安全了

6、在构造方法中调用重写的方法(一个坑)

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

在这里插入图片描述

  • 构造 D 对象的同时,会调用 B 的构造方法.
  • B 的构造方法中调用了 func 方法,此时会触发动态绑定,会调用到 D 中的 func
  • 此时 D 对象自身还没有构造,,时 num 处在未初始化的状态,值为 0

7、抽象类

在生活中,吃东西是一个非常广泛的概念。我可以对其进行细化,例如:吃米饭,吃包子,吃面条等。因此,可以将吃东西设计成为一个抽象方法,包含抽象方法的成为抽象类

abstract class Eat {
	abstract public void eating();
}
  • 在 eat方法前加上 abstract 关键字,表示这是一个抽象方法。同时抽象方法没有方法体(没有 { },不能执行具体代码)
  • 对于包含抽象方法的类,必须加上 abstract 关键字表示这是一个抽象类

注意事项:

1.抽象类不能直接实例化
在这里插入图片描述

2.抽象方法不能是 private 的

在这里插入图片描述

3.抽象类中可以包含其他的非抽象方法,也可以包含字段。这个非抽象方法和普通方法的规则都是一样的,可以被重写,也可以被子类直接调用

abstract class Eat {
    abstract public void eating();

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

class EatFood extends Eat {
    public void eating() {
        System.out.println("eat food");
    }

}

在这里插入图片描述

抽象类的作用
抽象类存在的最大意义就是为了被继承
抽象类本身不能被实例化,要想使用,只能创建该抽象类的子类,然后让子类重写抽象类中的抽象方法

普通的类也可以被继承呀,普通的方法也可以被重写呀,为啥非得用抽象类和抽象方法呢?

确实如此。但是使用抽象类相当于多了一重编译器的校验
当我们将一个类设置为抽象类的时候,就表明实际工作不应该由父类完成,而应由子类完成。那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的。但是父类是抽象类就会在实例化的时候提示错误,让我们尽早发现问题

很多语法存在的意义都是为了 “预防出错”,例如我们曾经用过的 final 也是类似。创建的变量用户不去修改,不就相当于常量嘛?但是加上 final 能够在不小心误修改的时候,让编译器及时提醒我们。充分利用编译器的校验,在实际开发中是非常有意义的

抽象类小总结:

  1. 包含抽象方法的类,叫做抽象类
  2. 什么是抽象方法,一个没有具体实现的方法,被abstract修饰
  3. 抽象类是不能被实例化的
  4. 因为不能被实例化,所以这个抽象类,只能被继承
  5. 抽象类中,也可以包含和普通类一样的成员和方法
  6. 一个普通类,继承了一个抽象类,那么这个普通类当中,需要重写这个抽象类中的所有抽象方法
  7. 抽象类最大的作用就是被继承
  8. 一个抽象类A,如果继承了一个抽象类B,那么这个抽象类A,可以不实现抽象父类B的抽象方法
  9. 结合第8点,当A类再次被其他普通类继承时,那么A和B两个抽象类中的抽象方法必须被重写
  10. 抽象类不能被final修饰,抽象方法也不能被final修饰
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值