记录Java Collections构造器踩坑
问题定义:Java Collections构造器克隆对象后后发现使用过程中未对当前对象进行修改,但是队列内部元素发生改变。
先上实验的问题代码
克隆队列的方法:
/**
* clone a queue.
* @param queue the target queue which should be cloned can't be null .
* @return the cloned queue.
*/
private synchronized ConcurrentLinkedQueue<Rung> cloneQueue(ConcurrentLinkedQueue<Rung> queue) {
assert queue != null;
return new ConcurrentLinkedQueue<Rung>(queue);
}
队列元素的定义(mutable)
/**
* mutable.
* Abstraction function:
*
* Representation invariant:
* the monkey should not be null.
* the rung should >=0 and <= info.RUNG.
* Safety from rep exposure:
* all the rep are safe from rep exposure(monkey is immutable)
* Thread safe argument:
* the class has no multi-thread method and all method have no accesses to multi-thread.
*/
public class Rung {
/**
* the monkey.
*/
private final Monkey monkey;
/**
* the rung.
*/
private int rung;
// 省略部分不重要的方法
/**
* the method of setting the rung.
* @param rung the rung should >=0 and <= info.RUNG.
*/
public void setRung(int rung) {
assert rung >= 0 && rung <= info.RUNG;
this.rung = rung;
}
}
仔细审查代码中的问题,猜测原因:队列元素mutable,而构造器对队列元素进行浅拷贝,实际效果可以理解为对该队列对象生成了一个新的内存别名,而内部元素仍然指向同一块内存。以ArrayList为例,查看JDK文档中的解释:
可以基本验证猜测是正确的,继续探索Java源码。
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// defend against c.toArray (incorrectly) not returning Object[]
// (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
又可以看出,执行的重点代码是Arrays.copyOf(elementData, size, Object[].class);这一句,继续查看copyOf的源码:
@HotSpotIntrinsicCandidate
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函数,查看其他资料可知该函数使用范围很广,Sun JDK版本中的ArrayList
和Vector
大量使用了System.arraycopy
来操作数据,特别是同一数组内元素的移动及不同数组之间元素的复制。
@HotSpotIntrinsicCandidate
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
从native标签可知System.arraycopy函数是一个本地方法,借鉴别人的JDK源码分析可知,该函数的底层实现有两个版本,一个是针对基本类型数据数组、另一个是针对对象的数组,针对对象的复制即为浅拷贝,复制了对象的引用,因此可以证明上述猜测的正确。附一张盗来的图:
解决方式
为队列中每个对象重写clone方法,实现对象的深拷贝,构造队列时先构造空队列,再遍历的将对象的深拷贝加入该队列。
感悟
首先是对mutable和immutable的理解更深了一些,在开发过程中对mutable的对象要时刻的考虑其内部变化所带来的问题,因此,如王先生在软件构造课堂上描述到的:尽量减少mutable类型的对象开发原则十分有必要。
其次是对源码解析的感悟,在分析自己代码的时候很容易就能发现是mutable引发的错误,也能很快的修改完成,但是之后的源码分析加深了我对JDK底层实现的理解,尤其是对System.arraycopy函数的理解,该本地函数使用范围很大,因此也有助于之后的源码分析。