在运行 Java 代码时,很多时候需要弄清楚程序执行的流程,而面向对象的 Java 程序并非像主要为面向过程而设计的 C 语言一样去顺序执行(简单按照代码的顺序),这使得对于类文件的加载以及执行流程的理解非常重要。本文简单介绍了 Java 类的初始化部分具体过程,包括成员变量、静态代码块、构造函数等的初始化时机及执行流程。
初始化时机
根据 javase 8 的文档说明
T 的一个实例被创建
T 的一个静态方法被调用;
T 声明的一个静态变量被赋值;
T 声明的一个静态变量被使用并且这个变量不是常量
暂不考虑这种情况(涉及到顶层类,断言,内部类等)
另外要注意:当一个类初始化时,它的父类如果没有初始化则会先被初始化。
初始化步骤
首先要弄清楚几个基本概念:静态代码块、构造代码块、构造方法、成员变量、子父类的初始化。
概念分析
静态代码块, 一般用于类的数据初始化,形式为:
class StaticDemo{
static {
int a=1;
System.out.println("I am a static block!");
}
}
构造代码块,与静态代码块的区别在于少了 static 关键字:
class ConstructorBlockDemo{
{
int a=1;
System.out.println("I am a constructor block!");
}
}
构造方法
public class ConstructorDemo{
public ConstructorDemo(){
int a=1;
System.out.println("I am a constructor!");
}
}
成员变量,一般就是类所定义的变量(描述类的属性),这个容易理解。分为默认初始化和显示初始化:
class FieldDemo{
int b;//initialized implicitly
int a=1;//initialized explicitly
}
需要注意的是,a=1;在有些情况下(比如 FieldDemo 还有父类)并不一定立即在变量 a 分配内存后被赋值。
子父类的初始化,一般先进行父类初始化,然后进行子类初始化,分层进行。
执行顺序
1. 首先程序运行时,从 main 方法所在的主类开始,但并 不意味着就是从 main 方法开始。而是 JVM 开始加载类:
public class LoadClassDemo{
static {
int a=1;
System.out.println("I am a static block!");
}
public static void main(String[] args) {
System.out.println("I am the main method!");
}
}
上述程序输出结果为(可以先自己考虑下,然后点击图片放大核对下):
JAVA: 理解Java中的类初始化
JVM 在加载 LoadClassDemo 时,静态代码块就开始执行了,从而对类的信息初始化。并且静态代码块只在类加载时执行,因此只执行一次。
2. 构造代码块。静态代码块执行完毕后,构造代码块就开始执行了,而且每次生成类的实例都会执行,区别于静态代码块只执行一次。比如:
class ObjectDemo {
static {
System.out.println("ObjectDemo static block!");
}
{
System.out.println("ObjectDemo constructor block");
}
public ObjectDemo() {
System.out.println("ObjectDemo constructor");
}
}
public class ConstructorBlockTest {
static {
System.out.println("I am a static block!");
}
public static void main(String[] args) {
System.out.println("I am the main method!");
ObjectDemo od1 = new ObjectDemo();
ObjectDemo od2 = new ObjectDemo();
}
}
上述程序输出结果为(可以先自己考虑下,然后点击图片放大核对下):
JAVA: 理解Java中的类初始化
3. 成员变量,在创建实例时被分配内存,之后进行默认初始化和显示初始化(如有)。
4. 构造方法,上述过程之后构造方法才被调用生成对象。
class ObjectDemo {
static {
System.out.println("ObjectDemo static block!");
}
{
System.out.println("ObjectDemo constructor block");
}
String f="field value";
public ObjectDemo() {
System.out.println("ObjectDemo constructor");
}
}
class ObjectDemo2 {
static {
System.out.println("ObjectDemo static block!");
}
{
System.out.println("ObjectDemo constructor block");
}
String f2="field value";
public ObjectDemo2() {
System.out.println("ObjectDemo constructor");
f2="field value changed!";
}
}
public class InitializationDemo {
static {
System.out.println("I am a static block!");
}
public static void main(String[] args) {
System.out.println("I am the main method!");
ObjectDemo od1 = new ObjectDemo();
ObjectDemo od2 = new ObjectDemo();
System.out.println(od1.f);
ObjectDemo2 od3 = new ObjectDemo2();
System.out.println(od3.f2);
}
}
仔细分析上述综合示例结果,(然后点击图片放大核对):
JAVA: 理解Java中的类初始化
实例理解
实例 1
请分析一下代码执行结果,然后思考注释掉的 super 与结果又何关系
class X {
static String xVar="x value";
Y b = new Y();
static{
System.out.println(xVar);
System.out.println("X static block");
xVar="static value";
}
X() {
System.out.println("X");
System.out.println(xVar);
xVar="x value changed!";
}
}
class Y {
String yVar="Y value";
Y() {
System.out.println("Y");
System.out.println(yVar);
yVar="y value changed!";
}
void show(){
System.out.println(yVar);
}
}
public class Z extends X {
Y y ;
static{
System.out.println("Z static block");
}
{
y = new Y();
y.show();
}
Z() {
//super
System.out.println("Z");
}
public static void main(String[] args) {
System.out.println(new Z().xVar);
}
}
分析 1
先给出结果(点击图片放大):
JAVA: 理解Java中的类初始化
分析过程如下(注意子父类的分层初始化):
1. 首先找到 main 方法所在类为 Z,所以最先开始加载 Z 类,由于 Z 类还有父类 X,因此又加载 X 类,先对 X 类初始化;
2.X 类加载并为静态变量 xVar 初始化,接着执行静态代码块,输出 x value,接着输出X static block 并改变 xVar 值;
3.X 类加载完之后,Z 类开始执行静态代码块输出 Z static block;
4. 接着初始化 X 类,初始化成员变量 y,因此输出Y 和y value,由于没有构造代码块,所以继续执行构造方法输出 X和static value;
5. 到此 X 类初始化完成,再接着执行 Z 类的构造代码块输出 Y 和Y value并改变 yVar 指,执行 show()方法后输出y value changed!,最后执行 Z 的构造方法,输出Z;
6. 到此 Z 类初始化完成,最终输出x value changed!
实例 2
还有一个比较典型的例子
class Base
{
Base()
{
preProcess();
}
void preProcess()
{
}
}
class Derived extends Base
{
public String whenAmISet = "set when declared";
public Derived()
{
whenAmISet = "set in constructor";
}
void preProcess()
{
whenAmISet = "set in preProcess()";
}
}
public class TestInitialization
{
public static void main(String[] args)
{
Derived d = new Derived();
System.out.println(d.whenAmISet);
}
}
分析 2
详见脚注 3,要注意本文中的例子在 Derived 类中添加了构造方法,所以最终结果为 set in constructor
更新:eclipse 调试演示图
后记
这次关于 Java 中类的初始化就介绍到这里了,关于 Java 类的生命周期可以参看
相关
Related Posts
Java学习笔记:内部类/匿名内部类的全面介绍
编写java程序时,一般一个类(或者接口)都是放在一个独立的java文件中,并且类名同文件名(如果类是public的,类名必须与文件名一致;非public得,无强制要求)。如果想把多个java类放在一个java文件中,则只能有一个public类。如下面的两个类放在同一个文件中就会报错,无法编译通过。 可以看出,因为TestOne.java文件中已经有一个public类TestOne,这时再加了一个public类TestTwo就报错了。如果将类TestTwo前面的public修饰符去掉就没有问题了。 我们下面介绍内部类的概念和使用,所谓内部类,简单的说,就是一个类定义在另一个类的内部。与上面的两个类在同一个文件中不同(TestOne和TestTwo虽然在一个文件中,但相互没有嵌套,是并列的)。 采用内部类,有时会对代码的可读性带来一点问题,但很多场景下还是很有必要的。尤其在使用一些框架(如java的swing,集合框架中的排序操作)等,使用内部类(尤其是匿名内部类)会带来很多便利;再比如我们在开发Android app时,就会大量的使用内部类,如果不了解内部类的含义和使用规则,几乎无法顺利的进行Android app的开发。 内部类有其特别的地方,其中核心之一是,内部类实例可以访问包含它的外部类的所有成员,包括private成员。另外非常关键的一点是,内部类的使用必须与一个外部类的实例绑定,注意是实例,后面的例子中会说明这点。 内部类也分好几种情况,下面一一来解释。 一、一般内部类…
JAVA: Socket(套接字)讲解
什么是 Socket Socket(套接字):是在网络上运行两个程序之间的双向通信链路的一个端点。socket绑定到一个端口号,使得 TCP 层可以标识数据最终要被发送到哪个应用程序。 正常情况下,一台服务器在特定计算机上运行,并具有被绑定到特定端口号的 socket。服务器只是等待,并监听用于客户发起的连接请求的 socket 。…
Java 取得使用者输入 java.util.Scanner用法
Scanner是新增的一个简易文本扫描器,在 JDK 5.0之前,是没有的。查看最新在线文档: public final class Scanner extends Object…