Java 初始化

Java 类初始化的时机

规范定义类的初始化时机为 “initialize on first active use”,即 “在首次主动使用时初始化” 。装载和链接在初始化之前就要完成

首次主动使用的情形:

  • 创建类的新实例 -- new,反射,克隆或反序列化
  • 调用类的静态方法
  • 操作类和接口的静态字段(final 字段除外,因为存在常量池中)
  • 调用 Java 的特定的反射方法
  • 初始化一个类的子类
  • 指定一个类作为 Java 虚拟机启动时的初始化类(含有 main 方法的启动类)

除了以上 6 种情形,Java 中类的其他使用方式都是被动使用,不会导致类的初始化

Java 对象初始化的时机

对象初始化又称为对象实例化。Java 对象在其被创建时初始化。有两种方式创建 Java 对象:
① 显示对象创建,通过 new 关键字来调用一个类的构造函数,通过构造函数创建一个对象
② 隐式对象创建:

  • 加载一个包含 String 字面量的类或接口会引起一个新的 String 对象创建,除非包含相同字面量的 String 对象已经在 JVM 中存在了
String s1 = "zheng";
  • 自动装箱机制可能会引起一个原子类型的包装类对象被创建
Integer iWrapper = 1;
  • String 连接符也可能会引起新的 String 或者 StringBuilder 对象被创建,同时还有可能引起原子类型的包装对象被创建
System.out.println("zheng" + 1);

Java 如何初始化对象

当一个对象被创建之后,虚拟机会为其分配内存,主要用来存放对象的实例变量及其从超类继承过来的实例变量(即使这些从超类继承过来的实例变量有可能被隐藏也会被分配空间)。在为这些实例变量分配内存的同时,这些实例变量也会被赋予默认值

关于实例变量隐藏

class Foo {
    int i = 0;
}

class Bar extends Foo {
    int i = 1;
    public static void main(String... args) {
        Foo foo = new Bar();
        System.out.println(foo.i);
    }
}

上面的代码中,Foo 和 Bar 中都定义了变量 i,在 main 方法中,我们用 Foo 引用一个 Bar 对象,如果实例变量与方法一样,允许被覆盖,那么打印的结果应该是 1,但是实际的结果却是 0

但是如果我们在 Bar 的方法中直接使用 i,那么用的会是 Bar 对象自己定义的实例变量 i,这就是隐藏,Bar 对象中的 i 把 Foo 对象中的 i 给隐藏了,这条规则对于静态变量同样适用

在内存分配完成之后,Java 的虚拟机就会开始对新创建的对象执行初始化操作

域初始化

如果在构造器中没有显式地给域赋予初值,那么就会被自动地赋为默认值:数值为 0、布尔值为 false、对象引用为 null

这是域与局部变量的主要不同点。必须明确地初始化方法中的局部变量,否则使用时会出现编译错误。但如果没有初始化类中的域,将会被自动初始化为默认值

但是,这并不是一种良好的编程习惯,如果调用 get 方法,则会得到一个 null 引用

一般都会在执行构造器之前,显式域初始化,直接将一个值赋给任何域。初始值不一定是常量值,可以调用方法对域进行初始化:

class Person {
    private static int nextId;
    private int id = assignId();
    
    private static int assignId() {
        return nextId ++;
    }
}

构造器

如果编写一个类时没有编写构造器,那么系统就会提供一个无参数构造器,这个构造器将所有实例域设为默认值

如果类中提供了至少一个构造器,但是没有提供无参数的构造器,则在构造对象时如果没有提供参数就会被视为不合法。例如,为 Employee 类提供了一个带参数的构造器:

Employee(String name, double salary)

如果不提供无参数构造器的话,默认构造对象就是不合法的,也就是说,调用

e = new Employee();

将会产生错误

超类构造器

Java 要求一个对象被初始化之前,其超类也必须被初始化,这一点是在构造器中保证的。Java 强制要求 Object 对象之外的所有对象构造器的第一条语句必须是超类构造器的调用语句或者是类中定义的其他的构造器,如果我们既没有调用其他的构造器,也没有显式调用超类的构造器,那么编译器会为我们自动生成一个对超类默认(没有参数)构造器的调用指令。如果超类没有不带参数的构造器,并且在子类的构造器又没有显式地调用超类的其他构造器,则编译器将报错

因此,如果我们显式调用超类的构造器,那么调用指令必须放在构造器所有代码的最前面,是构造函数的第一条指令。这么做才可以保证一个对象在初始化之前其所有的超类都被初始化完成

如果我们在一个构造器中调用另外一个构造器,如下所示:

public class ConstructorExample {
    private int i;
 
    ConstructorExample() {
        this(1);
        ....
    }
 
    ConstructorExample(int i) {
        ....
        this.i = i;
        ....
    }
}

对于这种情况,Java 只允许在 ConstructorExample(int i) 内出现调用超类的构造器,也就是说,下面的代码编译是无法通过的:

public class ConstructorExample {
    private int i;
 
    ConstructorExample() {
        super();
        this(1);
        ....
    }
 
    ConstructorExample(int i) {
        ....
        this.i = i;
        ....
    }
}

Java 对构造器作出这种限制,目的是为了要保证一个类中的实例变量在被使用之前已经被正确地初始化,不会导致程序执行过程中的错误

初始化块

初始化块将在构造器执行之前完成。实际上,如果对实例变量直接赋值或者使用初始化块赋值,那么编译器会将其中的代码放到类的构造器中去,并且这些代码会被放在对超类构造器的调用语句之后,构造器本身的代码之前

