深度剖析—继承和多态

Posted 一朵花花

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深度剖析—继承和多态相关的知识,希望对你有一定的参考价值。


之前提到OOP语言的基本三大特性:继承、封装、多态
本篇重点讨论:继承和多态

继承

什么是继承?

先举一个例子:

class Animal{
    public String name;
    public void eat(){
        System.out.println("Animal::eat()");
    }
    public void sleep(){
        System.out.println("Animal::sleep()");
    }
}
class Cat{
    public String name;
    public void eat(){
        System.out.println("Cat::eat()");
    }
    public void sleep(){
        System.out.println("Cat::sleep()");
    }
    public void mem(){
    System.out.println("Cat::mem()");
    }
}
class Bird{
    public String name;
    public void eat(){
        System.out.println("Bird::eat()");
    }
    public void fly(){
        System.out.println("Bird::fly()");
    }
}


由以上三个类,不难发现:

  • 三个类都具备一个相同的 name 属性,而且意义也完全一样
  • 三个类都具备一个相同的 eat 方法,而且行为也完全一样
  • 从逻辑上讲,Cat 和 Bird 都是一种Animal

此时我们就可以让 Cat 和 Bird 分别继承 Animal类,来实现代码复用的效果
Animal,这种被继承的类,称为:父类 / 基类 / 超类
Cat / Bird,这种继承的类,称为:子类 / 派生类

子类和父类的关系,就像现实生活中儿子继承父亲的财产类似,子类也会继承父类的字段和方法,以达到代码复用的效果

继承的语法

class 子类 extends 父类 {

}

注意事项

  • 使用 extends 指定父类,Java中使用 extends 只能继承一个类
  • Java中的一个子类只能继承一个父类,即:在Java中只有单继承
    (C++ / Python等语言支持多继承)
  • 子类会继承父类所有 public 的字段和方法
  • 对于父类中 private 的字段和方法,子类中是无法访问的
  • 子类的实例中,也包含着父类的实例,可以使用 super 关键字得到父类实例的引用

则上述举例改为继承为:

class Animal{
    public String name;
    public void eat(){
        System.out.println(this.name + "Animal::eat()");
    }
    public void sleep(){
        System.out.println(this.name + "Animal::sleep()");
    }
}
class Cat extends Animal{
    public void mem(){
        System.out.println("Cat::mem()");
    }
}
class Bird extends Animal{
    public void fly(){
        System.out.println("Bird::fly()");
    }
}

继承的意义

子类继承了父类除构造方法外所有的
面相对象思想中提出了继承的概念,专门用来进行共性抽取,实现代码复用,若不继承的话,那么重复的代码会非常多!

super 关键字

在子类方法中访问父类的成员。
子类在构造的时候,要先帮助父类进行构造

class Animal{
    public String name;
    public Animal(String name){
        this.name = name;
    }
}
class Cat extends Animal{
    public Cat(String name){
        super(name); //显式调用,不是继承
        System.out.println("Cat(String)");
    }
}

super 和 this 关键字的区别🔺

①.this关键字:当前对象的引用

  1. this( );调用本类中其他的构造方法
  2. this.data;访问当前类中的属性
  3. this.func( );调用本类中其他的成员方法

②.super关键字:代表父类对象的引用

  1. super( );调用父类中的构造方法,必须放到第一行
  2. super.data;访问父类中的属性
  3. super.func( );访问父类中的成员方法

protected 关键字

在类和对象篇,为了实现封装特性,Java中引入了访问限定符
主要限定:类或者类中成员能否在类外或者其他包中被访问

访问修饰符本类同包子类其他
private
public
protected
默认(default)

总结: private < default < protected < public

  1. private:只有类的内部能访问
  2. public:类内部和类的调用者都能访问
  3. protected:类内部 / 子类和同一个包中的类可以访问,其他类不能访问
  4. 默认(包访问权限):类内部能访问,同包中的类可以访问,其他类不能访问

若把字段设置为 private 时,会发现子类不能访问,但设成 public,又违背了"封装",这就引入了 protected 关键字 ,其主要体现在继承上

  • 对于类的调用者来说,protected 修饰的字段和方法是不能访问的
  • 对于类的子类和同一个包的其他类来说,protected修饰的字段和方法是可以访问的

1.同包中的同一类:

public class Animal {
 	protected String name;
}

2.同包中的不同类:

3.不同包中的子类:

