【JavaSE】JavaSE之继承和多态

JavaSE继承和多态

  • 一. 继承
    • 1.1 为什么需要继承
    • 1.2 继承概念
    • 1.3 继承的语法
    • 1.4 父类成员访问
    • 1.5 super关键字
    • 1.6 子类构造方法:
    • 1.7 super和this
    • 1.8 再谈初始化
    • 1.9 protected 关键字
    • 1.10 继承方式
    • 1.11 final 关键字
    • 1.12 继承与组合
  • 二. 多态
    • 2.1 多态的概念
    • 2.2 向上转移和向下转型
    • 2.3 重写
    • 2.4 多态实现条件
    • 2.5 多态的优缺点
    • 2.6 避免在构造方法中调用重写的方法

一. 继承

1.1 为什么需要继承

Java中使用类对现实世界中实体来进行描述,类经过实例化之后的产物对象,则可以用来表示现实中的实体,可是世界上这么多错综复杂的事物,有着各自的特性也有各自的共同点,我们该如何用类去简单有效去描述他们呢?
例如一只猫和一只狗,我们把他们各自都定义一个类:
【JavaSE】JavaSE之继承和多态_第1张图片
通过上面的描述,我们可以看出来他们都有相同的共性和各自的特点,其中有很多共性都是重复的,为了实现代码高效化,那么我们怎样把共性进行抽取呢?面相对象思想中提出了继承的概念,专门用来进行共性抽取,实现代码复用

1.2 继承概念

继承(inheritance)机制:是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特 性的基础上进行扩展,增加新功能,这样产生新的类,称派生类
继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。继承主要解决的问题是:共性的抽取,实现代码复用。
如上面提到的猫和狗,他们的共性都是动物,所以可以生成一个动物类,来抽取他们的共性,其中猫和狗称为:***子类,也可以是派生类。***其中动物称为:父类,基类,或者超类

1.3 继承的语法

在Java中如果要表示类之间的继承关系,需要借助extends关键字
如我们上文提到的猫和狗类,抽取他们的共性生成动物类:
【JavaSE】JavaSE之继承和多态_第2张图片
然后使用关键字extends来继承共性:
【JavaSE】JavaSE之继承和多态_第3张图片
这样我们就实现了代码的复用
然后对他们进行赋值调用:
【JavaSE】JavaSE之继承和多态_第4张图片【JavaSE】JavaSE之继承和多态_第5张图片

注意:

  1. 子类会将父类中的成员变量或者成员方法继承到子类中了。
  2. 子类继承父类之后,要新添加自己特有的成员,体现出与基类的不同,否则就没有必要继承了。

继承也可以看作是" is a "的关系,猫"是一个"动物。

1.4 父类成员访问

在继承体系中,子类将父类中的方法和字段继承下来了,那在子类中能否直接访问父类中继承下来的成员呢?
1.4.1 子类中访问父类的成员变量

  1. 子类和父类不存在同名成员变量
    【JavaSE】JavaSE之继承和多态_第6张图片

  2. 子类和父类成员变量同名
    【JavaSE】JavaSE之继承和多态_第7张图片
    其中this的范围可以作用于父类和子类,而super(后文详讲)只能作用于父类(但是也不可以说super代表父类对象的引用,因为父类没有实例化对象)。

在子类方法中 或者 通过子类对象访问成员时:

  1. 如果访问的成员变量子类中有,优先访问自己的成员变量
  2. 如果访问的成员变量子类中无,则访问父类继承下来的,
  3. 如果父类也没有定义,则编译报错。如果访问的成员变量与父类中成员变量同名,则优先访问自己的,即:子类将父类同名成员隐藏了。
  4. 成员变量访问遵循就近原则,自己有优先自己的,如果没有则向父类中找。

1.4.2 子类中访问父类的成员方法

  1. 成员方法名字不同
    【JavaSE】JavaSE之继承和多态_第8张图片
    成员方法没有同名时,在子类方法中或者通过子类对象访问方法时,则优先访问自己的,自己没有时再到父类中找,如果父类中也没有则报错。

  2. 成员方法名字相同
    【JavaSE】JavaSE之继承和多态_第9张图片

在这上面两个方法构成了重载,重载只能在一个类里面或者构成继承关系,所以符合重载要求。

说明:

  1. 通过子类对象访问父类与子类中不同名方法时,优先在子类中找,找到则访问,否则在父类中找,找到则访问,否则编译报错。
  2. 通过派生类对象访问父类与子类同名方法时,如果父类和子类同名方法的参数列表不同(重载),根据调用方法适传递的参数选择合适的方法访问,如果没有则报错;如果父类和子类同名方法的原型一致(重写-后面讲),则只能访问到子类的,父类的无法通过派生类对象直接访问到。

如果子类中存在与父类中相同的成员时,那如何在子类中访问父类相同名称的成员呢?

1.5 super关键字

