6.Java-类的继承

继承与多态

1.继承

  • 用来表示对象间的分类关系。父类是对子类的分类,子类是父类作为基类派生类,子类和父类是is-a的关系,例如Dog is an Animal。某一个子类也可以作为其他子类的父类。
  • 每个类有且只有一个父类(单继承),没有声明父类的,其父类为Object,子类继承了父类非private的属性和方法,可以增加自己的属性和方法,以及重写@Override父类的方法实现。
  • new过程中,父类先进行初始化,可通过super调用父类相应的构造方法,没有使用super的情况下,调用父类的默认构造方法。
  • 子类变量和方法与父类重名的情况下,可通过super强制访问父类的变量和方法。
  • 子类继承父类,自动拥有父类的属性和行为,并可扩展属性和行为,同时,可重写父类的方法以修改行为

2.多态

  • Shape图形符类,有Circle圆形、Line线形两个子类,ArrowLine带箭头的线形是Line线性的子类。定义变量Shape shape,可以引用Shap类所有的子类对象,子类对象可以赋值给父类引用变量,这叫多态。
  • Shape类为变量shape静态类型,Circle、Line和ArrowLine子类为变量shape的动态类型。子类重写了父类的成员方法draw(),shap变量引用子类对象时,实际执行时会调用子类重写的成员方法,这叫做动态绑定

继承的注意事项

1.构造函数

  • 父类没有默认构造方法,只有带参构造方法时,其子类的构造方法中,需要通过super调用父类的带参数构造方法,否则,Java会提示编译错误
        public class Base {
            private String member;
            public Base(String member){ // 父类base只有一个带参数的构造方法
                this.member = member;
            }
        }
       public class Child extends Base {
            public Child(String member) { 
                super(member);  // Base类的子类需要通过super调用Base类的构造方法
            }
        }
  • 在父类构造方法中调用可被子类重写的方法,会引起混淆,构造方法中只调用private方法
        public class Base {
            public Base(){
                test();   // 构造方法中有非private的成员方法test()
            }
            public void test(){ // test方法是可以被子类重写
            }
        }
       public class Child extends Base {
            private int a = 123;
            public Child(){
            }
            public void test(){   // 子类重写了父类的test方法,在调用的时候
                System.out.println(a);
        }
        // 调用子类的方法预期输出123,却输出了0、123
        public static void main(String[] args){
            Child c = new Child(); // 原因是new的过程会先初始化父类,父类构造方法调用test方法,发现test被重写,动态绑定机制会调用子类test方法,此时子类变量a的赋值还未执行,输出了默认的0值。
            c.test();
        }
    }

2.静态绑定

  • 动态绑定是子类可以重写父类非private的方法,父类对象引用子类对象后调用重写的方法时,会动态绑定,执行子类的方法。
  • 静态绑定是父类的实例变量、静态变量、静态方法、private方法,当子类有重名的变量或方法时,实际上有两个变量或方法,子类隐藏了父类的变量和方法。
  • 静态类型是父类,则访问父类的变量和方法;静态类型是子类,则访问的是子类的变量和方法。Base b = new Child(),其中Base为变量b的静态类型,Child为变量的动态类型。Child b = new Child(),Child则为变量b的静态类型。
  • 静态绑定在程序编译阶段即可决定,而动态绑定则要等到程序运行时。
        public class Base {
            public static String s = "static_base"; // 静态变量
            public String m = "base";  // 成员变量
            public static void staticTest(){
                System.out.println("base static: "+s); // 静态方法,只能访问静态变量
            }
        }
        // 子类重名的静态变量、成员变量,静态方法
        public class Child extends Base {
            public static String s = "child_base";
            public String m = "child";
            public static void staticTest(){
                System.out.println("child static: "+s);
            }
        }
        /**  以下代码输出
          static_base
          base
          base static: static_base
          child_base
          child
          child static: child_base
        **/
        public static void main(String[] args) {
            Child c = new Child(); // 静态类型是子类
            Base b = c;  // 静态类型是父类
            System.out.println(b.s);  // 访问父类静态变量
            System.out.println(b.m); // 访问父类成员变量
            b.staticTest(); // 访问父类静态方法
            System.out.println(c.s); // 访问子类静态变量
            System.out.println(c.m); // 访问子类成员变量
            c.staticTest(); // 访问子类静态方法
        }

3.其他

