深度剖析—继承和多态

继承
什么是继承?
先举一个例子:

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; //编译出错
1
2
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");
1
传参
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("卟卟");
    // 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,这个过程就成为运行时绑定 (动态绑定)

反汇编验证:

找到对应目录
.
找到主函数,并与代码结合
.
重写 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 语言中没有"继承"概念,同样也可以实现多态
无论哪种语言,多态的核心都是让调用者不必关注对象的具体类型,这也是降低用户使用成本的一种重要方式

多态是面向对象程序设计中比较难理解的部分,会在后续的抽象类和接口中进一步体会多态的使用,重点是多态带来的编码上的好处
————————————————
版权声明:本文为CSDN博主「一朵花花」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/m0_47988201/article/details/120069256

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值