封装继承多态_12

封装

封装的核心思想是将对象的内部状态(数据)和行为(方法)隐藏起来,只暴露必要的接口供外部访问。

通过访问修饰符(如 private、public、protected 和默认访问权限)控制对类的成员(字段和方法)的访问封装通过访问修饰符(如 private、public)和 getter、setter 方法实现

实际应用

  1. Java Bean

    Java Bean 是一种符合特定规范的类,通常用于封装数据。Java Bean 的特点包括

    • 私有字段。
    • 公共的 gettersetter 方法。
    • 无参构造函数。
  2. 工具类

    工具类通常包含静态方法,用于提供通用的功能。工具类的特点包括

    • 私有构造函数,防止实例化。
    • 静态方法,直接通过类名调用。

继承

继承的核心思想是通过创建一个新类(子类)来继承现有类(父类)的属性和方法,从而实现代码的重用和扩展

子类继承父类的属性和方法,并可以扩展或修改这些属性和方法

使用 extends 关键字实现继承

class ParentClass {
    // 父类的属性和方法
}

class ChildClass extends ParentClass {
    // 子类的属性和方法
}
  • Java 只支持单继承,即一个类只能直接继承一个父类。
  • 访问修饰符(public 和 protected等)在继承中起着关键作用,决定了子类对父类成员的访问权限
  • 通过接口(interface)可以实现多重继承的效果。
  • Java 支持多层继承,即一个类可以继承另一个类,而后者又可以继承其他类
  • 子类可以重写父类的方法,以提供不同的实现。
  • 重写的方法必须具有相同的方法签名(方法名、参数列表和返回类型)
  • 重写的方法抛出的异常必须更具体,修饰符不能更严格
  • 父类的静态方法会被子类继承,子类可以直接通过类名或对象调用父类的静态方法
  • 若子类定义了与父类同名的静态方法,父类的静态方法会被隐藏(Hide),而非重写(Override)。调用时根据引用类型决定执行哪个方法
  • 静态方法在编译时绑定,与对象实例无关,因此不参与多态。即使子类定义了同名静态方法,通过父类引用调用的仍是父类方法
  • 静态成员属于类本身,而非实例。继承仅是语法上的可见性,而非内存层面的复制,实际上,子类和父类共享同一份静态成员
  • 静态成员在内存中只有一份,无论是父类还是子类,访问的是同一份静态成员。例如,如果父类有一个静态变量,子类可以直接访问这个静态变量,但它们共享同一份内存

关于继承中的private属性

  • 父类中的任何变量都是会被子类继承,但是private修饰的变量子类不能直接进行访问,可以通过父类继承下来的方法进行访问

  • 父类的private属性,会被继承并且初始化在子类父对象中,只不过对外不可见

  • 因为在初始化子类时需要调用父类的构造器函数,所以理论上父类的任何属性子类都可见(在子类对象的堆内存中有一个完整的父类)。

  • 这些继承下来的私有成员虽对子类来说不可见,但子类仍然可以用父类的函数操作他们。这样的设计的意义就是我们可以用这个方法将我们的成员保护得更好,让子类的设计者也只能通过父类指定的方法修改父类的私有成员,这样将能把类保护得更好,这对一个完整的继承体系是尤为可贵的

  • 对于子类可以继承父类中的成员变量和成员方法,如果子类中出现了和父类同名的成员变量和成员方法时,父类的成员变量会被隐藏,父类的成员方法会被覆盖。需要使用父类的成员变量和方法时,就需要使用super关键字来进行引用

  • 当创建一个子类对象时,不仅会为该类的实例变量分配内存,也会为它从父类继承得到的所有实例变量分配内存,即使子类定义了与父类中同名的实例变量。 即依然会为父类中定义的、被隐藏的变量分配内存