多层继承

class Animal{
    protected String name;
    public Animal(String name){
        this.name = name;
        System.out.println("Animal(String)");
    }
    public void eat(){
        System.out.println(this.name + "Animal::eat()");
    }
    private void sleep(){
        System.out.println(this.name + "Animal::sleep()");
    }
}

class Cat extends Animal{
    public Cat(String name){
        super(name);   //显式调用,不是继承
        System.out.println("Cat(String)");
    }
    public void mem(){
        System.out.println(this.name + "Cat::mem()");
    }
}

class ChineseGardenCat extends Cat{
    public ChineseGardenCat(String name){
        super(name);
    }
}


一般继承关系不超过三层,若继承层次太多,就需要考虑对代码进行重构

final 关键字

如果想从语法上进行限制继承,则可以使用 final 关键字
final关键可以用来修饰变量、成员方法以及类

1.修饰变量或字段,表示常量

常量:即只能被初始化一次,故不能修改

final int a = 6;
a = 8; //编译出错

2.修饰类,表示此类不能被继承

功能是 限制 类被继承
final修饰类,也叫做:密封类

final public class Animal {
 ...
}
public class Bird extends Animal {
 ...
}

上述代码会编译出错,final 修饰的类被继承的时候,就会编译报错
我们平时是用的 String 字符串类,就是 final 修饰的,不能被继承

3.修饰方法,表示此类不能被继承

继承和组合

和继承类似,组合也是一种表达类之间关系的方式,也是能够达到代码重用的效果

public class Students{
       ...
}
public class Teachers{
       ...
}
public class School{
	public Students[] students;
	public Teachers[] teachers
}

组合并没有涉及到特殊的语法,仅仅是将一个类的实例作为另外一个类的字段

继承表示对象之间是 is-a 的关系
组合表示对象之间是 has-a 的关系

顺序表,链表,都运用到了组合,可以翻看之前相关博客内容
组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择,一般建议:能用组合尽量用组合

多态

多态概念

同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性
简单的说:就是用基类的引用指向子类的对象

多态前提:

  1. 父类引用子类对象
  2. 父类和子类有同名的覆盖方法
  3. 通过父类引用,调用这个重写的方法

向上转型

理解多态之前,需要理解:向上转型—将子类对象赋值给父类引用


向上转型后,通过父类的引用 只能访问父类自己的方法和属性,即:父类引用只能访问自己特有的

向上转型发生的几种情况?

  1. 直接赋值

上边例子就是直接赋值

Animal animal = new Cat("mimi");
  1. 传参
public static void func(Animal animal){
    animal.eat();
}
public static void main(String[] args) {
    Cat cat = new Cat("mimi");
    func(cat);
}
  1. 返回值
public static Animal func(){
    Cat cat = new Cat("mimi");
    return cat;
}
//返回值 向上转型
public static void main(String[] args) {
    Animal animal = func();
    animal.eat();
}

向下转型

向上转型是子类对象转成父类对象,向下转型是父类对象转成子类对象,相比于向上转型来说,向下转型并不常见,但也有一定用途

举例:

class Animal{
    protected String name;
    public Animal(String name){
        this.name = name;
        System.out.println("Animal(String)");
    }
    public void eat(){
        System.out.println(this.name + " " + "Animal::eat()");
    }
    private void sleep(){
        System.out.println(this.name + " " + "Animal::sleep()");
    }
}

class Bird extends Animal {
    public Bird(String name){
        super(name);
    }
    public void fly(){
        System.out.println(this.name + " " + "Bird::fly()");
    }
}

public static void main(String[] args) {
    Animal animal = new Bird("卟卟");
    animal.eat();
    //向下转型  父类的引用赋值给了子类
    Bird bird = (Bird)animal;
    bird.fly();
}

输出结果:


向下转型的不安全性:


为了提高向下转型的安全性,引入了 instanceof,若该表达式为true,则可以安全转换

public static void main(String[] args) {
    Animal animal = new Cat("卟卟");
    // A instanceof B 判断A是否为B的实例
    if(animal instanceof Bird){
        Bird bird = (Bird)animal;
        bird.fly();
    }
    else{
        System.out.println("异常!");
    }
}

输出结果:异常!

向下转型非常不安全,一般很少使用

动态绑定 (运行时绑定)

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

举例:

