一、基本介绍
1. 介绍
在 Java
工程中,我们经常涉及对对象的创建与赋值,今天就详细介绍一下其背后的机制。
在开始之前我们要了解一个对象在 Java
中是如何进行存储的?对于 int、 byte
等等几类基本数据类型,Java
专门在堆栈中创立了一片常量池,当声明此类数据时对于 Java
内部而言相同的值仅存了一份,它们所指向的内存地址是一致。
如下示例中的两个赋值语句在 Java
常量池中只存储一个数值 10
,然后将 a
与 b
的引用都指向这个值。若后续 a
的值发生变化,则会在常量池中再存入新的数值,将 a
的引用指向这个新数值。
int a = 10;
int b = 10;
a = 20;
这样做的一大好处即能够大大的节省内存空间,无需为相同的数据分配不同的内存空间。
2. 对象存储
我们都知道当需要创建一个对象时可以通过 new
关键字创建对象,但是与 Java
基本类型不同是其会在内存中为每个对象分配一个单独的空间。
其中需要特别注意的是 String
类型,因为其不像基本类型都有其对应的封装类如 int
对应的 Integer
。因此对于 String
类型而言,当通过 = 赋值创建时作用效果等同于 int
等基本类型,但通过 new String()
创建时则会为其分配专门的内存空间。
如下面的示例中使用两种不同方式两个 String
对象,两个对象的内容虽然都一致,但因为 a
是直接赋值的所以则会存在常量池中,而 b
是通过 new
关键字创建所以会为其分配单独的内存空间,二者底层指向的内存地址因此是不同的,所以使用 ==
进行引用比较时返回的结果将会是 false
。
String a = "test";
String b = new String("test");
// false
System.out.println(a == b);
二、对象引用
1. 介绍
在上面我们介绍了数据与对象的基本存储机制,下面再来了解一下对象引用赋值的相关原理。
在 Java
中通过 =
对对象进行复制时并不会创建一个新的对象,而是将新创建的对象的引用指向原对象的内存引用,如下示例中先创建了 user1
,再通过=
将 user1
赋值给 user2
。
User user1 = new User(1, "Alex");
User user2 = user1;
上述的代码在实际存储中效果如下,因此此时如果改变 user2
的值则 user1
会同步发生变化,在实际开发中要小心防止对象错误变更。
三、对象拷贝
1. 浅拷贝
通过上述的例子介绍了对象赋值存在的一些问题,但如果在开发中就是涉及到对象复制呢?根据上面的示例显然不能直接通过 = 直接赋值,有一个最简单的方式就是通过 new 重新创建一个对象。
如上述例子可以改造成如下:
User user1 = new User(1, "Alex");
User user2 = new User(1, "Alex");
但很显然这也有一个问题,但对象的属性过多时,这种方式将变得及其繁杂与低效。因此 Java
引入了 Cloneable
接口从而实现这一问题,被克隆对象只需实现 Cloneable
接口并重写 clone()
即可。
稍微改造一下 User
类为如下:
public class User implements Cloneable {
private int id;
private String name;
public User(int i, String name) {
this.id = id;
this.name = name
}
@Override
public User clone() {
try {
User user = (User) super.clone();
return user;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
这时候即可直接调用 clone()
方法接口实现对象拷贝:
User user1 = new User(1, "Alex");
User user2 = user1.clone();
System.out.println("user1: " + user1);
System.out.println("user2: " + user2);
此时 user1
与 user2
的内存存储示意图如下:
2. 深拷贝
所谓深拷贝为相对浅拷贝而言,当一个对象的属性仍为对象时,通过上述 clone()
方式只能实现非对象的子属性赋值,对于属性中的对象其仍会采用 =
的赋值方式。
假如此时有两个类 User
和 City
,其中 User
属性包含 City
类。
public class User implements Cloneable {
private int id;
private String name;
private City city;
//... 略去其它代码
}
class City {
String address;
//... 略去其它代码
}
首先创建 user1
再通过 clone()
拷贝给 user2
,但此时 user1
与 user2
的 City
对象其实指向的是同一个对象,若此时修改 user2
的 City
对象 user1
的也会同步发生变化。
User user1 = new User(1, "Alex", new City("Beijing"));
User user2 = user1.clone();
因此若包含多层对象嵌套拷贝时每一级的对象类都需要实现 Cloneable
接口并重写 clone()
,同时重写 clone()
方法是也有所区别,如下是修改后的 User
类和 City
类。
public class User implements Cloneable {
private int id;
private String name;
private City city;
@Override
public User clone() {
try {
User user = (User) super.clone();
// 子对象属性仍需要通过拷贝赋值
user.city = city.clone();
return user;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
class City implements Cloneable{
String address;
//... 略去其它代码
}
3. 序列化拷贝
在深拷贝的例子中可以看到,当多层对象嵌套时使用 clone()
十分繁杂,因此我们还可以采用其它方式实现对象拷贝,即序列化拷贝。
通过 ByteArrayOutputStream
与 ObjectOutputStream
等 IO
流将对象先进行序列化再实现转化拷贝,缺点显然易见这种方式将会产生额外的 IO 操作,因此需要根据实际情况进行选择。
public class User implements Serializable {
public static User myClone(User user){
User newUser = null;
try (
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
) {
out.writeObject(user);
try (
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
) {
newUser = (User) ois.readObject();
return newUser;
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}