1. 大部分人的直觉:new+构造方法
首先我们思考一下我们如何创建一个对象,最普通的方式就是:new+构造方法。所以很正常,我们会认为做完这件事之后,一个对象就被创建了。其实这并没错,因为我们完成这件事之后我们就可以对该对象做各种操作:访问某些字段,调用某些方法等。但是这取决于,你怎么理解“创建对象”对象这件事。
2. 构造方法其实是一个很矛盾的存在
我们在构造方法中最常做的事情是这样的:
public ClassA(int a) {
this.a = a;
}
这里的this,指代的就是当前对象。但是我们用构造方法来创建一个对象,我们的理解应该是:构造方法结束之后,该对象才被创建、实例化完成。那么为什么在构造方法内部就可以引用这个对象呢?
所以我们可以推断出来:在构造器被invoke之前,这个对象的空间就已经存在了。只是可能我们没有办法引用到。而真正开辟了这块空间的指令就是new。
3. 构造器在对象被创建之后才执行
除了上述的逻辑推理和思考,我们也可以用代码来验证:“在构造器被invoke之前,对象就已经存在了”这件事。
具体的代码可以看这篇:JAVA对象实例是何时被创建的?
4. 构造器做了什么事情?
好吧,那既然构造器没有给这个对象开辟内存空间,那它做了什么事情呢?
构造器其实并不是严格意义上的“创建了一个对象”,它做的任务有两个:
- 给对象进行构造器内部的初始化
- 把堆内对象的首地址赋给栈内的引用变量
5. 对象创建的步骤
第一步:判断类是否被加载、连接、初始化(类加载的过程,具体请看JVM类加载器的相关知识)。【这里是clinit方法,所以只涉及到类变量和静态方法】都在方法区里,因为涉及的是类。
第二步:为对象分配内存,这个就叫new;就是在堆里分配一块空间,这个时候分配多少已经可以知道了。如果是int这种就4个字节double就8个字节。如果是引用类型的话,也是4个字节,因为它只起一个引用的作用,没有真正把对象放在这里。
第三步:处理并发安全问题;因为对象的创建还是比较频繁的,肯定不能两个对象都抢同一块内存吧。
第四步:初始化分配到的空间;所有属性设置默认值,保证对象实例化字段在不赋值的时可以直接使用。
属性的默认初始化【4】,显式初始化/代码块中初始化【6】,构造器初始化【6】
第五步:设置对象的对象头。对象头里记录一下自己所属的类->指向方法区。还记录一些乱七八糟的东西hashCode, gc信息,锁之类的。
第六步:执行init()方法进行初始化:显式初始化,这里才涉及到类的构造器的调用。
在Java程序的视角来看,初始化才正式开始。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内的对象的首地址赋值给引用变量。
因此一般来说(由字节码中是否跟随者invokespecial指令所决定),new指令之后会接着就是执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全创建出来。
6. 总结
说到这里,我们得出结论:真正为对象开辟内存空间的指令是new,构造器在被调用之前该对象就已经存在了。
构造器其实并不是严格意义上的“创建了一个对象”,它做的任务有两个:1. 给对象进行构造器内部的初始化 2. 把堆内对象的首地址赋给栈内的引用变量。
构造器本身就是一个很矛盾的东西:你在里面可以写this.属性 = 形参这样初始化。this指代的是什么?其实就是当前对象。而我们以往认识的构造器是什么:是用来创建对象的,我们觉得,构造器内所有的语句执行完成了,这个对象才算是被创建了。但为什么可以在构造器内部直接指明当前对象呢?
所以推断出来:在构造器被invoke之前,这个对象的空间就已经存在了。只是可能我们没有办法引用到。而真正开辟了这块空间的指令就是new。
我们为什么总认为:是构造器创建了这个对象呢?
从人类而非机器的角度来思考这件事,说“使用构造器创建了一个对象”是完全可以理解的,这取决于你如何定义“一个对象被创建”这件事
如果你认为,一个对象有了自己的内存空间就算作被创建了,那么构造器确实没有为对象分配空间,对象是在构造器被invoke之前创建的。
但是对于一个人类来说,你想要使用这个对象,必须要通过构造器给引用变量赋值的方式,才能使用对象中的字段和方法。并且构造器之前对于对象的修饰和默认初始化(比如int默认初始化为0)是远远达不到我们使用对象的标准的,我们想要达到的,是通过构造器,把我们想要的参数传入,并赋值给字段。每个字段根据自我意愿赋值后的这样一个对象,才是我们希望去使用的。
本文探讨了对象创建的过程,指出构造方法并非真正创建对象,而是负责初始化和赋值。对象在构造器调用前已存在于内存中,构造器主要任务是执行初始化和将对象地址赋给引用变量。理解对象创建的步骤有助于深入掌握Java对象实例化原理。
1863

被折叠的 条评论
为什么被折叠?