class Animal{
    protected String name;
    public Animal(String name){
        this.name = name;
        System.out.println("Animal(String)");
    }
    public void eat(){
        System.out.println(this.name + " " + "Animal::eat()");
    }
}

class Cat extends Animal {
    public int count = 66;
    public Cat(String name){
        super(name);//显式调用,不是继承
        System.out.println("Cat(String)");
    }
    public void eat(){
        System.out.println(this.name + "喵喵喵Cat::eat()");
    }
}

public static void main(String[] args) {
    Animal animal = new Cat("mimi");
    animal.eat();
}

调用的是Animal(父类)的eat,运行时为Cat(子类)的eat,这个过程就成为运行时绑定 (动态绑定)

反汇编验证:

  1. 找到对应目录
    .
  2. 找到主函数,并与代码结合
    .

重写 override

子类实现父类的同名方法,并且参数的类型和个数完全相同,这种情况称为:覆写 / 重写 / 覆盖
重写,即 外壳不变,核心重写

之前的 方法篇 提到过重写的概念以及重写和重载的区别

重写重载
方法名称相同相同
返回值相同不做要求
参数列表相同不同(参数个数 / 参数类型)
不同的类(继承关系上)同一个类

重写注意事项🔺

  • static 修饰的静态方法不能重写,但是能够被再次声明
    声明为 final 的方法不能被重写
    构造方法不能被重写
  • 参数列表与被重写方法的参数列表必须完全相同
  • 重写中,子类的方法的访问权限不能低于父类的方法访问权限
  • 如果不能继承一个类,则不能重写该类的方法
  • 私有的方法是不能被重写的

坑来了~

在构造器中调用重写的方法:在构造方法中,可以发生动态绑定

先上代码,便于后面理解

class Animal{
    protected String name;
    public Animal(String name){
        this.name = name;
        //System.out.println("Animal(String)");
        eat();
    }
    public void eat(){
        System.out.println(this.name + " " + "Animal::eat()");
    }
    private void sleep(){
        System.out.println(this.name + " " + "Animal::sleep()");
    }
}

class Cat extends Animal {
    public int count = 66;
    public Cat(String name){
        super(name);//显式调用,不是继承
        //System.out.println("Cat(String)");
    }
    public void mem(){
        System.out.println(this.name +" " + "Cat::mem()");
    }
    public void eat(){
        System.out.println(this.name + "喵喵喵Cat::eat()");
    }
}

public static void main(String[] args) {
    Cat cat = new Cat("米米");
    //cat.eat();
}

在主函数里构造 Cat对象 时,会调用父类的构造方法,而父类的构造方法会调用eat,而最后打印的结果是子类中的eat
即:在构造器中调用重写的方法,也会发生动态绑定 (运行时绑定)

  • 构造 Cat 对象的同时,会调用 Animal 的构造方法
  • Animal 的构造方法中调用了 eat 方法,此时会触发动态绑定,会调用到 Cat 中的 eat

尽量不要在构造器中调用方法(如果这个方法被子类重写,就会触发动态绑定,但是此时子类对象还没构造完成,可能会出现一些隐藏的但是又极难发现的问题

多态好处:

1.类调用者对类的使用成本进一步降低

  • 封装是让类的调用者不需要知道类的实现细节,只管调用共有的方法即可,降低了代码管理的复杂度
  • 多态能让类的调用者连这个类的类型是什么都不用知道,只需要知道这个对象具有什么方法即可

2.能够降低代码的"圈复杂度",避免使用大量的条件语句

多态总结:

抛开Java,多态是一个更广泛的概念

  • C++中的"动态多态"和 Java 中的多态类似
    C++中的"静态多态",就和继承体系没有关系了
  • Python 中的多态性体现的是"鸭子类型",与继承体系没有关系
  • Go 语言中没有"继承"概念,同样也可以实现多态

无论哪种语言,多态的核心都是让调用者不必关注对象的具体类型,这也是降低用户使用成本的一种重要方式

多态是面向对象程序设计中比较难理解的部分,会在后续的抽象类和接口中进一步体会多态的使用,重点是多态带来的编码上的好处

以上是关于深度剖析—继承和多态的主要内容,如果未能解决你的问题,请参考以下文章

java中封装,继承,多态,接口学习总结

深度剖析 javascript call方法实现继承的原理

day25 多态和封装

多态实现原理剖析

深度剖析fork()的原理及用法

面向对象的三大特性 鸭子类型 类的约束 super的深度剖析