所谓高手程序员犯下的错误

看下面一段代码,这是一个网上程序员写下的一段代码,摘录而来,你能根据

[url]http://annan211.iteye.com/blog/2118115[/url]

来分析其出错的原因吗?


package com.ccb.framework.enums;

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

public class CachingEnumResolver {
//单态实例 一切问题皆由此行引起
private static final CachingEnumResolver SINGLE_ENUM_RESOLVER = new
CachingEnumResolver(); //[1]
/*MSGCODE->Category内存索引*/
private static Map CODE_MAP_CACHE;//[2]
static {
CODE_MAP_CACHE = new HashMap();
//为了说明问题,我在这里初始化一条数据
CODE_MAP_CACHE.put("0","北京市");
}

//private, for single instance
private CachingEnumResolver() {
//初始化加载数据 引起问题,该方法也要负点责任
initEnums();
}

/**
* 初始化所有的枚举类型
*/
public static void initEnums() {
// ~~~~~~~~~问题从这里开始暴露 ~~~~~~~~~~~//
if (null == CODE_MAP_CACHE) {
System.out.println("CODE_MAP_CACHE为空,问题在这里开始暴露.");
CODE_MAP_CACHE = new HashMap();
}
CODE_MAP_CACHE.put("1", "北京市");
CODE_MAP_CACHE.put("2", "云南省");

//..... other code...
}

public Map getCache() {
return Collections.unmodifiableMap(CODE_MAP_CACHE);
}

/**
* 获取单态实例
*
* @return
*/
public static CachingEnumResolver getInstance() {
return SINGLE_ENUM_RESOLVER;
}

public static void main(String[] args) {
System.out.println(CachingEnumResolver.getInstance().getCache());
}
}


写下这段代码的作者的本意是希望 输出一个包含 3个键值对的map,但是结果却只输出了
CODE_MAP_CACHE为空,问题在这里开始暴露.
{0=北京市}

要想发现问题的根源 和 解决问题,我们还需要从基础做起,从类的加载说起。

Java 文件被jvm编译器 变异成字节码 也就是class 文件,class 文件中包含了这个类所有的基本信息。字节码被加载进入内存,或者主动或被动的被初始化,这里的主动和被动 如下解释:

主动引用:

创建某个类的新实例--新实例可通过new 关键字,反射,克隆,反序列化创建
调用某个类的静态方法
调用某个类或接口的静态字段(final修饰的除外)

使用Java的某些反射方法
初始化某个类的子类
虚拟机启动时包含有main的启动类


以上6种情形会造成类的初始化。

这里有必要把类的加载和初始化区分开,加载是指把class字节码载入内存,一个类的初始化过程包含以下几个步骤
1 加载
加载 :
这里的加载是指将class文件载入内存,由类加载器执行,并且创建一个Class 对象放入堆中。

2 连接
连接:
这里的连接 是指 将已经加载的Java 二进制代码 组合到jvm运行时内存当中。
连接又包含以下几步骤:
验证阶段:
验证即是检验有无恶意代码,检查final 类有没有被继承等不符合Java规范的代码快存在。
准备阶段:
准备阶段主要是创建静态域,分配空间。将基本类型的变量0,boolean类型的变量设置为false,引用类型设置为 null.
解析阶段:
解析的过程就是对类中的接口、类、方法、变量的符号引用进行解析并定位,解析成直接引用(符号引用就是编码是用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址),并保证这些类被正确的找到。解析的过程可能导致其它的类被加载。需要注意的是,根据不同的解析策略,这一步不一定是必须的,有些解析策略在解析时递归的把所有引用解析,这是early resolution,要求所有引用都必须存在;还有一种策略是late resolution,这也是Oracle 的JDK所采取的策略,即在类只是被引用了,还没有被真正用到时,并不进行解析,只有当真正用到了,才去加载和解析这个类。


3 初始化
初始化
首先执行静态代码块static{},接着是静态变量,接着是静态方法。
当执行到 private static final CachingEnumResolver SINGLE_ENUM_RESOLVER = new CachingEnumResolver(); 时,会出现jvm对Java对象的初始化,会按照 类内部静态块 > 类静态属性 > 类内部属性 > 类构造函数 将类的属性进行初始化。


根据以上的理基础 我们来分析原作者的这段代码

第一步 字节码被加载
第二部 被加载的字节码需要经过连接阶段,在连接阶段 先后经过 检查 准备 解析三个小阶段。 在准备阶段 将 静态变量 SINGLE_ENUM_RESOLVER CODE_MAP_CACHE 设置为null.
接着解析,分析是否还有其他需要被载入初始化的类。
第三步 初始化阶段 按照 类内部静态块 > 类静态属性 > 类内部属性 > 类构造函数优先级顺序一次初始化


类初始化顺序



对于普通的Java程序,一般都不需要显式的声明来动态加载Java类,只需要用import关键字将相关联的类引入,类被第一次调用的时候,就会被加载初始化。那对于一个类对象,其内部各组成部分的初始化顺序又是如何的呢?
一个Java类对象在初始化的时候必定是按照一定顺序初始化其静态块、静态属性、类内部属性、构造方法。这里我们讨论的初始化分别针对两个对象,一个是类本身还有一个是类实例化的对象。
类本身的初始化会在类被加载完毕、链接完成之后,由Java虚拟机负责调用<clinit>方法完成。在这个方法中依次完成了堆类内部静态块的调用和类内部静态属性的初始化(如果存在父类,父类会优先进行初始化)。不论创建多少个实例化的对象,一个类只会被初始化一次。
类实例化的对象通过new操作创建,Java虚拟机保证一个类在new操作实例化其对象之前已经完成了类的加载、链接和初始化。之后Java虚拟机会调用<init>方法完成类实例化对象的初始化。这个方法会优先按照代码中顺序完成对类内部个属性的初始化,之后再调用类的构造函数(如果有父类,则优先调用父类的构造函数)。
  PS:需要注意的是上述提到的<init>和<clinit>方法都是非法的Java方法名,是由编译器命名的,并不能由编码实现。
  综上所述,我们大致可以得出以下结论,对于一个类,在实例化一个这个类的对象时,我们可以保证以下这样的优先级进行初始化:类内部静态块 > 类静态属性 > 类内部属性 > 类构造函数[
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

annan211

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值