类加载时属性的初始化顺序问题

本文探讨了Java中类加载时属性初始化的顺序,通过一个笔试题示例展示了由于初始化顺序导致的空指针异常问题。分析了两种解决方案,并介绍了实例代码块的执行时机,强调了正确初始化顺序的重要性。

类加载时属性的初始化顺序问题
今天我们来看一下类加载时属性的初始化顺序问题,首先我们来看一道笔试题。

分析如下代码是否有问题?

``java`
import java.util.HashMap;
import java.util.Map;

class ClassE{
static ClassE instance = new ClassE();
static Map<String,Object> map = new HashMap<>(16);
public ClassE() {
map.put(“A”, 100);
map.put(“B”, 200);
}
}
public class TestClassObject08 {
public static void main(String[] args) throws Exception {
Class.forName(“com.java.oop.cls.ClassE”);
}

}

我们来分析一下:
按照本题代码的执行循序:
1,static修饰的属性,会按照从上到下的顺序依次执行初始化
2,本题instance在前,map在后。第一次默认初始化都是null
那么第二次初始化,先初始化对象,执行动作new,要执行构造方法(里面有向map添加数据),但是map还没有初始化,所以就出空指针异常

属性初始化两次:
第一次初始化是默认值null,在(link)连接,校验)阶段
第二次初始化是(init)将等号右边的值给变量

解决方法:
方法一:将map放在instance前。


static Map<String,Object> map = new HashMap<>(16);

static ClassE instance = new ClassE();

方法二:将map前的static去掉

static ClassE instance = new ClassE();
Map<String,Object> map = new HashMap<>(16);//无static

分析: instance初始化的时候在new对象。
new对象的时候会先将实例变量先初始化
思路:构建对象的时候—>类没有加载时先加载类—>静态变量没有初始化,初始化静态变量-- -->实例变量(没有static修饰的变量)没有初始化,初始化实例变量—>执行构造方法

这种方法从语法上可以,但是从业务上,每构建依次对象就要初始化一次,那么所有变量共享一个map,就要用第一种方法。

拓展:可能还会见到实例代码块。
建对象时,每次构建对象都会执行,在构建对象(构造方法)之前执行实例代码块
代码:


import java.util.HashMap;
import java.util.Map;

class ClassE{
static Map<String,Object> map = new HashMap<>(16);
static ClassE instance = new ClassE();
{
//实例代码块(构建对象时,每次构建对象都会执行,在构建对象(构造方法)之前执行)

System.out.println("{}");
}

public ClassE() {
//输出测试

System.out.println(“ClassE()”);
map.put(“A”, 100);
map.put(“B”, 200);
}
}
public class TestClassObject08 {
public static void main(String[] args) throws Exception {
Class.forName(“com.java.oop.cls.ClassE”);
}

}
//输出:

{}
ClassE()

<think>我们正在讨论Java中static变量在类加载初始化问题。根据用户的当前查询以及之前的引用内容,我们需要详细解释static变量在类加载过程中的初始化原理和特点。 引用关键点: 引用[3]:static修饰的字段在加载过程中准备阶段被初始化,但这个阶段只赋默认值(0或null),在初始化阶段(类构造器<clinit>)才会赋变量定义的值。而static final修饰的字段(基本类型或String)在编译生成constantValue属性,在准备阶段直接赋值。 引用[4]:初始化阶段JVM会调用<clinit>方法(类构造器),它是由编译器自动收集类中所有静态变量的赋值动作和静态代码块合并产生的。 因此,回答应包含: 1. 类加载过程(特别是准备阶段和初始化阶段)中static变量的处理。 2. static变量初始化的两个阶段(准备阶段的默认值,初始化阶段的显式赋值)。 3. static final常量的特殊处理(编译常量,准备阶段直接赋值)。 4. 初始化顺序(根据源代码顺序)。 同,结合引用[2]中的示例,说明初始化顺序的重要性。 注意:回答中引用标识要自然添加。 结构建议: 1. 类加载过程中的初始化阶段 2. static变量的初始化步骤(两阶段) 3. static final常量的特殊处理 4. 初始化顺序问题(结合代码示例) 5. 总结特点 最后,生成相关问题。 </think>### Java中static变量在类加载初始化原理与特点 #### 1. **类加载过程中的初始化阶段** Java虚拟机(JVM)加载类,static变量的初始化发生在**类初始化阶段**(对应`<clinit>`方法的执行): - **准备阶段**:为static变量分配内存并设置**默认初始值**(如int为0,对象为null)。此阶段不执行显式赋值[^3]。 - **初始化阶段**:执行`<clinit>`方法(编译器自动生成),按源代码顺序执行static变量的显式赋值和静态代码块[^1][^4]。 例如: ```java public class Example { static int a = 10; // 声明并赋值 static { b = 20; } // 静态代码块赋值 static int b; // 声明 } ``` 初始化顺序: 1. `a`准备阶段默认值:0 → 初始化阶段赋值为10 2. `b`准备阶段默认值:0 → 静态代码块赋值为20 最终值:`a=10`, `b=20`[^2] #### 2. **static变量的初始化特点** | **特性** | **说明** | |-------------------|--------------------------------------------------------------------------| | **单次执行** | `<clinit>`方法仅在类**首次加载执行一次**,后续不再执行[^4] | | **顺序依赖性** | 初始化按源码顺序执行。若交换`static int b`和静态代码块位置,将导致编译错误[^1][^2] | | **线程安全** | JVM保证`<clinit>`方法线程安全,无需额外同步[^4] | | **默认值覆盖** | 显式赋值覆盖准备阶段的默认值(例如int从0变为10)[^3] | #### 3. **static final常量的特殊处理** 若变量被`static final`修饰: - 基本类型或String字面量:编译直接存入**常量池**,准备阶段直接赋值(**跳过默认值阶段**)[^3] - 非字面量(如对象):仍按普通static变量流程初始化 示例: ```java static final int MAX = 100; // 准备阶段直接赋100(无默认值0) static final Date NOW = new Date(); // 初始化阶段赋值 ``` #### 4. **关键原理:`<clinit>`方法** - **生成规则**:编译器合并所有**static赋值语句和静态代码块**生成`<clinit>`方法 - **触发机**: - 首次创建实例 - 首次访问static成员 - Class.forName()加载类[^4] - **与`<init>`区别**:`<init>`是实例构造器,在`new`对象执行;`<clinit>`是类构造器,在类加载执行[^4] #### 总结 static变量初始化依赖JVM类加载机制: 1. **准备阶段**:分配内存 → 设默认值 2. **初始化阶段**:执行`<clinit>` → 按源码顺序显式赋值 3. **static final常量**:编译常量直接嵌入字节码,避免运行开销[^3] > ⚠️ 注意:初始化顺序错误会导致逻辑问题(如引用未初始化的静态变量)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值