JAVA对象拷贝分为两种方式,一种是引用拷贝,一种是对象拷贝
引用拷贝:和对象拷贝的不同之处在于,引用拷贝只会生成一个新的对象引用地址,但两个地址其最终指向的还是同一个对象;
对象拷贝:这种方式会重新生成一个新的对象,生成的新对象与原来的对象没有任何关联。
1、引用拷贝
引用拷贝也就是我们常用的对象赋值,这种方式不会生成新的对象,只会在原对象上增加了一个新的对象引用,两个引用指向的对象还是是同一个;
java 对象默认的赋值方式都是引用拷贝,比如说下面代码对象赋值的过程就是引用拷贝:
User user1 = new User();
User user2 = user1;
引用拷贝需要注意的地方
下面我们来看一下我们程序里面常见的例子:
(1)首先定义一个User类
public class User {
private int age;//年龄
private String name;//姓名
}
(2)测试代码:
public static void main(String[] args) {
//1、实例化一个user1对象,并对属性赋值
User user1=new User();
user1.setName("我是user1");
user1.setAge(18);
//2、把user1对象放到一个List里面
List<User> userList=new ArrayList<User>();
userList.add(user1);
//3、然后创建user2 对象并从List里面拿出user1对象赋值给user2
User user2= userList.get(0);
//4、给user2的属性值
user2.setAge(1);
user2.setName("我是user2");
//5、这个的时候我们再输出user1的对应属性值
System.out.println(user1);
System.out.println(user2);
}
最后我们看看打印结果:
举了一个比较绕的例子是想更好的提现出对象赋值的方式可能会给我们带来的问题;
结合引用拷贝的图解我想我们不难理解这个结果是怎么出现的,因为user1和user2其实指向的是同一个对象,所以当我们修改user2的属性时其实修改的也是user1这个对象。
程序中如果像这种在一个对象上多次赋值再并使用其实是很危险的,有时候调用的层次多了被传递的使用者修改了对象属性会造成业务逻辑上的错误(想想上面的例子,如果在user2修改属性值之后,还有业务代码要拿user1来进行业务操作的话,那么此时的user1属性值都已经被修改了,这样势必会产生业务上的错误),而这样的问题又比较难发现,并且这样也会造成代码的理解成本变高,可读性也会变差,所以开发中我们尽量避免对象多层传递赋值。
2、对象浅拷贝
浅拷贝与引用拷贝不同,被浅拷贝的对象是会重新生成一个新的对象,新的对象和原来的对象是没有任何关系的,但是如果对象中的某个属性是引用类型的话,那么该属性对应的对象是不会重新生成的,浅拷贝只会重新当前拷贝的对象,并不会重新生成其属性引用的对象。
实现浅拷贝
需要拷贝的对象实现Cloneable 接口,再调用对象的clone方法可以实现对象的浅拷贝。
(1)实现 Cloneable 接口 并重写clone 方法
Teacher 类
public class Teacher
{
//老师姓名
private String teacherName;
}
User类
public class User implements Cloneable{
//名字
private String name;
//老师
private Teacher teacher;
}
(2)测试
public static void main(String args[]) throws Exception {
//user1有一个teacher对象的属性
Teacher teacher=new Teacher();
teacher.setTeacherName("我是teacher一号");
User user1 = new User();
user1.setName("我是user一号");
user1.setTeacher(teacher);
//对user1进行浅拷贝,再重新赋值其属性
User user2 = (User)user1.clone();
user2.setName("我是user二号");
user2.getTeacher().setTeacherName("我是teacher二号");
//最后我们再打印user1的对象属性
System.out.println("user1 的name"+user1.getName());
System.out.println("user1 的techerName"+user1.getTeacher().getTeacherName());
}
(3)打印结果
从打印结果我们可以看出:
(1)user2修改了name之后并没有影响user1的name,这说明user2和user1对象是独立的。
(2)user2修改了teacher 对象属性之后user1的teacher对象属性也同时改变了,这说明对象的clone方法并不会把其对象中引用的其他对象进行拷贝,这也是我们俗称的浅拷贝。
为什么浅拷贝不会拷贝其引用的对象?
也许你也有个疑问,为什么clone的方式不能把其引用的对象也重新生成一份,那多省事情,我想应该有以下几个原因;
1、不给其他类强加意义
这个就好比,User类为了能进行浅拷贝就实现了Cloneable 接口,但是其引用对象Teacher没有实现Cloneable 也许说明他本身就不想被拷贝,如果在拷贝User的情况下,同时也把Teacher拷贝了,这不就等于干了一件没有遵循他人同意的事,干了之后人家还不知道,傻傻的以为没人可以通过clone来拷贝出另外一个Teacher。
2、不破坏其原来对象的代码逻辑
如果User引用的Teacher 是个单例模式的对象,那如果在User拷贝的时候同时也拷贝出了一个Teacher 那是不是就会破坏Teacher这个单例模式对象的逻辑初衷。
3、对象深拷贝
深拷贝相比浅拷贝的不同就是,深拷贝会把拷贝的对象和其属性引用的对象都重新生成新的对象。
如何实现深拷贝
(1)对象实现序列化接口
User类
public class User implements Serializable {
//名字
private String name;
//老师
private Teacher teacher;
}
Teacher 类
public class Teacher implements Serializable {
//老师姓名
private String teacherName;
}
(2)把对象进行序列化后再反序列化
public static void main(String args[]) throws Exception {
//user1有一个teacher对象的属性
Teacher teacher=new Teacher();
teacher.setTeacherName("我是teacher一号");
User user1 = new User();
user1.setName("我是user一号");
user1.setTeacher(teacher);
//序列化写入到流里
ByteOutputStream bots=new ByteOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bots);
oos.writeObject(user1);
//反序列化成user2对象
ObjectInputStream ois=new ObjectInputStream(new ByteArrayInputStream(bots.toByteArray()));
User user2 = (User) ois.readObject();
user2.setName("我是user二号");
user2.getTeacher().setTeacherName("我是teacher二号");
//最后我们再打印user1的对象属性
System.out.println("user1 的name"+user1.getName());
System.out.println("user1 的techerName"+user1.getTeacher().getTeacherName());
}
(3)最后看结果,进行深拷贝后,改变User2对象中的属性不会对原来User1对象中的属性有任何影响,这说明,User1和User2 不管是属性还是其引用对象都是重新生成互不关联的两个对象:
(4)最后找了一个深度拷贝工具类供大家使用
public abstract class BeanUtil {
public static <T> T cloneTo(T src) throws RuntimeException {
ByteArrayOutputStream memoryBuffer = new ByteArrayOutputStream();
ObjectOutputStream out = null;
ObjectInputStream in = null;
T dist = null;
try {
out = new ObjectOutputStream(memoryBuffer);
out.writeObject(src);
out.flush();
in = new ObjectInputStream(new ByteArrayInputStream(memoryBuffer.toByteArray()));
dist = (T) in.readObject();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (out != null) {
try {
out.close();
out = null;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (in != null) {
try {
in.close();
in = null;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
return dist;
}
}