public class InstanceInitializer {
    {
        j = i;
    }
 
    private int i = 1;
    private int j;
}
 
public class InstanceInitializer {
    private int j = i;
    private int i = 1;
}

上面的这些代码都是无法通过编译的,编译器会抱怨说我们使用了一个未经定义的变量

对象初始化顺序

实例化一个类的对象的过程是一个典型的递归过程:

在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到 Object 类。此时,首先实例化 Object 类,再依次对以下各类进行实例化,直到完成对目标类的实例化。具体而言,在实例化每个类时,都遵循如下顺序:先依次执行实例变量初始化和实例代码块初始化,再执行构造器初始化。也就是说,编译器会将实例变量初始化和实例代码块初始化相关代码放到类的构造器中去,并且这些代码会被放在对超类构造器的调用语句之后,构造器本身的代码之前

class Foo {
    int i;
 
    Foo() {
        i = 1;
        int x = getValue();
        System.out.println(x);
    }
 
    protected int getValue() {
        return i;
    }
}
 
class Bar extends Foo {
    int j;
 
    Bar() {
        j = 2;
    }
 
    @Override
    protected int getValue() {
        return j;
    }
}
 
public class ConstructorExample {
    public static void main(String... args) {
        Bar bar = new Bar();
    }
}

运行上面这段代码,会发现打印出来的结果既不是 1,也不是 2,而是 0。

根本原因就是 Bar 重载了 Foo 中的 getValue 方法。在执行 Bar 的构造函数时,编译器会在 Bar 构造函数开头插入调用 Foo 的构造函数的代码,而在 Foo 的构造函数中调用了 getValue 方法。由于 Java 对构造函数的执行没有做特殊处理,因此这个 getValue 方法是被 Bar 重载的那个 getValue 方法,而在调用 Bar 的 getValue 方法时,Bar 的构造函数还没有被执行,这个时候 j 的值还是默认值 0,因此我们就看到了打印出来的 0

类初始化

当类被第一次使用的时候会被初始化,而且只会被一个线程初始化一次。类的初始化顺序和对象一样:初始化一个类前,会依次递归初始化该类的父类,直到递归到 Object 类。我们可以通过静态初始化器和静态域初始化来完成对类变量的初始化工作,比如:

public class StaticInitializer {
    static int i = 1;

    static {
        i = 2;
    }
}

静态域初始化和静态初始化器基本同实例域初始化和实例初始化器相同,也有相同的限制(按照编码顺序被执行,不能引用后定义和初始化的类变量)。静态域初始化和静态初始化器中的代码会被编译器放到一个名为 static 的方法中(static 是 Java 语言的关键字,因此不能被用作方法名,但是 JVM 却没有这个限制),在类被第一次使用时,这个 static 方法就会被执行。上面的 Java 代码编译之后的 static 方法字节码如下:

static {};
  Code:
   Stack=1, Locals=0, Args_size=0
   iconst_1
   putstatic    #10; //Field i:I
   iconst_2
   putstatic    #10; //Field i:I
   return

JVM 运行时数据区

JVM 内存可简单分为三个区:堆(heap)、栈(stack)和方法区(method):

  • 堆区
    存放对象本身(包括非 static 成员变量),所有线程共享

  • 栈区
    存放基础数据类型、对象的引用,每个线程独立空间,不可互相访问
    栈分为 3 个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)

  • 方法区(静态区,包含常量池)
    存永远唯一元素(类-class、类变量、static 变量、方法),所有线程共享

实例化一个对象的执行顺序

父类静态代码 -> 子类静态代码 -> 父类非静态代码 -> 父类构造函数 -> 子类非静态代码 -> 子类构造函数

由流程图可知:

  1. static 代码只在类初始化时加载一次,加载后存在方法区,而每一个对象在实例化时,只是在堆中保存指向方法区的引用,所以全局唯一,一改都改,节省资源
  2. 因为 static 代码在对象被实例化之前和类初始化一起执行,所以除了可以通过对象应用外,也可以直接通过类名引用
  3. 因为先执行静态代码,再执行非静态代码,所以 static 代码仅能访问 static 数据、static 方法
  4. this、super 属于非静态代码,所以不能引用 this 和 super

一个实例变量在对象初始化的过程中会被赋值几次

  • JVM 在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个时候实例变量被第一次赋值,这个赋值过程是没有办法避免的

  • 如果在域初始化中对某个实例变量做了初始化操作,那么这个时候,这个实例变量就被第二次赋值了

  • 如果在初始化器中,又对变量做了初始化操作,那么这个时候,这个实例变量就被第三次赋值了

  • 如果在类的构造函数中,也对变量做了初始化操作,那么这个时候,变量就被第四次赋值

也就是说,一个实例变量,在 Java 的对象初始化过程中,最多可以被初始化 4 次

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,494评论 18 139
  • Java的初始化可以分为两个部分:(a)类的初始化(b)对象的创建(a)类的初始化 **一、概念介绍: ** 一个...
    梦工厂阅读 4,161评论 1 24
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,493评论 18 399
  • 大一萌新( ̄∇ ̄): 窦靖童说:作为大一的小白 希望今后的自己可以认真学习 不挂科 多参与一些社团活动 再变得美美...
    江黍离阅读 149评论 0 0
  • 德国探亲游记五 2月19日星期五,我们一行三人从马尔堡出发,坐火车到法兰克福1小时。到法兰克福换高铁4个小时直达巴...
    年伢阅读 1,052评论 12 26