• 类与对象(十七)----继承extend


    继承是什么?

    面向对象三大巨头知识点----继承
    有一个大黄猫类:
    属性有:名字,年龄,颜色
    方法有:吃鱼,跳,跑
    还有一个大白猫类:
    属性有:名字,年龄,颜色
    方法有:吃猫罐头,跳,跑
    可以看到除了吃的方法不一样,其他的几乎都是一样的,那么如果使用代码写出来,就会重复的工作,代码耦合度较高。两只猫或许浪费的时间不多,但如果是成百上千个这种猫类,就比较繁琐。
    因此,为了缓解代码的耦合度,可以使用继承这个特性来解决。

    继承的概念

    继承可以解决代码复用,让编程更接近人类思维,当多个类存在相同的属性和方法时,可以从这些类中抽象出父类,在父类中定义相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过extends关键字来声明继承符类即可

    • 当子类继承父类时,就会自动拥有父类定义的属性和方法
    • 父类又叫 超类 基类
    • 子类又叫派生类

    继承的基本语法

    class 子类名 extends 父类名{
    }
    继承示意图
    开头说的那个案例:我们可以将耦合度高的属性和方法抽象出来,重新定义一个父类Cat类,后面所有的猫类不管是黄猫 白猫 小猫 大猫,都继承Cat类。这样就不用再次定义重复的属性和方法了

    • 当子类继承父类后也可以再定义自己的特有的属性和方法。
    • 当子类既有父类的属性和方法,也定义了自己特有的属性和方法再继承给别的类时,别的类就会同时有顶级父类和继承父类的所有属性和方法
      ![在这里插入图片描述](https://img-blog.csdnimg.cn/a3b323282fce48728b14f7514497e218.png

    C可以再往下继承,属性方法继续叠加。注意不同分支继承下去的,属性不会继承(例如P类不会有C类的属性)

    继承入门案例

    以开头说到的猫类问题,使用继承解决代码复用性高的问题
    有一个大黄猫类:
    属性有:名字,年龄,颜色
    方法有:吃鱼,跳,跑

    还有一个大白猫类:
    属性有:名字,年龄,颜色
    方法有:吃猫罐头,跳,跑
    根据以上信息发现可抽象出来的属性有:名字,年龄,颜色 方法有跳,跑
    因此可以定义一个父类 Cat类 然后创建大黄猫类和大白猫类继承Cat类

    //父类
    public class Cat {
        String name;
        int age;
        String color;
        public void run(){
            System.out.println("正在跑~~");
        }
        public void jump(){
            System.out.println("正在跳~~");
        }
    }
    //子类
    public class WhiteCat extends Cat{
        public void eat(){
            System.out.println("吃罐头");
        }
    }
    //子类2
    public class YellowCat extends Cat{
        public void eat(){
            System.out.println("吃鱼");
        }
    }
    //测试类
    public class TestExtends {
        public static void main(String[] args) {
            WhiteCat xiaobai = new WhiteCat();
            xiaobai.name = "大白猫";
            xiaobai.age = 4;
            xiaobai.color = "白色";
            System.out.println("白猫信息如下:"+xiaobai.name+xiaobai.color+xiaobai.age+"岁");
            xiaobai.eat();
            xiaobai.run();
            YellowCat xiaohuang = new YellowCat();
            xiaohuang.name = "大黄猫";
            xiaohuang.age = 5;
            xiaohuang.color = "黄色";
            System.out.println("黄猫信息如下:"+xiaobai.name+xiaobai.color+xiaobai.age+"岁");
            xiaohuang.eat();
            xiaohuang.run();
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    输出结果:
    在这里插入图片描述
    可以看到,虽然两只猫类没有定义属性,还是可以输出赋值,这就是因为继承了Cat的属性和方法。同时也定义了自己的方法eat。

    • 注意点:子类继承父类属性是copy的形式,连同父类赋的值一起copy。需要自己修改成自己想要的值。
      假设将父类Cat类的name属性赋值cat112。然后在测试类中实例化,先不修改自己的name直接输出看看,然后修改再输出看看
    public class TestExtends {
        public static void main(String[] args) {
            WhiteCat xiaobai = new WhiteCat();
            YellowCat xiaohuang = new YellowCat();
            System.out.println(xiaobai.name);
            xiaobai.name = "白色";
            System.out.println(xiaobai.name);
            System.out.println(xiaohuang.name);
    
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    输出结果
    在这里插入图片描述
    可以看到没有自己修改前,输出的值是父类自己的值。修改后才变成了自己的。没有修改的黄猫的name还是父类的值
    因此得出结论:子类继承父类的属性连同赋的值会随着变量一起copy到对象中,需要自己修改。

    继承细节

    继承细节一:父类的不同修饰符,子类该如何访问。以及父类子类不同包的解决方案

    • 子类继承了父类的所有属性和方法,权限内的属性和方法可以直接在子类访问。权限外的需要通过父类提供public修饰的方法去访问。
      列如 父类中分别定义了:
      public int n1;
      protected int n2;
      int n3;
      private int n4

      那么其中n1和n2,不管子类和父类同不同包,都可以直接访问。
      n3因为是默认修饰符,只有父类和子类在同一个包内,才可以访问。不在同包,子类不可以直接访问,需要通过父类通过的公开方法才能访问。
      n4 不管同包还是不同包,子类都只能通过父类提供的公开方法访问

    对于父类的的私有属性,或者是子类没有权限访问的属性。都可以通过跟封装一样的set/get方法来获取和进行修改。

    继承细节二:子类继承父类时,构造器的细节

    重点要素:子类继承父类时,必须先调用一次父类的构造器(默认调用父类的无参构造器)

    • 子类继承父类时,首先会调用父类的构造器,然后再调用自己的构造器。
      案例演示:
    //父类
    public class Boos {
        public Boos(){//父类的无参构造器
            System.out.println("父类的无参构造器被调用");
        }
    }
    //子类
    public class Ceo extends Boos{
        public Ceo(){
            System.out.println("子类Ceo的无参构造器被调用");
        }
    }
    //测试类
    public class Test02 {
        public static void main(String[] args) {
            Ceo ceo = new Ceo();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    输出效果
    在这里插入图片描述

    • 因为子类继承父类时会默认调用父类的无参构造器,但是又因为当在类中自己手写了有参构造器,无参构造器就会消失这个特性。所以会导致当父类有有参构造器而无参构造器没有写出来时,子类继承父类时就会报错:找不到父类的无参构造器。

    所以为了解决这个问题有两种方法:

    1. 将父类的无参构造器再写出来
    2. 在子类的构造器中使用super关键字代入对应的父类有参构造器的参数,以达到让编译器使用父类的有参构造器的切换。

    案例演示:

    //父类
    public class Boos {
    public String name;
        public Boos(String name){//父类的有参构造器
        this.name = name;
            System.out.println("父类的有参构造器被调用");
        }
    }
    //子类
    public class Ceo extends Boos{
        public Ceo(){
        super("张三");
            System.out.println("子类Ceo的无参构造器被调用");
        }
    }
    //测试类
    public class Test02 {
        public static void main(String[] args) {
            Ceo ceo = new Ceo();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    因为子类继承父类时,通过super触发了父类的有参构造器。所以父类的name被赋值了张三,然后再传给子类Ceo所有属性和方法,因此ceo的name也是张三

    • 当父类和子类都有相同参数的有参构造器,实例化子类对象时会发生什么
    1. 例如一个父类Boss类,手写出他的有参构造器用于构造器修改属性
    //父类
    public class Boos {
    public String name;
        public Boos(String name){//父类的有参构造器
        this.name = name;
            System.out.println("父类的有参构造器被调用");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2.子类同时也写出子类的有参构造器用于构造器修改属性

    //子类
    public class Ceo extends Boss{
        public Ceo(String name){
            super("张三");
            System.out.println("子类此时的name是:"+this.name);
            this.name = name;
            System.out.println("子类Ceo的有参构造器被调用");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    1. 然后在测试类中实例化子类,并输出子类的name属性
    public class Tesst03 {
        public static void main(String[] args) {
            Ceo c1 = new Ceo("李四");
            System.out.println(c1.name);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    输出结果:
    在这里插入图片描述
    通过输出结果和debug可以看到,通过实例化子类,首先调用的是父类的构造器,将参数穿入父类的构造器,变成张三,然后再调用子类的构造器传入参数变成李四。
    通过以上案例测试可以清晰的发现,实例化子类传入的实参( Ceo c1 = new Ceo(“李四”);)是传入子类的构造器的,而父类实际的实参需要在子类的构造器中由super定义。不通用

    • 因此单想要指定调用调用父类的某个构造器时,需在子类的构造器中显示调用:super(参数列表);
    super关键字语句只能写在构造器中,且只能是第一条语句

    因为super的这个特性和this关键字在构造器中调用另一条构造器的特性相同,都只能是第一条语句,因此this();和super();只能存在一个,否则会冲突。
    那么 可以实验以下super关键字不显写出来,而是让子类默认的去调用父类无参构造器,然后再写this看看会不会冲突。

    //父类
    public class Boos {
        public String name;
        public Boos(){//父类的有参构造
            System.out.println("父类的无参构造器被调用");
        }
    }
    //子类
    public class Ceo extends Boos{
        public Ceo(){
            this("张三");
            System.out.println("子类Ceo的无参构造器被调用");
        }
        public Ceo(String name){
            this.name = name;
            System.out.println("子类Ceo的有参构造器被调用");
        }
    }
    //测试类
    public class Test02 {
        public static void main(String[] args) {
            Ceo ceo = new Ceo();
            System.out.println(ceo.name);
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    输出结果
    在这里插入图片描述
    可以看到没有编译错误,也就是说如果要在子类构造器中使用this调用其他构造器,那么就不能写super指定调用父类构造器,只能让编译器默认调用父类的无参构造器。

    java所有的类都是Object类的子类,Object类是所有类的基类
    • 父类构造器的调用不限于直接父类,它会一直向上追溯直到Object类(顶级父类),然后从Object的构造器开始,依次往下进行调用构造器
      举例说明:
      例如 上面案例的Boos类和它的子类Ceo类,Boos类也是继承于Object类。当实例化子类Ceo时,因为子类中有默认调用的父类构造器,所以会找到Boos的构造器,同理Boos的构造器中同样也有对Object的默认调用构造器。然后从object–>Boos–>Ceo依次进行构造器的调用

    在这里插入图片描述

    继承细节三:子类最多只能直接继承一个父类,java是单继承机制

    现在有三个类 A类 B类 C类
    要求让 B类同时有C类A类的属性和方法
    根据java中单继承的规定,不可以直接B类继承C类的同时又继承A类,所以我们可以A类继承C类,然后B类继承C类。这样就实现了要求

    在这里插入图片描述
    ps:虽然java不允许多继承,但是可以实现多个接口

    继承不可以滥用,必须满足is a逻辑关系

    例如 Person is a Muisc 人是一个音乐,显然是错误的 Cat is a Animal 猫是一个动物,就满足了is a逻辑 Cat类就可以继承Animal类

    继承的本质分析

    当实例化一个子类的时候内存发生了什么?
    先看以下代码
    Son - extends-Father-extends-GrandPa

    //爷爷类
    public class GrandPa {
        String name = "大头爷爷";
        String hobby = "旅游";
    }
    //父类
    public class Father extends GrandPa{
        String name = "大头爸爸";
        int age = 31;
    }
    //子类
    public class Son extends Father{
        String name = "大头儿子";
    }
    //测试类实例化子类
    Son son = new Son();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    此时在内存中依次执行了哪些操作:

    1. 在方法区依次加载:Object类 GrandPa类 Father类 Son类
    2. 在堆中开辟空间,并在此空间内划分不同区域依次存放各个继承类的属性
    3. 将此空间的地址给到Son
      图示:
      在这里插入图片描述
      从示意图可以看到在堆中开辟的只有一个空间,而在这个空间中有不同的区域划分,以防止同名的属性冲突。
    • 那么问题就随之而来了如果在测试类实例化son类后,输出son.name。到底输出的是哪个的name呢。
    Son son = new Son();
    System.out.println(son.name);
    
    • 1
    • 2

    输出结果为 大头儿子。
    在继承中,查找每个属性是按照查找关系来返回信息的
    步骤会如下:

    1. 首先查看子类自己有没有这个属性
    2. 如果子类有,且可以访问,则就返回信息
    3. 如果子类没有这个属性,就看父类(Father类)有没有也
    4. 如果父类(Father类)也没有,就继续向上查找GrandPa类,再没有就Object类。
      也就是依向上查找,并且一旦找到之后如果可以访问就直接返回,如果是不可以访问的修饰符,也不会向上查找另一个了,就直接就地报错没有权限。
    • 就像如果要输出子类son的age,最终会输出 31
      在这里插入图片描述
      查找 hobby
      在这里插入图片描述
      如果再查找过程中发现查找的属性 找到了 但是修饰符是private无法访问,就会报错,只能通过父类提供的公开访问方法访问

    继承练习

    练习一:

    以下代码会输出什么:

    class A{
        A(){
            System.out.println("a");
        }
    }
    class B extends A{
        B(){
            this("abc");
            System.out.println("b");
        }
        B(String name){
            System.out.println("b name");
        }
    }
    //测试类
    B b = new B();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    a
    b name
    b

    练习二:

    练习当子类中有this调用其他构造器时super的位置
    看下面代码会输出什么

    class A{
        A(){
            System.out.println("A类的无参构造");
        }
    }
    class B extends A{
        B(){
            System.out.println("B类的无参构造");
        }
        B(String name){
            System.out.println("B类的有参构造");
        }
    }
    class C extends B{
    C(){
    	this("hha");
    	System.out.println("C类的无参构造器");
    }
    C(String name){
    	super("554");
    	System.out.println("C类的有参构造器");
    }
    }
    //测试类
    C c = new C();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    执行步骤分析
    1.进入c的构造器,此时发现this调用的另一个构造器中有super指向B类的有参构造器。
    2.跟着this到C的有参构造器,通过super到B类的有参构造器在执行B类的有参构造之前再跟随默认的super到A类的无参构造器
    3.到A类的无参构造器 输出—A类的无参构造
    4.再返回到B类的有参构造器 输出 ----B类的有参构造
    5.再返回到C类的有参构造器输出—C类的有参构造器
    6.this执行完毕返回到C类的无参构造器输出—C类的无参构造器
    所以结果为:
    A类的无参构造
    B类的有参构造
    C类的有参构造
    C类的无参构造器
    总之:实例化子类时,依次从顶级父类开始依次往下执行构造器,当父类重载了构造器时,根据子类构造器中的super参数匹配执行具体的构造器,子类没有传入参数,默认执行无参构造器。

    练习三:

    编写Computer类,包含Cpu 内存,硬盘等属性,创建getDetails方法用于输出Computer的详细信息
    编写EtPc子类继承Computer类,添加特有属性-品牌brand
    编写RcPc子类继承Computer类,添加特有属性-颜色color
    编写Test04类在main方法中创建EtPC和RcP对象,分别给对象中特有的属性赋值,且给Computer类继承来的属性赋值,并使用方法打印输出信息。

    //Computer类
    public class Computer {
        String Cpu;
        String Ram;
        String Disk;
        Computer(String Cpu,String Ram,String Disk){
            this.Cpu = Cpu;
            this.Ram = Ram;
            this.Disk = Disk;
        }
        public String getDetails(){
            return "CPU="+Cpu+"内存="+Ram+"硬盘="+Disk;
        }
    }
    //EtPc子类
    public class EtPC extends Computer{
        private String brand;
        EtPC(String Cpu,String Ram,String Disk,String brand){
            super(Cpu,Ram,Disk);
            setBrand(brand);
        }
        public String printInfo(){
            return getDetails()+"品牌="+brand;
        }
        public void setBrand(String brand) {
            this.brand = brand;
        }
    }
    //RcPc子类
    public class RcPc extends Computer{
        private String color;
        RcPc(String Cpu,String Ram,String Disk,String color){
            super(Cpu,Ram,Disk);
            setColor(color);
        }
        public String printInfo(){
            return getDetails()+"颜色=" + color;
        }
        public void setColor(String color) {
            this.color = color;
        }
    }
    //测试类
    public class Test04 {
        public static void main(String[] args) {
            EtPC e1 = new EtPC("i9-12600K","32g","1t","联想");
            System.out.println(e1.printInfo());
            RcPc r1 = new RcPc("i5-12600k","16g","500g","黑色");
            System.out.println(r1.printInfo());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

  • 相关阅读:
    uniapp:配置动态接口域名,根据图片访问速度,选择最快的接口
    Ninja: Towards Transparent Tracing and Debugging on ARM【TEE的应用】
    Maven多模块版本统一管理
    【Chrome 浏览器自带谷歌翻译用不了】
    区块链(9):java区块链项目的Web服务实现之实现web服务
    继续总结Python中那些简单好用的用法
    【毕业季】这四年一路走来都很值得——老学长の忠告
    六要素微气象仪
    03 转换css元素的类别
    TCP(三次握手和四次挥手)与UDP特点、概念、区别
  • 原文地址:https://blog.csdn.net/WINorYU/article/details/127093041