super关键字

  1. 调用父类的构造函数

    在子类的构造函数中,可以使用 super 调用父类的构造函数。

    class Parent {
        Parent() {
            System.out.println("Parent constructor");
        }
    }
    
    class Child extends Parent {
        Child() {
            super(); // 调用父类的无参构造函数
            System.out.println("Child constructor");
        }
    }
    
    • 如果没有显示调用父类构造参数则默认调用父类的无参构造

    • 如果父类没有无参构造函数,子类必须显式调用父类的带参构造函数

    • super 必须是子类构造函数中的第一条语句

    • super 必须用于访问父类的成员或调用父类的构造函数,不能单独使用

    • 父类构造器只能调用一次,父类构造器的调用是为了初始化继承自父类的成员变量。多次调用可能导致父类状态被重复初始化,破坏对象一致性

  2. 访问父类的字段

    如果子类和父类有同名的字段,可以使用 super 关键字访问父类的字段

    class Parent {
        String name = "Parent";
    }
    
    class Child extends Parent {
        String name = "Child";
    
        void printNames() {
            System.out.println("Child name: " + name); // 访问子类的字段
            System.out.println("Parent name: " + super.name); // 访问父类的字段
        }
    }
    
  3. 访问父类的方法

    如果子类重写了父类的方法,可以使用 super 关键字调用父类的方法

    class Parent {
        void printMessage() {
            System.out.println("Message from Parent");
        }
    }
    
    class Child extends Parent {
        @Override
        void printMessage() {
            super.printMessage(); // 调用父类的方法
            System.out.println("Message from Child");
        }
    }
    
  4. 构造器函数执行流程

    • 在初始化子类中会先初始化父类,初始化父类是调用super()方法实现的,所以构造器的执行流程需要关注super()方法的位置
    • 子类构造函数中显式调用了 this(),则不会自动插入 super()
    • 子类构造函数中显示调用了 super(),则不会自动插入 super()
    class Parent{
        public Parent(){
            System.out.println("父类无参构造器")
        }
        public Parent(String name){
            System.out.println("父类有参构造器")
        }
    }
    
    class Child extends Parent{
        public Child(){
            this("111");
            System.out.println("子类无参构造器")
        }
        public Child(String name){
            System.out.println("子类有参构造器")
        }
    }
    
    //new Child()的输出为 
    //父类无参构造器
    //子类有参构造器
    //子类无参构造器
    
    super的一些知识点
    super直接从父类中查找属性或者方法,如果没找到则继续向爷爷类中查找,如果找到没有对应权限则报错

多态

多态的核心思想是允许不同的类对同一消息做出不同的响应

多态是指同一个方法调用可以根据对象的不同而具有不同的行为

