`
Mysun
  • 浏览: 273908 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

Java对象初始化详解

阅读更多
在Java中,一个对象在可以被使用之前必须要被正确地初始化,这一点是Java规范规定的。本文试图对Java如何执行对象的初始化做一个详细深入地介绍(与对象初始化相同,类在被加载之后也是需要初始化的,本文在最后也会对类的初始化进行介绍,相对于对象初始化来说,类的初始化要相对简单一些)。
1.Java对象何时被初始化
Java对象在其被创建时初始化,在Java代码中,有两种行为可以引起对象的创建。其中比较直观的一种,也就是通常所说的显式对象创建,就是通过new关键字来调用一个类的构造函数,通过构造函数来创建一个对象,这种方式在java规范中被称为“由执行类实例创建表达式而引起的对象创建”。
当然,除了显式地创建对象,以下的几种行为也会引起对象的创建,但是并不是通过new关键字来完成的,因此被称作隐式对象创建,他们分别是:
  • 加载一个包含String字面量的类或者接口会引起一个新的String对象被创建,除非包含相同字面量的String对象已经存在与虚拟机内了(JVM会在内存中会为所有碰到String字面量维护一份列表,程序中使用的相同字面量都会指向同一个String对象),比如,
  • class StringLiteral {
    	private String str = "literal";
    	private static String sstr = "s_literal"; 
    }
    
  • 自动装箱机制可能会引起一个原子类型的包装类对象被创建,比如,
  • class PrimitiveWrapper {
    	private Integer iWrapper = 1;
    }
    
  • String连接符也可能会引起新的String或者StringBuilder对象被创建,同时还可能引起原子类型的包装对象被创建,比如(本人试了下,在mac ox下1.6.0_29版本的javac,对待下面的代码会通过StringBuilder来完成字符串的连接,并没有将i包装成Integer,因为StringBuilder的append方法有一个重载,其方法参数是int),
  • public class StringConcatenation {
    	private static int i = 1;
    	
    	public static void main(String... args) {
    		System.out.println("literal" + i);
    	}
    }
    

    2.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的虚拟机就会开始对新创建的对象执行初始化操作,因为java规范要求在一个对象的引用可见之前需要对其进行初始化。在Java中,三种执行对象初始化的结构,分别是实例初始化器、实例变量初始化器以及构造函数。
    2.1. Java的构造函数
    每一个Java中的对象都至少会有一个构造函数,如果我们没有显式定义构造函数,那么Java编译器会为我们自动生成一个构造函数。构造函数与类中定义的其他方法基本一样,除了构造函数没有返回值,名字与类名一样之外。在生成的字节码中,这些构造函数会被命名成<init>方法,参数列表与Java语言书写的构造函数的参数列表相同(<init>这样的方法名在Java语言中是非法的,但是对于JVM来说,是合法的)。另外,构造函数也可以被重载。
    Java要求一个对象被初始化之前,其超类也必须被初始化,这一点是在构造函数中保证的。Java强制要求Object对象(Object是Java的顶层对象,没有超类)之外的所有对象构造函数的第一条语句必须是超类构造函数的调用语句或者是类中定义的其他的构造函数,如果我们即没有调用其他的构造函数,也没有显式调用超类的构造函数,那么编译器会为我们自动生成一个对超类构造函数的调用指令,比如,
    public class ConstructorExample {
    	
    }
    

    对于上面代码中定义的类,如果观察编译之后的字节码,我们会发现编译器为我们生成一个构造函数,如下,
       aload_0
       invokespecial	#8; //Method java/lang/Object."<init>":()V
       return
    

    上面代码的第二行就是调用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;
    		....
    	}
    }
    

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

    Java对构造函数作出这种限制,目的是为了要保证一个类中的实例变量在被使用之前已经被正确地初始化,不会导致程序执行过程中的错误。但是,与C或者C++不同,Java执行构造函数的过程与执行其他方法并没有什么区别,因此,如果我们不小心,有可能会导致在对象的构建过程中使用了没有被正确初始化的实例变量,如下所示,
    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。
    2.2. 实例变量初始化器与实例初始化器
    我们可以在定义实例变量的同时,对实例变量进行赋值,赋值语句就时实例变量初始化器了,比如,
    public class InstanceVariableInitializer {
    	private int i = 1;
    	private int j = i + 1;
    }
    

    如果我们以这种方式为实例变量赋值,那么在构造函数执行之前会先完成这些初始化操作。
    我们还可以通过实例初始化器来执行对象的初始化操作,比如,
    public class InstanceInitializer {
    	
    	private int i = 1;
    	private int j;
    	
    	{
    		j = 2;
    	}
    }
    

    上面代码中花括号内代码,在Java中就被称作实例初始化器,其中的代码同样会先于构造函数被执行。
    如果我们定义了实例变量初始化器与实例初始化器,那么编译器会将其中的代码放到类的构造函数中去,这些代码会被放在对超类构造函数的调用语句之后(还记得吗?Java要求构造函数的第一条语句必须是超类构造函数的调用语句),构造函数本身的代码之前。我们来看下下面这段Java代码被编译之后的字节码,Java代码如下,
    public class InstanceInitializer {
    	
    	private int i = 1;
    	private int j;
    	
    	{
    		j = 2;
    	}
    	
    	public InstanceInitializer() {
    		i = 3;
    		j = 4;
    	}
    }
    

    编译之后的字节码如下,
       aload_0
       invokespecial	#11; //Method java/lang/Object."<init>":()V
       aload_0
       iconst_1
       putfield	#13; //Field i:I
       aload_0
       iconst_2
       putfield	#15; //Field j:I
       aload_0
       iconst_3
       putfield	#13; //Field i:I
       aload_0
       iconst_4
       putfield	#15; //Field j:I
       return
    

    上面的字节码,第4,5行是执行的是源代码中i=1的操作,第6,7行执行的源代码中j=2的操作,第8-11行才是构造函数中i=3和j=4的操作。
    Java是按照编程顺序来执行实例变量初始化器和实例初始化器中的代码的,并且不允许顺序靠前的实例初始化器或者实例变量初始化器使用在其后被定义和初始化的实例变量,比如,
    public class InstanceInitializer {
    	{
    		j = i;
    	}
    	
    	private int i = 1;
    	private int j;
    }
    
    public class InstanceInitializer {
    	private int j = i;
    	private int i = 1;
    }
    

    上面的这些代码都是无法通过编译的,编译器会抱怨说我们使用了一个未经定义的变量。之所以要这么做,是为了保证一个变量在被使用之前已经被正确地初始化。但是我们仍然有办法绕过这种检查,比如,
    public class InstanceInitializer {
    	private int j = getI();
    	private int i = 1;
    	
    	public InstanceInitializer() {
    		i = 2;
    	}
    	
    	private int getI() {
    		return i;
    	}
    	
    	public static void main(String[] args) {
    		InstanceInitializer ii = new InstanceInitializer();
    		System.out.println(ii.j);
    	}
    }
    

    如果我们执行上面这段代码,那么会发现打印的结果是0。因此我们可以确信,变量j被赋予了i的默认值0,而不是经过实例变量初始化器和构造函数初始化之后的值。
    引用

    一个实例变量在对象初始化的过程中会被赋值几次?
    在本文的前面部分,我们提到过,JVM在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个时候实例变量被第一次赋值,这个赋值过程是没有办法避免的。
    如果我们在实例变量初始化器中对某个实例x变量做了初始化操作,那么这个时候,这个实例变量就被第二次赋值了。
    如果我们在实例初始化器中,又对变量x做了初始化操作,那么这个时候,这个实例变量就被第三次赋值了。
    如果我们在类的构造函数中,也对变量x做了初始化操作,那么这个时候,变量x就被第四次赋值。
    也就是说,一个实例变量,在Java的对象初始化过程中,最多可以被初始化4次。

    2.3. 总结
    通过上面的介绍,我们对Java中初始化对象的几种方式以及通过何种方式执行初始化代码有了了解,同时也对何种情况下我们可能会使用到未经初始化的变量进行了介绍。在对这些问题有了详细的了解之后,就可以在编码中规避一些风险,保证一个对象在可见之前是完全被初始化的。
    3.关于类的初始化
    Java规范中关于类在何时被初始化有详细的介绍,在3.0规范中的12.4.1节可以找到,这里就不再多说了。简单来说,就是当类被第一次使用的时候会被初始化,而且只会被一个线程初始化一次。我们可以通过静态初始化器和静态变量初始化器来完成对类变量的初始化工作,比如,
    public class StaticInitializer {
    	static int i = 1;
    	
    	static {
    		i = 2;
    	}
    }
    

    上面通过两种方式对类变量i进行了赋值操作,分别通过静态变量初始化器(代码第2行)以及静态初始化器(代码第5-6行)完成。
    静态变量初始化器和静态初始化器基本同实例变量初始化器和实例初始化器相同,也有相同的限制(按照编码顺序被执行,不能引用后定义和初始化的类变量)。静态变量初始化器和静态初始化器中的代码会被编译器放到一个名为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
    

    在第2节中,我们介绍了可以通过特殊的方式来使用未经初始化的实例变量,对于类变量也同样适用,比如,
    public class StaticInitializer {
    	static int j = getI();
    	static int i = 1;
    	
    	static int getI () {
    		return i;
    	}
    	
    	public static void main(String[] args) {
    		System.out.println(StaticInitializer.j);
    	}
    }
    

    上面这段代码的打印结果是0,类变量的值是i的默认值0。但是,由于静态方法是不能被覆写的,因此第2节中关于构造函数调用被覆写方法引起的问题不会在此出现。
    分享到:
    评论
    1 楼 wantodare 2014-03-20  
    class Test1 {
    	{
    		a=1;
    	}
    	private static int a;
    	
    	public static void main(String[] args) {
    		System.out.println(a);
    	}
    }
    

    代码编码通过,输出结果为1,请问初始化未经声明的实例变量有什么特殊的地方吗?

    相关推荐

      java中对象创建、初始化、引用

      ### Java中对象创建、初始化与引用详解 #### 一、Java对象、引用及创建过程 在Java中,对象是程序的基本单位,它包含了属性(成员变量)和行为(方法)。对象是由类创建出来的实例,而类则是一组具有相同属性和...

      java对象初始化代码详解

      Java 对象初始化代码详解 Java 对象初始化代码详解主要介绍了 Java 对象初始化代码详解,涉及实例变量的初始化,类变量的初始化等相关介绍几代码示例,具有一定参考价值,需要的朋友可以了解下。 一、Java 对象...

      03.Java对象初始化1

      Java 对象初始化过程详解 Java 对象的初始化过程是通过 new 指令开始的,首先会根据指令参数在常量池中定位到一个类的符号引用。如果没有定位到这个符号引用,那么这个类就没有被加载,就需要 JVM 进行类的加载。...

      Java变量初始化

      Java 变量初始化详解 Java 变量初始化是 Java 语言的基础知识点之一,但也往往被学习者所忽略。 Java 变量初始化的时机是指在 Java 语言中变量的初始化过程,包括变量的声明、初始化和赋值的步骤。 Java 变量声明 ...

      详解Java的初始化与清理

      接下来,我们讨论Java对象的初始化顺序。在继承体系中,初始化顺序遵循以下规则: 1. 首先,执行静态初始化块(如果存在)。静态初始化块在类加载时执行一次,按它们在代码中的顺序进行。 2. 然后,执行父类的非...

      精通 Hibernate:Java 对象持久化技术详解(第2版).part2

      第2章 Java对象持久化技术概述  2.1 直接通过JDBC API来持久化实体域对象  2.2 ORM简介  2.2.1 对象-关系映射的概念  2.2.2 ORM中间件的基本使用方法  2.2.3 常用的ORM中间件  2.3 实体域对象的其他持久化模式...

      Java中初始化块详解及实例代码

      "Java初始化块详解及实例代码" Java中初始化块是Java语言中的一种特殊的代码块,它可以在类加载或对象创建时执行某些操作。本文将详细介绍Java中初始化块的概念、种类、特点和应用场景。 什么是初始化块 初始化块...

      java中类的初始化顺序

      ### Java中类的初始化顺序详解 #### 一、概述 在Java编程语言中,类的初始化是一个非常重要的概念。类的初始化涉及到多个方面,包括静态成员变量、实例成员变量、静态初始化块、实例初始化块以及构造函数等。本文...

      Java初始化顺序1

      Java 初始化顺序详解 在 Java 中,变量可以分为两类:类变量(静态变量)和实例变量(对象变量)。类变量是使用 static 关键字修饰的变量,它们属于类,而不是对象。实例变量则是没有使用 static 关键字修饰的变量...

      java对象与接口详解

      Java 中的对象和接口是面向对象编程的重要概念,对于初学者来说,理解这两个概念至关重要。本篇将详细解释这两个概念以及它们在实际编程中的应用。 首先,我们来看一下对象。在 Java 中,对象是类的实例,它是程序...

      学习java静态数据初始化.doc

      Java 静态数据初始化详解 在 Java 中,静态数据初始化是指在类加载过程中对静态变量的初始化。静态变量是在类加载时被初始化的,而不是在实例创建时。静态变量的初始化顺序是按照它们在类中的定义顺序进行的。 在 ...

      Java——对象初始化顺序使用详解

      tr); fatherStr = "fatherStr...总之,Java对象初始化顺序是编程过程中必须掌握的基础知识,它涉及到类的加载、实例化过程以及字段和方法的调用顺序。正确理解和运用这些知识,能够使我们写出更加健壮、高效的代码。

      java类中元素初始化顺序详解

      Java 类的初始化顺序是一个关键的编程概念,尤其对于理解和避免潜在的运行时错误至关重要。在 Java 中,类的元素初始化顺序遵循以下规则: 1. **静态变量与静态初始化块**: 首先,Java 解释器会执行类中的静态...

      Java持久化数据结构详解.pdf

      这个类有两个属性:`price`(价格)和`onward`(指向下一个旅程的引用),以及一个构造函数来初始化这些属性。`onward` 属性创建了一个链表结构,可以表示一系列连续的火车旅程。 代码中的 `link()` 函数用于连接两...

      Java中Class对象详解.docx

      ### Java中Class对象详解 #### 一、Class对象概述 在Java编程语言中,`Class`对象是一个非常重要的概念,它代表了Java中的一个类。每个加载到Java虚拟机(JVM)中的类都有对应的`Class`对象。通过`Class`对象,...

      探讨Java的对象是怎么在内存中产生的?

      Java对象的创建不仅仅涉及到简单的`new`关键字操作,而是包含了类加载检查、内存分配、对象初始化等多个步骤。同时,对象在内存中的布局也非常重要,它不仅决定了对象的存储方式,还直接影响到程序的性能表现。通过...

      java对象创建过程

      ### Java对象创建过程详解 在Java编程语言中,对象是程序的基本单元,一切皆对象这一概念使得Java在面向对象编程领域具有重要的地位。本文将详细阐述Java对象的创建过程,帮助读者深入理解Java基础。 #### 一、类...

      Java基础入门编程详解

      学习如何声明、初始化和操作数组是Java基础的一部分。 9. **类与对象**:面向对象是Java的核心特性。类是对象的蓝图,它定义了对象的属性(字段)和行为(方法)。通过`new`关键字实例化对象,实现面向对象编程。 ...

      java学习之神奇初始化

      ### Java学习之神奇初始化 #### 知识点详解 在Java编程语言中,类的初始化顺序是一个非常重要的概念。特别是当涉及到静态成员(`static`)的初始化时,这一顺序对于理解程序的行为至关重要。根据提供的文件信息,...

    Global site tag (gtag.js) - Google Analytics