List的浅拷贝和深拷贝

List的拷贝

在写实验的时候发现List的常见的复制方式复制完后的结果居然不是和原List无关的,查阅资料之后记录这一情况。
首先展示一下我发现问题的一个简化示例:

public class Person {
    private String name;
    private int age;
    public Person(String name,int age)
    {
        this.name=name;
        this.age=age;
    }
    public void setAge(int age)
    {
        this.age=age;
    }
    @Override
    public String toString()
    {
        return this.name+this.age;
    }
    public static void main(String[]argv)
    {
        List<Person> a = new ArrayList<>();
        Person p1=new Person("john",20);
        Person p2=new Person("jack",30);
        a.add(p1);
        a.add(p2);
        List<Person> b = new ArrayList<>();
        for(int i=0;i<a.size();i++)
        {
            b.add(a.get(i));
        }
        System.out.println("刚复制完时:");
        System.out.println("a:"+a.toString());
        System.out.println("b:"+b.toString());
        b.get(1).setAge(40);
        System.out.println("修改b之后:");
        System.out.println("a:"+a.toString());
        System.out.println("b:"+b.toString());
    }

}

输出结果如下:

刚复制完时:
a:[john20, jack30]
b:[john20, jack30]
修改b之后:
a:[john20, jack40]
b:[john20, jack40]

可以发现,a居然跟随着b的变化一起变化了,这显然和我们的设计要求不一致,接下来就来看看List的复制的几种情况。

1.浅拷贝

我们上面展示的这种方法就是浅拷贝的一种,可以说是我日常复制List的时候最常用的。顾名思义,浅拷贝将原List和拷贝List中的元素指向同一个地址,要是刚好这个元素的类型是mutable的,那么就会出现上述情况,修改b结果把a也给修改了。以下是浅拷贝的几种不同的方式

1.1 遍历循环复制

也就是上述代码展示的了,不再赘述。

1.2 使用List实现类的构造方法

如下代码展示,其实和遍历复制本质相同,只是使用了构造方法。

List<Person> b = new ArrayList<>(a);

我们可以分析一下ArrayList的源码,就可以发现事实上它是利用了一个叫做copyOf的函数实现的构造函数的主要功能,构造函数源码如下:

  public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

我们可以继续分析copyOf函数的源码,发现调用了一个System.arraycopy的函数,copyOf源码如下:

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

继续看System.arraycopy的源码,这是一个naive函数,那就不继续分析了,关键在于这个函数实现的功能就是实现数组之间的复制,而且由调用这一方法的构造函数也是浅拷贝的一种。

1.3 list.addAll()

使用方法:

List<Person> a = new ArrayList<>();
        Person p1=new Person("john",20);
        Person p2=new Person("jack",30);
        a.add(p1);
        a.add(p2);
        List<Person> b = new ArrayList<>();
        b.addAll(a);

源码如下:

public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }

通过简单查看其源码发现和构造函数的复制函数方法没什么两样,不再赘述。

1.4 System.arraycopy()

不再赘述,和上面一样,调用方法参见addAll源码即可。

1.5 使用Stream的方式copy

使用方法:

List<Person> b = a.stream().collect(Collectors.toList());

2.List深拷贝

和浅拷贝不同,那么显然深拷贝就是a与b的元素指向不同的地址,因此a与b内容相同,但是修改的时候互不影响,这才是我们在大多数情况下比较符合我们要求的拷贝方法。以下两种方法参考博客:https://blog.csdn.net/qq_35507234/article/details/85070429

2.1 使用序列化方法
public static <T> List<T> deepCopy(List<T> src) throws IOException, ClassNotFoundException {  
    ByteArrayOutputStream byteOut = new ByteArrayOutputStream();  
    ObjectOutputStream out = new ObjectOutputStream(byteOut);  
    out.writeObject(src);  
 
    ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());  
    ObjectInputStream in = new ObjectInputStream(byteIn);  
    @SuppressWarnings("unchecked")  
    List<T> dest = (List<T>) in.readObject();  
    return dest;  
}  
 
List<Person> destList=deepCopy(srcList);  //调用该方法
2.2 clone方法
public class A implements Cloneable {   
    public String name[];   
 
    public A(){   
        name=new String[2];   
    }   
 
    public Object clone() {   
        A o = null;   
        try {   
            o = (A) super.clone();   
        } catch (CloneNotSupportedException e) {   
            e.printStackTrace();   
        }   
        return o;   
    }   
}  
for(int i=0;i<n;i+=){
copy.add((A)src.get(i).clone());
}

3. 使用场景

很显然,在没有特殊情况的时候使用浅拷贝绰绰有余,例如List中的元素如果是String,那么使用浅拷贝并不会存在a与b同时修改的情况,这是因为String是Immutable的,例如我们对b这个List中的某一个String进行修改,那么这个String会指向一段新的地址,而a的相同位置的元素指向原来的地址不变,因此不存在同步变化的情况,使用浅拷贝即可。
但是如果我们真的需要使用一个元素是mutable类型的List的话,而且这个List还有可能在多处被复制使用的话就需要考虑深拷贝了。例如文章最开始那个例子,如果List的元素是自定义的Person类,而且这是一个mutable的ADT,那么使用浅拷贝可能存在风险。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值