Java 深入:static、类加载顺序与多态

static、类加载顺序与多态是 Java 面向对象与运行时行为的核心知识。掌握它们有助于理解程序启动时发生了什么、为什么某些初始化行为会出现意外输出、以及如何写出安全且易维护的类设计。


1. 基础回顾:数据类型与内存概念

  • 基本数据类型byte, short, int, long, float, double, boolean, char —— 存储在栈或对象内部(按上下文),值语义。

  • 引用数据类型:类、接口、数组 —— 引用指向堆上的对象实例。

  • JVM 相关区(理解 static 很重要):

    • 堆(Heap):对象实例存放处(运行时分配)。

    • 栈(Stack):方法调用帧、局部变量引用、基本类型的局部拷贝(按情况)。

    • 方法区 / 元空间(Method Area / Metaspace):类的元信息、静态变量、常量池等(static 成员通常归类到此处或相应区域)。

    • 运行时常量池:字符串常量等可驻留于此。


2. static 的本质与作用

  • 归属层级static 修饰的字段/方法/代码块属于(class-level),而不是某个实例(instance-level)。

  • 共享特性:同一个类的所有实例共享同一份静态变量(所有实例引用同一内存位置)。

  • 访问方式:推荐使用 类名.静态成员 的形式访问静态内容,而不是通过对象实例调用(可读性与设计意图清晰)。

  • 静态方法的限制

    • 不能使用 this,因为无“当前实例”语义。

    • 不能直接访问非静态成员(因为在类被加载但未实例化时,这些实例成员可能不存在)。

  • 静态代码块(static{})

    • 在类初始化阶段执行(类被首次主动使用时),且先于任何实例构造。

    • 可用于复杂静态资源初始化(比如初始化静态集合、读取配置等)。


3. staticfinal 的差别与联用要点

  • static:表示“属于类”。

  • final:表示不可变

修饰常量

规则:被 final 修饰的常量必须赋值,且只能赋值一次,后续无法修改。示例:final int MAX_AGE = 120;(一旦赋值,MAX_AGE 的值就固定不变)。

修饰方法

规则:被 final 修饰的方法不能被子类重写。

修饰类

规则:被 final 修饰的类不能被继承

典型案例:String 类就是 final 修饰的类(如代码示例中 public final class String),这意味着任何类都无法继承 String 来扩展其功能。

作用:用于锁定方法逻辑,确保方法行为在继承体系中保持稳定。  

  • static final(常量)

    若是编译时常量(例如 static final int MAX = 10;static final String S = "X";),编译器可能把它内联到使用处(调用方的字节码含值),这会导致常量值改变后依赖方未重新编译仍旧使用旧值的情况。

不可变对象也推荐使用 static final(如 static final List<String> LIST = Collections.unmodifiableList(...)),但注意不可让该引用指向可变内部状态被外部修改。


4. 类加载与初始化顺序(必须掌握的执行流程)

  1. 类加载(Loading):JVM 找到类二进制数据并放入方法区(或元空间)。

  2. 连接(Linking)

    • 验证准备(Preparation)(为静态变量分配默认值)、解析(Resolution)(符号引用变为直接引用)。

  3. 初始化(Initialization)

    • 执行静态变量的显式赋值与 static 代码块,按源代码中出现的顺序执行。

    • 这是执行类显式静态初始化的阶段(发生在类的首次主动使用)。

  4. 实例创建时

    • 分配对象内存(堆),设置默认值;

    • 执行实例字段的显式赋值与实例初始化块(按顺序);

    • 执行构造函数体。

继承关系中的顺序

  • 加载/初始化父类 → 父类的静态初始化完成 → 子类的静态初始化 → 创建对象时先父类的实例初始化与构造,再子类的实例初始化与构造。
    换言之:static(类级)先于实例构造,且父类 static 先于子类 static


5. 典型示例(演示输出顺序)

class Animal {
    static { 
        System.out.println("动物会跑"); 
    }
    void eat() {
        System.out.println("动物在吃饭");
    }
}

class Dog extends Animal {
    static {
         System.out.println("狗会跑"); 
    }
    void bark() {
        System.out.println("狗在叫");
    } 
}

public class Demo {
    public static void main(String[] args) {
        System.out.println("程序开始执行");

        // 创建Dog对象(触发类加载 + 构造过程)
        Dog d = new Dog();

        // 调用方法
        d.eat();
        d.bark();

        System.out.println("程序结束执行");
    }
}

输出顺序

动物会跑
狗会跑
程序开始执行
动物在吃饭
狗在叫
程序结束执行

说明

先加载类(static先执行)

  • 程序运行时,JVM 会先加载 Animal 类 → 输出 “动物会跑”。

  • 接着加载 Dog 类 → 输出 “狗会跑”。
    (静态代码块在类加载时执行,只执行一次)

执行 main 方法

  • 输出 “程序开始执行”。

创建对象并调用方法

  • 创建 Dog 对象(此时构造方法无输出)。

  • 调用 eat() → 输出 “动物在吃饭”。

  • 调用 bark() → 输出 “狗在叫”。

程序结束

  • 输出 “程序结束执行”


6. 多态

  • 定义:父类引用可以指向子类对象(Parent p = new Child();),方法调用根据对象的实际类型(运行时)进行动态绑定(即调用子类重写的方法)。

  • 限制:通过父类引用只能调用父类声明的方法;若要访问子类特有方法需向下转型。

  • 内存视角:堆上的对象含有子类与父类的字段(对象只有一份),方法区保存类方法信息,运行时根据对象实际类型查找方法表进行调用。


7. 常见坑与最佳实践

  • 避免可变的 static 状态:全局可变静态变量会带来隐藏依赖、并发问题与测试困难。

  • 初始化顺序陷阱:静态变量依赖其他静态变量(尤其跨类)可能因加载顺序产生 null 或默认值问题。尽量避免复杂的静态互相依赖。

  • static final 字符串/基本类型常量的内联:变更常量值后,记得重新编译所有使用该常量的模块。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值