多态分为编译时多态和运行时多态

  1. 编译时多态:通过方法重载实现

  2. 运行时多态:

    • 运行时多态通过方法重写和向上转型实现,具体调用哪个方法由对象的实际类型决定

    • 向上转型是指将子类对象赋值给父类引用

      class Animal {
          void makeSound() {
              System.out.println("Animal is making a sound.");
          }
      }
      
      class Dog extends Animal {
          @Override
          void makeSound() {
              System.out.println("Dog is barking.");
          }
      }
      
      class Cat extends Animal {
          @Override
          void makeSound() {
              System.out.println("Cat is meowing.");
          }
      }
      
      public class Main {
          public static void main(String[] args) {
              Animal myAnimal = new Dog(); // 向上转型
              myAnimal.makeSound();        // 调用 Dog 的 makeSound 方法
      
              myAnimal = new Cat();        // 向上转型
              myAnimal.makeSound();        // 调用 Cat 的 makeSound 方法
          }
      }
      
  3. 向上转型与向下转型

    • 向上转型

      • 向上转型是指将子类对象赋值给父类引用。
      • 向上转型是隐式的,不需要显式类型转换。
      • 向上转型后,父类引用只能访问父类中定义的成员,不能访问子类特有的成员
    • 向下转型

      • 向下转型是指将父类引用强制转换为子类引用。
      • 向下转型需要显式类型转换。
      • 向下转型只有在父类引用实际指向子类对象时才安全,否则会抛出 ClassCastException
      • 为了避免 ClassCastException,可以在向下转型前使用 instanceof 进行类型检查
  4. 方法的访问机制与属性的访问机制

    • 在多态中,方法的调用是基于对象的实际类型(运行时类型)决定的
    • 属性的访问是基于引用的类型(编译时类型)决定的。也就是说,属性的访问不会受到多态的影响
    • 属性的访问是基于引用的类型决定的,而不是对象的实际类型
    • 方法的调用是通过 动态绑定(Dynamic Binding) 实现的,即在运行时根据对象的实际类型决定调用哪个方法
    • 属性的访问是通过 静态绑定(Static Binding) 实现的,即在编译时根据引用的类型决定访问哪个属性
    • 想要访问子类的属性可以通过向下转型实现
    class Animal {
        String name = "Animal";
    }
    
    class Dog extends Animal {
        String name = "Dog";
    }
    
    public class Main { 
        public static void main(String[] args) {
            Animal myAnimal = new Dog(); 
            System.out.println(myAnimal.name); // 访问 Animal 的 name,如果Animal中没有name,则从Animal的父类查找该属性,如果找到了没有权限访问则报错
        }
    }
    
  5. 多态中的初始化赋值机制

    • 父类和子类的属性内存空间是一次性分配的,但初始化是分阶段的。父类的初始化(显式赋值和构造器代码)必须全部完成后,才会进行子类的初始化

    • 子类对象的内存布局由父类和子类属性共同组成,父类属性位于内存空间的前半部分,子类属性在后半部分

    • 初始化流程遵循严格的继承链顺序

      父类属性默认初始化 → 父类显式赋值 → 父类构造器 → 子类属性默认初始化 → 子类显式赋值 → 子类构造器
      
      1. 父类显示赋值

        class Parent {
            int x = 10; // 显式赋值 
            
            { 
                x=20 // 实例代码块显式赋值
            }  
        }
        
        • 父类构造器执行时,子类属性仍处于默认值状态,此时访问子类属性会得到默认值而非显式赋值
        • 显示赋值和实例代码块的执行顺序是按照声明顺序顺序执行
        • 在实例代码块中声明的变量是局部变量
        • 实例代码块通常用于多个构造器共享的初始化逻辑
        {
            value = 20; // 赋值语句  ✔
            System.out.println(value); // 输出语句 ❌
        }
        
        int value = 10; // 变量声明
        
        • 实例变量的声明会被提升到类的顶部(类似于变量声明被“提前”),因此 value 在整个类中都是可见的

        • 虽然实例变量的声明会被提升,但它的初始化(即 int value = 10;)不会被提升

        • 在代码块中,System.out.println(value); 尝试访问 value 时,value 尚未被初始化

        • 在 Java 中,实例变量在声明时会被赋予默认值(int 的默认值是 0),但在代码块中直接访问未初始化的变量会导致编译错误

        • 错误信息为:非法前向引用(Illegal Forward Reference)

        • Java 禁止在变量初始化之前对其进行访问,这种访问被称为“非法前向引用”

        • Java 编译器通过禁止非法前向引用,避免循环初始化或逻辑混乱。例如,允许左值(赋值操作)引用后文声明的变量,但禁止右值(读取操作)引用

          static {
              a = 10;    // 合法(左值操作)
              int b = a; // 非法(右值操作)
          }
          static int a;
          
    1. 潜在问题

      如果父类构造器中调用了被子类重写的方法,可能导致子类属性尚未初始化就被使用。

      class Parent {
          Parent() {
              print(); // 调用子类重写的 print()
          }  
          void print() {
              System.out.println("Parent");  
          }
      }
      class Child extends Parent {
          int value = 10; //如果该变量为final修饰则输出10 即final int value=10
          Child(){
              value=10; //如果在构造器中赋值则仍然会出现该问题
          }
          @Override 
          void print() {
              System.out.println(value);  // 输出 0(未初始化)
       }  
      }
      
    
    
  6. 动态绑定机制

    • 动态绑定机制允许程序在运行时根据对象的实际类型(而不是引用类型)来决定调用哪个方法

      • 动态绑定只适用于实例方法(非静态方法),不适用于静态方法、字段或构造器,即在寻找静态方法或者字段时会在运行当前方法的类中寻找相关静态方法或者字段。如果没有则会在父类中进行寻找
      public class Hello {
          public static void main(String[] args) {
              Parent parent=new Child();
              parent.add();
          }
      }
      
      class Parent {
          public int c=20;
      
          public static void say(){
              System.out.println("这是父类的静态say方法");
          }
          public void add(){
              System.out.println(c); //执行这里的语句c=20
              say(); //执行本类的say方法
          }
      }
      
      class Child extends Parent {
      //    public int c=10;
      
      //    public static void say(){
      //        System.out.println("这是子类的静态say方法");
      //    }
      
          @Override
          public void add(){
              System.out.println(c); //执行这里的语句c=10
              say(); //执行本类的say方法
          }
      
      }
      
    • 动态绑定工作原理

      1. 编译时在编译时,编译器会检查引用类型(Parent)是否有 add() 方法。

        如果存在,编译器会通过编译,但不会确定具体调用哪个类的方法

      2. 在运行时,JVM 会根据对象的实际类型(Child)来决定调用哪个方法。

        JVM 会查找对象的实际类型的方法表(Method Table),找到正确的方法并调用

    • 方法表

      1. 每个类在 JVM 中都有一个方法表,存储了该类所有实例方法的入口地址

      2. 子类的方法表会包含父类的方法,但如果子类重写了父类的方法,子类方法表中的方法入口地址会指向子类的方法

      3. 静态方法是与类关联的,而不是与对象关联的。静态方法的调用在编译时就确定了,不需要在运行时查找方法表

      4. 静态方法的地址存储在类的元数据(Class Metadata)中,而不是方法表中。

        类的元数据包括类的静态字段、静态方法、常量池等信息。

        静态方法的调用直接通过类名解析,不需要依赖对象实例

posted @ 2025-03-03 21:54  QAQ001  阅读(24)  评论(0)    收藏  举报