由于一些特殊原因,在父类和子类中可能存在相同成员,如果要在子类方法中访问父类同名成员时,该如何操作?直接访问是无法做到的,Java提供了super关键字,该关键字主要作用:在子类方法中访问父类的成员。
在这里插入图片描述
这样就可以直接获取到父类的成员数据,在子类方法中,如果想要明确访问父类中成员时,借助super关键字即可。

注意:

  1. 只能在非静态方法中使用
  2. 在子类方法中,访问父类的成员变量和方法。

后文还会详细介绍super

1.6 子类构造方法:

子类对象构造时,需要先调用父类构造方法,然后执行子类的构造方法。
在父类中构造一个方法:
【JavaSE】JavaSE之继承和多态_第10张图片

在主函数实例化一个对象之后,必须在子类里先对父类进行构造
在这里插入图片描述

此处我们用super进行对父类的赋值,然后再初始化子类成员变量,必须注意super放在第一行,或者可以直接在构造方法里传参:在实例化的时候就传值
【JavaSE】JavaSE之继承和多态_第11张图片

1.7 super和this

相同点:

  1. 都是Java中的关键字
  2. 只能在类的非静态方法中使用,用来访问非静态成员方法和字段
  3. 在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在

不同点:

  1. this是当前对象的引用,当前对象即调用实例方法的对象,super相当于是子类对象中从父类继承下来部分成员的引用
  2. 在非静态成员方法中,this用来访问本类的方法和属性super用来访问父类继承下来的方法和属性
  3. this是非静态成员方法的一个隐藏参数,super不是隐藏的参数
  4. 成员方法中直接访问本类成员时,编译之后会将this还原,即本类非静态成员都是通过this来访问的;在子类中如果通过super访问父类成员,编译之后在字节码层面super实际是不存在的(通过字节码文件可以验证)
  5. 在构造方法中:this(…)用于调用本类构造方法,super(…)用于调用父类构造方法,两种调用不能同时在构造方法中出现
  6. 构造方法中一定会存在super(…)的调用,用户没有写编译器也会增加,但是this(…)用户不写则没有

1.8 再谈初始化

当我们在子类和父类当中都写了各自的静态代码块和实例代码块,以及构造方法的时候,这时候的执行顺序和我们之前讲的有点小差异,执行顺序应该是:静态代码块–>实例代码块–>构造方法,在这个地方我们引进了继承,所以我们的执行顺序应该是:父类和子类的静态代码块–>父类的实例代码块和构造方法–>子类的实例代码块和构造方法,且注意这里的静态代码块还是只执行一次
【JavaSE】JavaSE之继承和多态_第12张图片

总结:

  1. 静态代码块先执行,并且只执行一次,在类加载阶段执行
  2. 当有对象创建时,才会执行实例代码块,实例代码块执行完成后,最后构造方法执行
  3. 父类静态代码块优先于子类静态代码块执行,且是最早执行
  4. 父类实例代码块和父类构造方法紧接着执行
  5. 子类的实例代码块和子类构造方法紧接着再执行
  6. 第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行

1.9 protected 关键字

在类和对象中我们提到过protected的作用域,它可以被同一包中的同一类,同一包中的不同类,不同包中的子类给访问,可以通过super关键字访问,切记不可以再main函数中用super,否则报错,而且被protected修饰的那个类必须是被public修饰的,否则没有权限访问这个包。
父类中private成员变量在子类中不能直接访问,但是也继承到子类中了
那么我们该如何区别使用这些关键字呢?要尽量做到 “封装”, 即隐藏内部实现细节, 只暴露出 必要 的信息给类的调用者.因此我们在使用的时候应该尽可能的使用 比较严格 的访问权限. 例如如果一个方法能用 private, 就尽量不要用 public,所以我们必须明确写代码的时候认真思考, 该类提供的字段方法到底给 “谁” 使用(是类内部自己用, 还是类的调用者使用, 还是子类使用).

1.10 继承方式

通过我们对继承的学习,我们还可以深挖继承的各种形式,继承又分为:单继承,多层继承,不同类继承同一个类,多继承。
单继承就是我们前面写的B继承A,一个类继承一个类。多层继承就是B继承A,C又继承B。不同类继承同一个类是B继承A,C也继承A。多继承是C继承A和B。
【JavaSE】JavaSE之继承和多态_第13张图片

但是在Java里是不支持多继承的:我们写的类是现实事物的抽象,而我们真正在公司中所遇到的项目往往业务比较复杂,,可能会涉及到一系列复杂的概念, 都需要我们使用代码来表示,,所以我们真实项目中所写的类也会有很多.,类之间的关系也会更加复杂,但是即使如此,,我们并不希望类之间的继承层次太复杂.,一般我们不希望出现超过三层的继承关系.,如果继承层次太多,就需要考虑对代码进行重构了,如果想从语法上进行限制继承, 就可以使用 final 关键字。

1.11 final 关键字

