Java中Map的深拷贝和浅拷贝

时间:2022-02-27 21:58:03

背景: 在项目中需要将维护的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