(软件构造博客)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,那么使用浅拷贝可能存在风险。
上一篇: 软件构造Lab2