背景: 在项目中需要将维护的map进行复制进行一些操作,但是不希望对原始map产生影响,一开始直接使用=赋值给新的map,但是在调试的时候发现原map也发生了变化,才意识到自己犯了拷贝的错误,整理资料如下,便于后续复习。
将一个对象的引用复制给另外一个对象,一共有三种方式。第一种方式是直接赋值,第二种方式是浅拷贝,第三种是深拷贝。这三种概念实际上都是为了拷贝对象。
java赋值是复制对象引用,如果我们想要得到一个对象的副本,使用赋值操作是无法达到目的的:
@Test
public void testassign(){
Person p1=new Person();
p1.setAge(31);
p1.setName("Peter");
Person p2=p1;
System.out.println(p1==p2);//true
}
如果创建一个对象的新的副本,也就是说他们的初始状态完全一样,但以后可以改变各自的状态,而互不影响,就需要用到java中对象的复制,如原生的clone()方法。
如何进行对象克隆
Object对象有个clone()方法,实现了对象中各个属性的复制,但它的可见范围是protected的,所以实体类使用克隆的前提是:
① 实现Cloneable接口,这是一个标记接口,自身没有方法。
② 覆盖clone()方法,可见性提升为public。
@Data
public class Person implements Cloneable {
private String name;
private Integer age;
private Address address;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
@Test
public void testShallowCopy() throws Exception{
Person p1=new Person();
p1.setAge(31);
p1.setName("Peter");
Person p2=(Person) p1.clone();
System.out.println(p1==p2);//false
p2.setName("Jacky");
System.out.println("p1="+p1);//p1=Person [name=Peter, age=31]
System.out.println("p2="+p2);//p2=Person [name=Jacky, age=31]
}
该测试用例只有两个基本类型的成员,测试达到目的了。
事情貌似没有这么简单,为Person增加一个Address类的成员:
@Data
public class Address {
private String type;
private String value;
}
再来测试,问题来了。
@Test
public void testShallowCopy() throws Exception{
Address address=new Address();
address.setType("Home");
address.setValue("北京");
Person p1=new Person();
p1.setAge(31);
p1.setName("Peter");
p1.setAddress(address);
Person p2=(Person) p1.clone();
System.out.println(p1==p2);//false
p2.getAddress().setType("Office");
System.out.println("p1="+p1);
System.out.println("p2="+p2);
}
查看输出:
false
p1=Person(name=Peter, age=31, address=Address(type=Office, value=北京))
p2=Person(name=Peter, age=31, address=Address(type=Office, value=北京))
遇到了点麻烦,只修改了p2的地址类型,两个地址类型都变成了Office。
深拷贝和浅拷贝
前面实例中是浅拷贝和深拷贝的典型用例。
浅拷贝:被复制对象的所有值属性都含有与原来对象的相同,而所有的对象引用属性仍然指向原来的对象。
深拷贝:在浅拷贝的基础上,所有引用其他对象的变量也进行了clone,并指向被复制过的新对象。
也就是说,一个默认的clone()方法实现机制,仍然是赋值。
如果一个被复制的属性都是基本类型,那么只需要实现当前类的cloneable机制就可以了,此为浅拷贝。
如果被复制对象的属性包含其他实体类对象引用,那么这些实体类对象都需要实现cloneable接口并覆盖clone()方法。
@Data
public class Address implements Cloneable {
private String type;
private String value;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
重新跑测试:
false
p1=Person(name=Peter, age=31, address=Address(type=Home, value=北京))
p2=Person(name=Peter, age=31, address=Address(type=Office, value=北京))
clone方式深拷贝小结
① 如果有一个非原生成员,如自定义对象的成员,那么就需要:
该成员实现Cloneable接口并覆盖clone()方法,不要忘记提升为public可见。同时,修改被复制类的clone()方法,增加成员的克隆逻辑。
② 如果被复制对象不是直接继承Object,中间还有其它继承层次,每一层super类都需要实现Cloneable接口并覆盖clone()方法。
与对象成员不同,继承关系中的clone不需要被复制类的clone()做多余的工作。
一句话来说,如果实现完整的深拷贝,需要被复制对象的继承链、引用链上的每一个对象都实现克隆机制。
前面的实例还可以接受,如果有N个对象成员,有M层继承关系,就会很麻烦。
利用序列化实现深拷贝
clone机制不是强类型的限制,比如实现了Cloneable并没有强制继承链上的对象也实现;也没有强制要求覆盖clone()方法。因此编码过程中比较容易忽略其中一个环节,对于复杂的项目排查就是困难了。
要寻找可靠的,简单的方法,序列化就是一种途径。
被复制对象的继承链、引用链上的每一个对象都实现java.io.Serializable接口。这个比较简单,不需要实现任何方法,serialVersionID的要求不强制,对深拷贝来说没毛病。
实现自己的deepClone方法,将this写入流,再读出来。俗称:冷冻-解冻。
回到之前的map问题
可以使用putAll方法实现深拷贝与浅拷贝。
使用Map对象只能实现浅拷贝:
public class CopyMap {
public static void main(String[] args) {
Map<String,Integer> map = new HashMap<String,Integer>();
map.put( "key1", 1);
Map<String,Integer> mapFirst = new HashMap<String,Integer>();
mapFirst.putAll(map);
System. out.println(mapFirst);
map.put( "key2", 2);
System. out.println(mapFirst);
}
}
如何实现Map的深拷贝呢?
有一种方法,是使用序列化的方式来实现对象的深拷贝,但是前提是,对象必须是实现了Serializable接口才可以,Map本身没有实现 Serializable 这个接口,所以这种方式不能序列化Map,也就是不能深拷贝Map。但是HashMap是可以的,因为它实现了 Serializable。下面的方式,基于HashMap来讲,非Map的拷贝。
具体实现如下:
public class CloneUtils {
@SuppressWarnings("unchecked")
public static <T extends Serializable> T clone(T obj){
T clonedObj = null;
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
clonedObj = (T) ois.readObject();
ois.close();
}catch (Exception e){
e.printStackTrace();
}
return clonedObj;
}
}
如何使用呢,下面是个使用的例子,同时证明了Map的putAll方法并没有实现深拷贝,putAll仅对基本数据类型起到深拷贝的作用。
举例:
public static void main(String[] args) {
List<Integer> list = new ArrayList<Integer>();
list.add(100);
list.add(200);
HashMap<String,Object> map = new HashMap<String,Object>();
//放基本类型数据
map.put("basic", 100);
//放对象
map.put("list", list);
HashMap<String,Object> mapNew = new HashMap<String,Object>();
mapNew.putAll(map);
System.out.println("----数据展示-----");
System.out.println(map);
System.out.println(mapNew);
System.out.println("----更改基本类型数据-----");
map.put("basic", 200);
System.out.println(map);
System.out.println(mapNew);
System.out.println("----更改引用类型数据-----");
list.add(300);
System.out.println(map);
System.out.println(mapNew);
System.out.println("----使用序列化进行深拷贝-----");
mapNew = CloneUtils.clone(map);
list.add(400);
System.out.println(map);
System.out.println(mapNew);
}
输出结果如下:
----数据展示----
{basic=100, list=[100, 200]}
{basic=100, list=[100, 200]}
----更改基本类型数据----
{basic=200, list=[100, 200]}
{basic=100, list=[100, 200]}
----更改引用类型数据----
{basic=200, list=[100, 200, 300]}
{basic=100, list=[100, 200, 300]}
----使用序列化进行深拷贝----
{basic=200, list=[100, 200, 300, 400]}
{basic=200, list=[100, 200, 300]}
最上面的两条是原始数据,使用了putAll方法拷贝了一个新的mapNew对象,
中间两条,是修改map对象的基本数据类型的时候,并没有影响到mapNew对象。
但是看倒数第二组,更改引用数据类型的时候,发现mapNew的值也变化了,所以putAll并没有对map产生深拷贝。
最后面是使用序列化的方式,发现,更改引用类型的数据的时候,mapNew对象并没有发生变化,所以产生了深拷贝。
上述的工具类,可以实现对象的深拷贝,不仅限于HashMap,前提是实现了Serlizeable接口。
参考原文
1、 https://blog.csdn.net/u012611620/article/details/81698596
2、 https://www.cnblogs.com/cxxjohnson/p/6258742.html