欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

(软件构造博客)List的拷贝

程序员文章站 2022-03-10 14:15:07
...

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,那么使用浅拷贝可能存在风险。