final关键可以用来修饰变量、成员方法以及类。

  1. 修饰变量或字段,表示常量(即不能修改)
    在这里插入图片描述

  2. 修饰类:表示此类不能被继承(密封类)
    在这里插入图片描述
    我们平时是用的 String 字符串类, 就是用 final 修饰的, 不能被继承.

  3. 修饰方法:表示该方法不能被重写(后序介绍)

1.12 继承与组合

和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果。组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段。
继承表示对象之间是is-a的关系,比如:狗是动物,猫是动物
组合表示对象之间是has-a(is part of)的关系,比如:学校
【JavaSE】JavaSE之继承和多态_第14张图片

学校可以复用学生的属性和方法,也可以复用老师的属性和方法。
【JavaSE】JavaSE之继承和多态_第15张图片

这里的小学又把学校的各种属性和方法继承下来了
组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择,一般建议:能用组合尽量用组合。

二. 多态

2.1 多态的概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同 的状态
比如汽车:有的汽车动力来源是汽油,有的汽车动力是柴油。
比如动物:猫和狗都吃粮食,猫吃猫粮,狗吃狗粮。
总的来说:同一件事情,发生在不同对象身上,就会产生不同的结果。

2.2 向上转移和向下转型

2.2.1 向上转型
向上转型:实际就是创建一个子类对象,将其当成父类对象来使用(父类引用子类对象)。
语法格式:父类类型 对象名 = new 子类类型();
在这里插入图片描述
animal是父类类型,但可以引用一个子类对象,因为是从小范围向大范围的转换。
在子类中可以访问父类的方法,而在父类中不可以访问子类的方法,只能访问父类自己特有的成员,因为父类没有这个方法

使用场景:

  1. 直接赋值
    在这里插入图片描述

  2. 方法传参
    【JavaSE】JavaSE之继承和多态_第16张图片

  3. 方法返回
    【JavaSE】JavaSE之继承和多态_第17张图片

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

2.2.2 向下转型
将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换。
【JavaSE】JavaSE之继承和多态_第18张图片

向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。如向上转型就是所有的狗是动物,向下转型就是所有的动物都是狗,明显不正确。Java中为了提高向下转型的安全性,引入了 instanceof ,如果该表达式为true,则可以安全转换。(了解即可)
【JavaSE】JavaSE之继承和多态_第19张图片

2.3 重写

重写(override):也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰等的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。子类的访问修饰限定符一定要大于等于父类的访问修饰限定符的范围。
此处拓展一下:协变类型:比如返回值构成父子类关系,父类重写方法返回值是父类类名,子类重写方法返回值是子类类名。
规则:

  1. 子类在重写父类的方法时,一般必须与父类方法原型一致:修饰符 返回值类型 方法名(参数列表) 要完全一致
  2. JDK7以后,被重写的方法返回值类型可以不同,但是必须是具有父子关系的
  3. 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为 protected
  4. 父类被static、private修饰的方法都不能被重写。
  5. 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法。
  6. 子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法。
  7. 重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写.

当我们在子类和父类当中都写了一个一摸一样的eat方法(重写)的时候,这时候主函数引用eat方法时会输出子类的结果,但是实际上编译的时候这里还是父类的eat,但是在运行的时候,变成了子类自己的方法了,运行时绑定,我们称为动态绑定,是多态的基础。
【JavaSE】JavaSE之继承和多态_第20张图片

那我们如何证明在编译的时候是父类的方法呢?打开我们的字节码文件,然后查看主函数:
【JavaSE】JavaSE之继承和多态_第21张图片

重写和重载的区别:
【JavaSE】JavaSE之继承和多态_第22张图片

可以看出:方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。

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

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

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

2.4 多态实现条件

前提:

  1. 向上转型:父类引用 引用子类对象
  2. 发生重写:父类和子类当中有同名的覆盖方法
  3. 通过父类引用,调用这个同名的方法,此时会发生动态绑定

思想:同一个方法,因为这个调用方法的引用,引用的对象不同,所执行的行为也是不一样的
多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法。
【JavaSE】JavaSE之继承和多态_第23张图片

上面是各种重写继承,下面则是传递不同的对象。
【JavaSE】JavaSE之继承和多态_第24张图片

2.5 多态的优缺点

优点:

  1. 能够降低代码的 “圈复杂度”, 避免使用大量的 if - else
    圈复杂度是一种描述一段代码复杂程度的方式,一段代码如果平铺直叙, 那么就比较简单容易理解,而如果有很多的条件分支或者循环语句,就认为理解起来更复杂,因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”.如果一个方法的圈复杂度太高,就需要考虑重构。
  2. 可扩展能力更强
    如果要新增一种新的方法,使用多态的方式代码改动成本也比较低, 只要创建一个新类的实例就可以了。

缺点:代码的运行效率降低

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

我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用 func
我们可以知道以下状况:

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

结论: “用尽量简单的方式使对象进入可工作状态”, 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成),可能会出现一些隐藏的但是又极难发现的问题。

继承和多态就完全写完了,你收获到了吗?

你可能感兴趣的:(JavaSE,java)