Java中类的初始化和实例化区别详解
作者:Marvin-Fox
一、区别
类的初始化:
是完成程序执行前的准备工作。
在这个阶段,静态的(变量,方法,代码块)会被执行。同时在会开辟一块存储空间用来存放静态的数据。
初始化只在类加载的时候执行一次
类的实例化(实例化对象):
是指创建一个对象的过程。这个过程中会在堆中开辟内存,将一些非静态的方法,变量存放在里面。在程序执行的过程中,可以创建多个对象,既多次实例化。每次实例化都会开辟一块新的内存。(就是调用构造函数)
在每个类初始化使用前,都会先对该类进行加载(不是指类的实例化)
类加载有几个步骤,加载->验证->准备->解析->初始化
在编译过程会把常量的值放入类的常量池中,在准备过程会对类变量(static修饰的变量)赋初始值,也就是零值,同时会将常量的值赋予常量;在初始化过程会按照类文件中的声明顺序执行类变量的赋值和静态语句块(static{}块),如果父类还没有初始化会先进行父类的初始化,完成后才会进行子类的初始化。
可以看到在初始化阶段就会执行static{}块的语句,而每一个类在运行过程中一般只会被加载一次(不是指实例化,特殊情况是使用其他类加载器对类进行加载),所以只会完成一次初始化过程,因此也就只会执行static{}块一次。 类的实例化和类的加载是两个不同的概念。
1.主要区别
2.基础知识
2.1 java类的生命周期:
指一个class文件从加载到卸载的全过程,类的完整生命周期包括7个部分:加载——验证——准备——解析——初始化——使用——卸载,如下图所示
其中,验证——准备——解析 称为连接阶段,除了解析外,其他阶段是顺序发生的,而解析可以与这些阶段交叉进行,因为Java支持动态绑定(晚期绑定),需要运行时才能确定具体类型;在使用阶段实例化对象
2.2 类加载过程:
- 加载:通过类名获取类的二进制字节流是通过类加载器来完成的。其加载过程使用“双亲委派模型”
- 验证:当一个类被加载之后,必须要验证一下这个类是否合法,比如这个类是不是符合字节码的格式、变量与方法是不是有重复、数据类型是不是有效、继承与实现是否合乎标准等等。总之,这个阶段的目的就是保证加载的类是能够被jvm所运行。
- 准备:为类变量(静态变量)在方法区分配内存,并设置零值。注意:这里是类变量,不是实例变量,实例变量是对象分配到堆内存时根据运行时动态生成的。
- 解析:把常量池中的符号引用解析为直接引用:根据符号引用所作的描述,在内存中找到符合描述的目标并把目标指针指针返回。
- 初始化:类的初始化过程是这样的:按照顺序自上而下运行类中的变量赋值语句和静态语句,如果有父类,则首先按照顺序运行父类中的变量赋值语句和静态语句。在类的初始化阶段,只会初始化与类相关的静态赋值语句和静态语句,也就是有static关键字修饰的信息,而没有static修饰的赋值语句和执行语句在实例化对象的时候才会运行。执行<clinit>()方法(clinit是class initialize的简写)
- 实例化:在堆区分配内存空间,执行实例对象初始化,设置引用变量a指向刚分配的内存地址
二、案例详解
1、示例1
首先来看一个最简单的例子(无父类且无静态成员变量)
public class OrderOfInitialization1 { public static void main(String[] args) { House house = new House(); house.f(); } } class Window { Window(int market) { System.out.println("Window(" + market + ")"); } } class House { Window w1 = new Window(1);// before constructor House() { // show we're in constructor System.out.println("House()"); w3 = new Window(33);//reinitialize w3 } Window w2 = new Window(2);// after constructor void f() { System.out.println("f()"); } Window w3 = new Window(3);//at end }
output:
Window(1)
Window(2)
Window(3)
House()
Window(33)
f()
从输出结果分析,House 实例变量的初始化顺序是: w1
, w2
, w3
,然后才是构造函数。
即:实例变量按照其在代码中出现的顺序执行初始化,然后执行构造函数里面的初始化。
2、示例2
接下来看一个稍微复杂一些的例子(有父类但无静态成员变量)
public class OrderOfInitialization2 { public static void main(String[] args) { SubClass subClass = new SubClass(); } } class Print { Print(int i) { System.out.println("new Print(" + i + ")"); } } class SuperClass { Print print1 = new Print(1); public SuperClass() { System.out.println("new SuperClass()"); } Print print2 = new Print(2); } class SubClass extends SuperClass { Print print3 = new Print(3); public SubClass() { //这个地方其实是调用了父类的默认的无参构造函数,super(); //如果父类没有无参构造函数,则这个地方必须显式的调用父类的构造函数,否则编译不通过 System.out.println("new SubClass()"); } Print print4 = new Print(4); }
output:
new Print(1)
new Print(2)
new SuperClass()
new Print(3)
new Print(4)
new SubClass()
从输出结果分析:这个地方是先调用了父类 SuperClass 的构造函数,然后调用子类 SubClass 的构造函数。
即:如果一个类有父类,在实例化子类的时候,会先执行父类的构造函数,然后执行子类的构造函数。
3、示例3
继续看一个更复杂一些的例子(有父类且有静态成员变量)
public class OrderOfInitialization3 { public static void main(String[] args) { Man man = new Man(); Man man1 = new Man(); } static Print1 print0 = new Print1(0); } class Print1 { Print1(int i) { System.out.println("new Print1(" + i + ")"); } } class People { Print1 print1 = new Print1(1); public People() { System.out.println("new People()"); } Print1 print2 = new Print1(2); static Print1 print5 = new Print1(5); } class Man extends People { Print1 print3 = new Print1(3); public Man() { System.out.println("new Man()"); } Print1 print4 = new Print1(4); static Print1 print6 = new Print1(6); }
output:
new Print(0)
new Print(5)
new Print(6)
new Print(1)
new Print(2)
new People()
new Print(3)
new Print(4)
new Man()
new Print(1)
new Print(2)
new People()
new Print(3)
new Print(4)
new Man()
从输出结果分析:这里首先执行了 OrderOfInitialization3 类的静态变量 print0 的初始化(输出 new Print(0)),然后执行静态方法 main;
紧接着是执行 People 类的静态成员变量 print5 的初始化(输出 new Print(5)),再接着是 Man 类的静态成员变量 print6 的初始化(输出 new Print(6));
之后是 People 的实例变量(输出new Print(1)、new Print(2))、构造函数(输出 new People())初始化
最后才是 Man 实例变量(输出 new Print(3)、new Print(4))、构造函数(输出 new Man())的初始化。
在第二次实例化一个 Man 的时候,所有的静态成员变量都没有相应的输出,即静态成员变量只初始化了一次。
所以这个地方执行的顺序是:首先执行 main 所在类的静态成员变量的初始化,然后是 Man 的父类的静态成员变量的初始化,然后是子类的静态成员的初始化;
接着是父类的构造函数,最后才是子类的构造函数。
这个地方 Man 实例化了两次,但是其父类和本身的静态成员变量只初始化了一次。
为什么静态成员变量只会初始化一次呢?
- 实际上,静态成员变量初始化的过程本质上就是一个类的加载和初始化的过程,虚拟机保证了在同一个类加载器下,一个类型只会初始化一次。
4、总结一下
这个地方把类和对象分开会更好理解一点。
(1)类的初始化
- 静态成员变量初始化发生在静态方法之前
- 父类的初始化必须在子类初始化之前
- 静态成员变量的初始化顺序为其在代码中出现的顺序
(2)实例化对象
- 如果有父类,先执行父类的实例化
- 成员变量初始化发生在构造函数之前
- 成员变量的初始化顺序为其在代码中出现的顺序
(3)实例化对象之前如果该类没有初始化,必须先执行该类的初始化。
5、最后看一个比较特殊的例子
public class NestedInitialization { public static void main(String[] args) { staticFunction(); } static NestedInitialization st = new NestedInitialization(); static { System.out.println("1"); } { System.out.println("2"); } NestedInitialization() { System.out.println("3"); System.out.println("a=" + a + ",b=" + b); } public static void staticFunction() { System.out.println("4"); } int a = 110; static int b = 112; }
output:
2
3
a=110,b=0
1
4
这个例子的特殊性在于,**该类还没有完成初始化,就去实例化一个该类的对象。**我们从类的生命周期来分析,一个类会经历加载、验证、准备、解析、初始化、使用和卸载七个阶段,在执行到 static NestedInitialization st = new NestedInitialization();
这一步时,已经是类的初始化阶段了,此时,NestedInitialization 类里面的静态成员的值都还是准备阶段设置的初始零值,即 static NestedInitialization st = null
, static int b = 0;
,然后这个地方需要实例化 NestedInitialization,所以是以此时的状态去实例化 NestedInitialization 类的,执行类的实例化,然后在继续类的初始化。所以才会出现上面的输出结果。
到此这篇关于Java中类的初始化和实例化区别详解的文章就介绍到这了,更多相关Java初始化和实例化内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!