获得对象的方式
先定义一个Programmer类:
package com.zrt.pojo;
public class Programmer {
private int id;
private String name;
private int age = 18;
public Programmer(){
}
public Programmer(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
1、new一个对象
用关键字 new 进行对象的创建,几乎是写代码时最常用的操作之一,比如:
Programmer programmer01 = new Programmer(1,"zhangsan");
Programmer programmer02 = new Programmer();
通过new的方式,我们可以调用类的无参构造和有参构造来实例出一个Programmer对象。
由上可看出,对象简简单单地new一下就有了,可真相真的如此吗?其实不然,因为有JVM这个牵线红娘背后默默地帮我们做了很多的工作。
new出一个对象的过程大致可描述如图 1-1所示。
-
首先,当我们new一个对象的时候,例如 Programmer programmer02 = new Programmer() ,JVM 首先就会去检查 programmer02 这个引用所代表的Programmer类是否已经被加载过,如果没有的话就要执行对应类的加载过程。
-
声明类型引用很简单,例如 Programmer programmer02 = new Programmer() 就会声明一个Programmer类型的引用programmer02。
-
第1步的类加载完成以后,其实对象所需要的内存大小就已经确定下来了,接下来JVM就会在堆上为对象分配内存。单个对象的内存图如图 1-2所示。
图 1-2 - 所谓的属性“ 0 ”值初始化,其实非常地好理解,就是实例化对象的各个属性并为其赋上默认初始值“ 0 ”值,比如 int 类型的初始值就是0,而一个对象的初始化“ 0 ”值就是null。
- 接下来JVM会进行对象头的设置,主要包括对象的运行时数据(如Hash码、分代年龄、锁状态标志、锁指针、偏向线程ID、偏向时间戳等等)以及类型指针(JVM通过该类型指针来确定该对象是哪个类的实例)。
- 属性的显示初始化也比较好理解,比如上述在定义Programmer类的时候,针对某个属性字段手动的赋值,如:“ private int age = 18“ , 就在这个时候初始化。
- 最后是调用类的构造方法,进行构造方法内描述的初始化动作。
应该说,经过了上述的一系列操作,一个新的对象才算是真正的诞生了。
2、反射出一个对象
通过Java的反射机制,只要能拿到Class对象,就可以通过强大的反射机制来创造出实例对象。
一般而言,拿到Class对象有三种方式:
- 类名.class
- 对象名.getClass()
- Class.forName(全限定类名)
有了 Class 对象之后,便可调用其 newInstance() 方法来创建一个对象,如:
Class pClass = Class.forName("com.zrt.pojo.Programmer");
Programmer programmer01 = (Programmer) pClass.newInstance();
Programmer programmer02 = Programmer.class.newInstance();
与此同时,这种方法的局限性也是很明显的,因为使用的是Programmer类的无参构造来创建对象的。
所以比上述的方法更进一步的方式是通过java.lang.relect.Constructor这个类的newInstance()方法来创建对象,因为它可以明确地指定通过某个构造器来创建对象。比如,当我们拿到了类的Class对象后,就可以通过**getDeclaredConstructor()**函数来获取到类的所有构造函数,这样我们便可指定其中一个来创建对象,就像这样:
Constructor[] constructors = Programmer.class.getConstructors();
Programmer programmer03 = (Programmer) constructors[0].newInstance();
Programmer programmer04 = (Programmer) constructors[1].newInstance(1, "zhangsan");
而且,如果我们想明确获取类的某个构造函数,便可以调用getDeclaredConstrutor(Class<?> … parameterTypes)函数里直接指定构造函数传参类型来准确控制,如:
Constructor constructor = Programmer.class.getConstructor(int.class, String.class);
Programmer programmer05 = (Programmer) constructor.newInstance();
3、对象克隆
Java中还支持对象的克隆操作,顾名思义,就是通过对象占有的堆内存空间的保存内容实现数据的赋值,利用已有对象克隆出一个成员属性内容完全相同的实例化对象。
对象的克隆可以使用Object类提供的clone()方法,此方法定义如下:
protected Object clone() throws CloneNotSupportedException
注意的是,进行克隆的对象必须实现一个Cloneable接口(此接口只是一个标识接口,只表示一种能力,接口中并没有任何方法),否则clone()方法会抛出一个CloneNotSupportedException(不支持克隆异常)。
Object类提供的clone()方法在类中使用protected设置了访问权限,所以该方法只能通过Object类的子类来进行调用。
public class CloneTestDemo {
public static void main(String[] args) throws CloneNotSupportedException {
// 实例化 User 对象
User user01 = new User("张三",18);
User user02 = (User)user01.clone();
user02.setName("李四");
user02.setAge(30);
System.out.println(user01);
System.out.println(user02);
}
}
// 实现 Cloneable 接口,该对象可以克隆
class User implements Cloneable {
private String name;
private int age;
public User() {
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "[" + super.toString() + "]" + "{" + "name = " + this.name + ", age = " + this.age + "}";
}
@Override
protected Object clone() throws CloneNotSupportedException {
// 调用父类 clone() 方法
return super.clone();
}
}
/*
输出结果:
[edu.zhku.test01.User@3f3afe78]{name = 张三, age = 18}
[edu.zhku.test01.User@47f6473]{name = 李四, age = 30}
*/
原来的对象克隆成功后,就会利用已有的对象构建一个全新的对象(此对象与原来的对象的成员属性内容完全相同,但是占有的堆内存空间不同),两个对象彼此之间的操作不会有任何的影响。
4、对象的隐式创建场景
除了上述几种显示地对象创建场景之外,还有一些我们并没有进行手动进行创造对象的隐式场景。
4.1、Class类实例隐式创建
我们都知道JVM虚拟机在加载一个类的时候,也会创建一个类对应的Class实例对象,很明显这一个过程是JVM虚拟机背着我们偷偷干的。
4.2、字符串隐式对象创建
典型的,比如定义一个String类型的字符变量,就可能引起一个新的String对象的创建,如:
String programmer = "Programmer";
再者,较常见的还比如String的**“+”号连接符也会隐式地导致新的String**对象的创建,如:
String str = str1 + str2;
4.3、自动装箱机制
这种例子较多,比如执行如下代码:
Integer programmerAge = 30;
会触发的自动装箱机制就会导致一个新的包装类型的对象在后台被隐式地创建出来。
4.4、函数可变参数
当我们使用可变参数语法int … nums来描述一个函数的形式参数时,如:
public double avg(int ... nums){
double sum = 0;
int length = nums.length;
for(int i = 0; i < length; i++){
sum += nums[i];
}
return sum / length;
}
avg(1, 2, 4);
avg(1, 2, 4, 8);
avg(1, 2, 4, 8, 16);
从表面来看,函数的调用处可以传入各种离散参数参与计算,背后可能会隐式地产生一个对应地数组对象进行计算。
5、反序列化出一个对象
当我们进行对象的序列化和反序列化的过程中,反序列化操作会返回序列化的实例化对象,也是相当于一种“隐式的”对象构造。
由于之前也发表过一篇有关序列化&反序列化的文章,此处便不再详细讲述。
注:此文章只是我在学习过程中做的个人笔记,若有错误,敬请指正。