说在前面的几句废话
前几天我的文章一直没有更新,大概断了4天左右,因为外出有事,所以给耽搁了,有朋友建议,我可以集中花几天,每天只写文章,这样以后觉得哪里不妥还可以修改,觉得言之有理就果断采纳了,前一个月开始做自媒体写文章,这段时间有很多人给我提出了宝贵的意见,真的感谢大家的支持。同时,今天也是建党节,作为一个爱国青年,真心祝愿祖国越来越好。
废话可以不看,下面就开始今天的文章。java对象的创建操作其实我在《JVM系列之类的加载机制》一文曾经提到过,包含两个过程:类的初始化和实例化。为此为了理解的深入,我们还需要再来看一下类的生命周期。一张图表示:
image从上面我们可以看到,对象的创建其实包含了初始化和使用两个阶段。有了这个印象之后,我们就能开始今天的文章了。先给出这篇文章的大致脉络:
首先,介绍一下java中对象的创建基本知识
然后,介绍一下对象初始化的顺序
接下来,介绍一下创建对象的几种方式
最后,进行一个总结。(从内存角度去分析:重点)重点
一、基本知识
我们知道,一个对象的创建过程包含两个过程:初始化和实例化
我们在使用一个对象时,JVM首先会检查相关类型是否已经加载并初始化,如果没有,则JVM立即进行加载并调用类构造器完成类的初始化。在类初始化过程中或初始化完毕后,根据具体情况才会去对类进行实例化。
实例化时候,java虚拟机就会为其分配内存来存放自己及其从父类继承过来的实例变量。在为这些实例变量分配内存的同时,这些实例变量先会被赋予默认值(零值)。在内存分配完成之后,Java虚拟机才会对新创建的对象赋予我们程序员给定的值。
小结:创建一个对象包含下面两个过程:
1、类构造器完成类初始化(分配内存、赋予默认值)
2、类实例化(赋予给定值)
二、类初始化
下面我们直接给出一个例子看一下java是如何初始化的。我们知道一个类中,往往包含静态变量、静态代码块、变量、普通方法、构造方法等信息。那么他们是如何初始化的呢?
public class InitialTest {
//静态变量
public static String staticField = "静态变量";
//变量
public String field = "==普通变量==";
// 静态代码块
static {
System.out.println( staticField );
System.out.println( "静态初始化块" );
}
// 初始化块
{
System.out.println( field );
System.out.println( "==初始化块==" );
}
// 构造方法
public InitialOrderTest(){
System.out.println( "构造器" );
}
public static void main( String[] args ){
new InitialOrderTest();
}
}
输出:
image上面这个例子比较简单,我们再来看看带有父类和接口的情况。
第一步:定义一个父类
class FatherClass {
// 静态变量
public static String parent_StaticField = "父----静态变量";
// 变量
public String parent_Field = "父类----普通变量";
// 静态初始化块
static {
System.out.println(parent_StaticField);
System.out.println("父类------静态初始化块");
}
// 初始化块
{
System.out.println(parent_Field);
System.out.println("父类-----初始化块");
}
// 构造器
public FatherClass() {
System.out.println("父类--构造器");
}
}
第二步:定义一个子类实现
public class SubSon extends FatherClass {
// 静态变量
public static String son_StaticField = "子类--静态变量";
// 变量
public String son_Field = "子类--变量";
// 静态初始化块
static {
System.out.println(son_StaticField);
System.out.println("子类--静态初始化块");
}
// 初始化块
{
System.out.println(son_Field);
System.out.println("子类--初始化块");
}
// 构造器
public SubSon(){
System.out.println( "子类--构造器" );
}
public static void main(String[] args) {
System.out.println("子类-----main方法");
new SubSon();
}
}
第三步:看结果
image小结,类的初始化顺序,这样看确实不好记,不过没有继承关系的我们都能很好的看到。带继承关系的,使用网上一张图来表示:
imageOK,类的初始化中的知识点基本上就是初始化的顺序。
三、创建对象的几种方式
其实对象的初始化就是在创建对象的时候由jvm完成的。对于创建对象,主要是研究创建对象的几种方式。下面一一的解答.这里给出6种方式,面试的时候足够你zhuangbility。
- 使用new关键字
- Class对象的newInstance()方法
- 构造函数对象的newInstance()方法
- 对象反序列化
- Object对象的clone()方法
- 使用Unsafe类创建对象
- 最后再揭晓。。。
OK,先认识一个,下面一个一个看。
(1)使用new关键字
Test t1 = new Test();
Test t2 = new Test("java的架构师技术栈");
(2)class的newInstance()方法
public static void main(String[] args) throws Exception {
String className = "com.fdd.Test";
Class clasz = Class.forName(className);
Test t = (Test) clasz.newInstance();
}
首先我们通过Class.forName()动态的加载类的Class对象,
然后通过newInstance()方法获得Test类的对象
(3)构造函数的newInstance()方法
public static void main(String[] args) throws Exception {
Constructor<Test> constructor;
try {
constructor = Test.class.getConstructor();
Test t = constructor.newInstance();
} catch (Exception){
e.printStackTrace();
}
}
类Constructor也有newInstance方法,这一点和Class有点像。从它的名字可以看出它与Class的不同,Class是通过类来创建对象,而Constructor则是通过构造器。
(4)序列化
public static void main(String[] args) throws Exception {
String filePath = "sample.txt";//序列化的路径
Test t1 = new Test("java的架构师技术栈");
try {
//t1开始序列化
FileOutputStream fileOutputStream = new FileOutputStream(filePath);
ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);
outputStream.writeObject(t1);
outputStream.flush();
outputStream.close();
//t2开始反序列化
FileInputStream fileInputStream = new FileInputStream(filePath);
ObjectInputStream inputStream = new ObjectInputStream(fileInputStream);
Test t2 = (Test) inputStream.readObject();
inputStream.close();
System.out.println(t2.getName());
} catch (Exception ee) {
ee.printStackTrace();
}
}
首先我们要对Test实现Serializable接口。然后开始序列化数据。最后得到反序列化的对象。
(5)clone方式
Object对象中存在clone方法,它的作用是创建一个对象的副本。
public static void main(String[] args) throws Exception {
Test t1 = new Test("java的架构师技术栈");
Test t2 = (Test) t1.clone();
System.out.println(t2.getName());
}
(6)使用Unsafe类创建对象
Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。Oracle正在计划从Java 9中去掉Unsafe类,如果真是如此影响就太大了。
我们无法直接创建Unsafe对象。这里我们使用反射方法得到
private static Unsafe getUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
return unsafe;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
拿到这个对象后,调用其中的native方法allocateInstance 创建一个对象实例
Object event = unsafe.allocateInstance(Test.class);
四、总结
我们想要创建一个对象。基本上就是java虚拟机分配内存的过程。因此我们可以先回顾一下java程序的执行过程。给一张网上的图,写的很清晰
image一个例子去解释:(摘自我之前的文章《java8的内存结构》)
public class Person{
int age;
String name;
public void walk() {
System.out.println("我正在走路。。。。");
}
}
然后我们测试一下:
public class Test {
public static void main(String[] args) {
Person person = new Person();
person.name = "java的架构师技术栈";
person.age = 18;
person.walk();
}
}
我们分析一下这个过程
第一步,JVM去方法区寻找Test类的代码信息,如果有直接调用,没有的话使用类的加载机制把类加载进来。同时把静态变量、静态方法、常量加载进来。这里加载的是(“冯冬冬的IT技术栈”,“冯XX”);这是因为字符串是常量,age中的18是基本类型。
第二步,jvm进入main方法,看到Person person=new Person()。首先分析Person这个类,同样的寻找Person类的代码信息,有就加载,没有的话类加载机制加载进来。同时也加载静态变量、静态方法、常量(“我正在走路。。。”)
第三步,jvm接下来看到了person,person在main方法内部,因而是局部变量,存放在栈空间中。
第四步,jvm接下来看到了new Person()。new出的对象(实例),存放在堆空间中。
第五步,jvm接下来看到了“=”,把new Person的地址告诉person变量,person通过四字节的地址(十六进制),引用该实例。 是不是有点晕,别着急,画个图看一下。
内存结构模板1.png
第六步,jvm看到person.name = “冯冬冬的IT技术栈”;person通过引用new Person实例的name属性,该name属性通过地址指向常量池的"冯冬冬的IT技术栈"。 第七步,jvm看到person.age = 18; person的age属性是基本数据类型,直接赋值。
第八步,jvm看到person.walk(); 调用实例的方法时,并不会在实例对象中生成一个新的方法,而是通过地址指向方法区中类信息的方法。走到这一步再看看图怎么变化的
image大多数人基本上都能看懂。创建一个对象的过程基本上就是这。
如有问题还请批评指正。谢谢
喜欢的还请关注我的微信公众号:java的架构师技术栈。可获取从基础一直到架构师的各个方面的视频教程和书籍。
微信公众号.png
网友评论