(1) 函数匹配:当有多个重名函数的时候,在决定要调用哪个函数的过程中,首先是按照参数类型进行匹配的,换句话说,寻找在所有重载版本中最匹配的,然后才看变量的动态类型,进行动态绑定。如sum(2,3),当子类方法是sum(long, long),父类方法是sum(int, int),根据类型匹配会访问父类的方法。
(2) 向下转型: 父类赋值给子类叫做向下转型。能否转换成功,取决于这个父类变量的动态类型(即引用的对象类型)是不是这个子类或这个子类的子类。如成功向下转型:Base b = new Child(); Child c = (Child)b; 向下转型抛异常 Base b = new Base(); Child c = (Child)b,通过instanceof判断父类变量是否引用子类对象
(3) protected:一个常用的场景是模板模式,父类定义了实现的模板,为方法执行的步骤,而用protected修饰的方法具体实现由子类提供。

        public class Base {
            protected   int currentStep;
            protected void step1(){  
            }
            protected void step2(){
        }
        public void action(){ // 模板方法
            this.currentStep = 1;
            step1(); // 模板-步骤1
            this.currentStep = 2;
            step2(); // 模板-步骤2
        }
        public class Child extends Base {
            protected void step1(){ // 模板-步骤1的具体实现
                System.out.println("child step " + this.currentStep);
            }
            protected void step2(){ // 模板-步骤2的具体实现
                System.out.println("child step " + this.currentStep);
            }
        }

(4) 可见性重写:子类重写方法时,可以升级父类方法的可见性但不能降低。可见范围是public > default> protected > private,从做左到右为降低。即父类是public,子类只能是public,父类是protected,子类可以是protected,也可以是public。
(5) 防止继承:使用final关键字。

  • final修饰变量,变量只能赋值一次,且不可修改
  • fianl修饰protected方法和public方法,方法不能被重写
  • fianl修饰Java类,该类不能被继承了

3.继承基本原理

一个类完整的信息包括

  • 类变量 (静态变量)
  • 类初始化代码 (静态代码块)
  • 类方法 (静态方法)
  • 实例变量
  • 实例初始化代码
  • 实例方法
  • 父类信息引用

类初始化的过程

  • 静态变量的赋值语句
  • 静态代码块

实例初始化的过程

  • 实例变量时的赋值语句
  • 实例初始化代码块
  • 实例初始化代码块

类加载的过程

  • 分配内存保存类的信息
  • 给类变量赋默认值
  • 加载父类
  • 设置父子关系
  • 执行类初始化代码 (先执行父类再执行子类,父类执行时子类变量为默认值)

以上初始化过程通过下面打印的结果来理解

        /**打印结果:
         ---- new Child()
        基类静态代码块, s: 1
        子类静态代码块, s: 10
        基类实例代码块, a: 1
        基类构造方法, a: 2
        子类实例代码块, a: 10
        子类构造方法, a: 20
        ---- c.action()
        start
        child s: 20, a: 30
        end
        ---- b.action()
        start
        child s: 20, a: 30
        end
        ---- b.s: 2
        ---- c.s: 20
        ---- b.a: 3
        ---- c.a: 30
        **/
        public class Base {
            public static int s = 1; // step-1
            public int a = 1; // step-1
            static {
                System.out.println("基类静态代码块, s: "+s); // step-2 由于先执行赋值语句,所以打印是结果为1
                s = 2;
            }
            {
                System.out.println("基类实例代码块, a: "+a); // step-3
                a = 2;
            }
            public Base(){
                System.out.println("基类构造方法, a: "+a); // step-4 先执行实例的代码块,在执行构造方法
                a = 3;
            }
            protected void step(){
                System.out.println("base s: " + s +", a: "+a);
            }
            public void action(){
                System.out.println("start");
                step();
                System.out.println("end");
            }
        }
        public class Child extends Base {
            public static int s = 10;
            public int a = 10;
            static {
                System.out.println("子类静态代码块, s: "+s);
                s = 20;
            }
            {
                System.out.println("子类实例代码块, a: "+a);
                  a = 20;
              }
              public Child(){
                  System.out.println("子类构造方法, a: "+a);
                  a = 30;
              }
              protected void step(){
                  System.out.println("child s: " + s +", a: "+a);
              }
          }
        public static void main(String[] args) {
            System.out.println("---- new Child()");
            Child c = new Child(); // 对象初始化前先初始化类信息(静态方法),子类初始化前先初始化父类
            System.out.println("\n---- c.action()");
            c.action();
            Base b = c;
            System.out.println("\n---- b.action()");
            b.action(); // 动态绑定
            System.out.println("\n---- b.s: " + b.s);
            System.out.println("\n---- c.s: " + c.s);
            System.out.println("\n---- b.a: " + b.a);
            System.out.println("\n---- c.a: " + c.a);
        }

内存分为栈和堆,栈存放函数的局部变量,而堆存放动态分配的对象,还有一个内存区,存放类的信息,这个区在Java中称为方法区

书籍:18年出版/机械工业出版社/马俊昌/《Java编程的逻辑》第二部分第三章

posted @   顾的江小鱼  阅读(197)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示