之前提到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关键字:当前对象的引用
- this( );调用本类中其他的构造方法
- this.data;访问当前类中的属性
- this.func( );调用本类中其他的成员方法
②.super关键字:代表父类对象的引用
- super( );调用父类中的构造方法,必须放到第一行
- super.data;访问父类中的属性
- super.func( );访问父类中的成员方法
protected 关键字
在类和对象篇,为了实现封装特性,Java中引入了访问限定符 主要限定:类或者类中成员能否在类外或者其他包中被访问
访问修饰符 | 本类 | 同包 | 子类 | 其他 |
---|
private | √ | | | | public | √ | √ | √ | √ | protected | √ | √ | √ | | 默认(default) | √ | √ | | |
总结: private < default < protected < public
- private:只有类的内部能访问
- public:类内部和类的调用者都能访问
- protected:类内部 / 子类和同一个包中的类可以访问,其他类不能访问
- 默认(包访问权限):类内部能访问,同包中的类可以访问,其他类不能访问
若把字段设置为 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 的关系
顺序表,链表,都运用到了组合,可以翻看之前相关博客内容 组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择,一般建议:能用组合尽量用组合
多态
多态概念
同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性 简单的说:就是用基类的引用指向子类的对象
多态前提:
- 父类引用子类对象
- 父类和子类有同名的覆盖方法
- 通过父类引用,调用这个重写的方法
向上转型
理解多态之前,需要理解:向上转型—将子类对象赋值给父类引用
向上转型后,通过父类的引用 只能访问父类自己的方法和属性,即:父类引用只能访问自己特有的
向上转型发生的几种情况?
- 直接赋值
上边例子就是直接赋值
Animal animal = new Cat("mimi");
- 传参
public static void func(Animal animal){
animal.eat();
}
public static void main(String[] args) {
Cat cat = new Cat("mimi");
func(cat);
}
- 返回值
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("卟卟");
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,这个过程就成为运行时绑定 (动态绑定)
反汇编验证:
- 找到对应目录
. - 找到主函数,并与代码结合
.
重写 override
子类实现父类的同名方法,并且参数的类型和个数完全相同,这种情况称为:覆写 / 重写 / 覆盖 重写,即 外壳不变,核心重写
之前的 方法篇 提到过重写的概念以及重写和重载的区别
| 重写 | 重载 |
---|
方法名称 | 相同 | 相同 | 返回值 | 相同 | 不做要求 | 参数列表 | 相同 | 不同(参数个数 / 参数类型) | 类 | 不同的类(继承关系上) | 同一个类 |
重写注意事项🔺
- static 修饰的静态方法不能重写,但是能够被再次声明
声明为 final 的方法不能被重写 构造方法不能被重写 - 参数列表与被重写方法的参数列表必须完全相同
- 重写中,子类的方法的访问权限不能低于父类的方法访问权限
- 如果不能继承一个类,则不能重写该类的方法
- 私有的方法是不能被重写的
坑来了~
在构造器中调用重写的方法:在构造方法中,可以发生动态绑定
先上代码,便于后面理解
class Animal{
protected String name;
public Animal(String name){
this.name = name;
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);
}
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,而最后打印的结果是子类中的eat 即:在构造器中调用重写的方法,也会发生动态绑定 (运行时绑定)
- 构造 Cat 对象的同时,会调用 Animal 的构造方法
- Animal 的构造方法中调用了 eat 方法,此时会触发动态绑定,会调用到 Cat 中的 eat
尽量不要在构造器中调用方法(如果这个方法被子类重写,就会触发动态绑定,但是此时子类对象还没构造完成,可能会出现一些隐藏的但是又极难发现的问题
多态好处:
1.类调用者对类的使用成本进一步降低
- 封装是让类的调用者不需要知道类的实现细节,只管调用共有的方法即可,降低了代码管理的复杂度
- 多态能让类的调用者连这个类的类型是什么都不用知道,只需要知道这个对象具有什么方法即可
2.能够降低代码的"圈复杂度",避免使用大量的条件语句
多态总结:
抛开Java,多态是一个更广泛的概念
- C++中的"动态多态"和 Java 中的多态类似
C++中的"静态多态",就和继承体系没有关系了 - Python 中的多态性体现的是"鸭子类型",与继承体系没有关系
- Go 语言中没有"继承"概念,同样也可以实现多态
无论哪种语言,多态的核心都是让调用者不必关注对象的具体类型,这也是降低用户使用成本的一种重要方式
多态是面向对象程序设计中比较难理解的部分,会在后续的抽象类和接口中进一步体会多态的使用,重点是多态带来的编码上的好处
|