0. 引子
如何复制一个类? 简单来说我们有一个Class:
public class CopyClass{
int x;
int y;
public CopyClass(){
x = 0;
y = 0;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
}
简单来想,我们可以通过这样的操作来进行复制。
CopyClass copyClass1 = new CopyClass();
CopyClass copyClass2 = copyClass1;
但是,当我们继续执行以下代码时:
copyClass2.setX(5);
System.out.println("Class 1's X value is: " + copyClass1.getX());
System.out.println("Class 2's X value is: " + copyClass2.getX());
会得到以下结果:
Class 1's X value is: 5
Class 2's X value is: 5
为什么?
实际上,我们只是把copyClass1所指向的在堆(Heap)中的地址赋给了copyClass2. 因此导致了改变了copyClass2.X的值后copyClass1.X的值也被改变。
1. 拷贝原型
为了在Heap中重新创建一片新的地址,我们有如下方案可以选择:
1. 直接用Setter和Getter复制
CopyClass copyClass1 = new CopyClass();
CopyClass copyClass2 = new CopyClass();
copyClass2.setX(copyClass1.getX());
copyClass2.setY(copyClass1.getY());
-
或者在CopyClass中新建一个新的构造函数
public CopyClass(int x, int y){
this.x = x;
this.y = y;
}
再执行以下代码:
CopyClass copyClass1 = new CopyClass();
CopyClass copyClass2 = new CopyClass(copyClass1.getX(),copyClass1.getY());
能达到效果,但是太蠢。如果一个类有10个变量,那可读性就非常差了,而且传参时还容易出错。
2. 浅拷贝(Shallow Copy)
实际上,我们有如下方案可供选择:
- 拷贝构造函数(Copy Constructor)
- clone()方法
先说拷贝构造函数。
public CopyClass(CopyClass cc){
this.x = cc.x;
this.y = cc.y;
}
调用
CopyClass copyClass1 = new CopyClass();
CopyClass copyClass2 = new CopyClass(copyClass1);
执行测试代码:
copyClass2.setX(5);
System.out.println("Class 1's X value is: " + copyClass1.getX());
System.out.println("Class 2's X value is: " + copyClass2.getX());
得到结果:
Class 1's X value is: 0
Class 2's X value is: 5
可以看到此时拷贝正确。
再来说clone()方法。clone()是java.lang.Object中一个默认实现的protected方法。且,一般来说,以下三个表达式的返回值均为True.
x.clone() != x;//恒为真
x.clone().getClass() == x.getClass();//为真,如果x.clone()是调用Object的clone()来实现的话
x.clone().equals(x);//取决于equals()方法的实现
其源代码如下。
protected native Object clone() throws CloneNotSupportedException;
可以看到,这是一个JNI的方法。
打开OpenJDK的Object.c:
static JNINativeMethod methods[] = {
{"clone", "()Ljava/lang/Object;", (void *)&JVM_Clone},
};
可以看到,clone()方法被映射为了JVM_Clone.返回类型是Object.
继续深究,找到jvm.cpp中对于JVM_Clone方法的实现。
JVM_ENTRY(jobject, JVM_Clone(JNIEnv* env, jobject handle))
JVMWrapper("JVM_Clone");
Handle obj(THREAD, JNIHandles::resolve_non_null(handle));
const KlassHandle klass (THREAD, obj->klass());
JvmtiVMObjectAllocEventCollector oam;
#ifdef ASSERT
// Just checking that the cloneable flag is set correct
if (obj->is_array()) {
guarantee(klass->is_cloneable(), "all arrays are cloneable");
} else {
guarantee(obj->is_instance(), "should be instanceOop");
bool cloneable = klass->is_subtype_of(SystemDictionary::Cloneable_klass());
guarantee(cloneable == klass->is_cloneable(), "incorrect cloneable flag");
}
#endif
// Check if class of obj supports the Cloneable interface.
// All arrays are considered to be cloneable (See JLS 20.1.5)
if (!klass->is_cloneable()) {
ResourceMark rm(THREAD);
THROW_MSG_0(vmSymbols::java_lang_CloneNotSupportedException(), klass->external_name());
}
// Make shallow object copy
const int size = obj->size();
oop new_obj_oop = NULL;
if (obj->is_array()) {
const int length = ((arrayOop)obj())->length();
new_obj_oop = CollectedHeap::array_allocate(klass, size, length, CHECK_NULL);
} else {
new_obj_oop = CollectedHeap::obj_allocate(klass, size, CHECK_NULL);
}
// 4839641 (4840070): We must do an oop-atomic copy, because if another thread
// is modifying a reference field in the clonee, a non-oop-atomic copy might
// be suspended in the middle of copying the pointer and end up with parts
// of two different pointers in the field. Subsequent dereferences will crash.
// 4846409: an oop-copy of objects with long or double fields or arrays of same
// won't copy the longs/doubles atomically in 32-bit vm's, so we copy jlongs instead
// of oops. We know objects are aligned on a minimum of an jlong boundary.
// The same is true of StubRoutines::object_copy and the various oop_copy
// variants, and of the code generated by the inline_native_clone intrinsic.
assert(MinObjAlignmentInBytes >= BytesPerLong, "objects misaligned");
Copy::conjoint_jlongs_atomic((jlong*)obj(), (jlong*)new_obj_oop,
(size_t)align_object_size(size) / HeapWordsPerLong);
// Clear the header
new_obj_oop->init_mark();
// Store check (mark entire object and let gc sort it out)
BarrierSet* bs = Universe::heap()->barrier_set();
assert(bs->has_write_region_opt(), "Barrier set does not have write_region");
bs->write_region(MemRegion((HeapWord*)new_obj_oop, size));
Handle new_obj(THREAD, new_obj_oop);
// Special handling for MemberNames. Since they contain Method* metadata, they
// must be registered so that RedefineClasses can fix metadata contained in them.
if (java_lang_invoke_MemberName::is_instance(new_obj()) &&
java_lang_invoke_MemberName::is_method(new_obj())) {
Method* method = (Method*)java_lang_invoke_MemberName::vmtarget(new_obj());
// MemberName may be unresolved, so doesn't need registration until resolved.
if (method != NULL) {
methodHandle m(THREAD, method);
// This can safepoint and redefine method, so need both new_obj and method
// in a handle, for two different reasons. new_obj can move, method can be
// deleted if nothing is using it on the stack.
m->method_holder()->add_member_name(new_obj(), false);
}
}
// Caution: this involves a java upcall, so the clone should be
// "gc-robust" by this stage.
if (klass->has_finalizer()) {
assert(obj->is_instance(), "should be instanceOop");
new_obj_oop = InstanceKlass::register_finalizer(instanceOop(new_obj()), CHECK_NULL);
new_obj = Handle(THREAD, new_obj_oop);
}
return JNIHandles::make_local(env, new_obj());
JVM_END
首先检查是不是Cloneable,所有的arrays被假定为是Cloneable.如果不是,抛出“CloneNotSupportedException” 。然后对该传入的对象进行浅拷贝,让GC整理后返回。
clone()方法的原理横跨了java和c++,看起来很复杂。但是源代码注释中提到了一件事情:浅拷贝(shallow copy)。我们暂且把这个问题留到3. 深拷贝的引子中叙述。
通过读源代码可以发现,要使用clone()方法必须implements Conleable. 与此同时,由于clone()是一个protected方法,我们不能直接调用。需要在具体的使用类中写一个方法,调用Object的clone()才能实现。
现在CopyClass这个类变成了这样:
public class CopyClass implements Cloneable{
int x;
int y;
public CopyClass(){
x = 0;
y = 0;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public CopyClass(int x, int y){
this.x = x;
this.y = y;
}
public CopyClass(CopyClass cc){
this.x = cc.x;
this.y = cc.y;
}
@Override
public Object clone() {
try {
return super.clone();
}
catch (CloneNotSupportedException e){
return new CopyClass(this);
}
}
}
下面执行测试工作。
CopyClass copyClass1 = new CopyClass();
CopyClass copyClass2 = (CopyClass) copyClass1.clone();
copyClass2.setX(5);
System.out.println("Class 1's X value is: " + copyClass1.getX());
System.out.println("Class 2's X value is: " + copyClass2.getX());
得到结果:
Class 1's X value is: 0
Class 2's X value is: 5
可以看到,拷贝构造函数和clone()方法均得到了正确的拷贝结果。
3. 深拷贝(Deep Copy)
回到刚才的问题,什么是浅拷贝?深拷贝是与浅拷贝相对应的概念。
假设,我们再次修改CopyClass这个类。添加一个名为z的ZCoordinate的类变量。
public class CopyClass implements Cloneable{
int x;
int y;
ZCoordinate z;
public CopyClass(){
x = 0;
y = 0;
z = new ZCoordinate();
}
public ZCoordinate getZ() {
return z;
}
public void setZ(ZCoordinate z) {
this.z = z;
}
//此处省去其余setter和getter
}
ZCoordinate类的构造
public class ZCoordinate {
int z;
public int getZ() {
return z;
}
public void setZ(int z) {
this.z = z;
}
public ZCoordinate(){
z = 0;
}
}
执行测试。
CopyClass copyClass1 = new CopyClass();
CopyClass copyClass2 = (CopyClass) copyClass1.clone();
copyClass2.getZ().setZ(5);
System.out.println("Class 1's Z value is: " + copyClass1.getZ().getZ());
System.out.println("Class 2's Z value is: " + copyClass2.getZ().getZ());
得到结果:
Class 1's Z value is: 5
Class 2's Z value is: 5
发现引子中的问题重现了。为什么? 因为如同之前clone()的源代码所说,clone()是一个浅拷贝。clone()方法只拷贝了z的引用地址,而不是z的所有变量。
解决办法?Zcoordinate实现Copy Constructor,然后在CopyClass中的Copy Constructor调用Zcoordinate的Copy Constructor.
public ZCoordinate(ZCoordinate z){
this.z = z.z;
}
public CopyClass(CopyClass cc){
this.x = cc.x;
this.y = cc.y;
this.z = new ZCoordinate(cc.z);
}
测试:
CopyClass copyClass1 = new CopyClass();
CopyClass copyClass2 = new CopyClass(copyClass1);
copyClass2.getZ().setZ(5);
System.out.println("Class 1's Z value is: " + copyClass1.getZ().getZ());
System.out.println("Class 2's Z value is: " + copyClass2.getZ().getZ());
或者Zcoordinate实现clone()方法,然后修改CopyClass的clone()方法:
public Object clone() {
try {
Object result = super.clone();
((CopyClass)result).z = (ZCoordinate) z.clone();
return result;
}
catch (CloneNotSupportedException e){
return new CopyClass(this);
}
}
测试:
CopyClass copyClass1 = new CopyClass();
CopyClass copyClass2 = (CopyClass) copyClass1.clone();
copyClass2.getZ().setZ(5);
System.out.println("Class 1's Z value is: " + copyClass1.getZ().getZ());
System.out.println("Class 2's Z value is: " + copyClass2.getZ().getZ());
结果均为:
Class 1's Z value is: 0
Class 2's Z value is: 5
可以看到,这样做很繁琐,如果一个数据类内部嵌套了许多层其他数据类结构,那么这种“深度拷贝”实现起来就是个灾难。
下一篇准备写写如何用其他方法快速实现深度拷贝。
4. 性能测试
测试对比Copy Constructor与clone()在一层深度拷贝下的效率。考虑以下测试用例:
public static void main(String[] args){
final int SIZE = 1000000;
final int ITERATE_TEIMES = 50;
int totalOfCopyConstructor = 0;
int totalOfClone = 0;
long current;
for (int i = 0; i < ITERATE_TEIMES; i++){
current = System.currentTimeMillis();
copyConstructor(SIZE);
totalOfCopyConstructor += System.currentTimeMillis() - current;
current = System.currentTimeMillis();
clone(SIZE);
totalOfClone += System.currentTimeMillis() - current;
}
System.out.println("Average Copy Constructor run time is : " + (totalOfCopyConstructor / ITERATE_TEIMES));
System.out.println("Average clone run time is : " + (totalOfClone / ITERATE_TEIMES));
}
private static void copyConstructor(int size){
for (int i = 0; i < size;i++){
CopyClass copyClass1 = new CopyClass();
CopyClass copyClass2 = new CopyClass(copyClass1);
}
}
private static void clone(int size){
for (int i = 0; i < size;i++){
CopyClass copyClass1 = new CopyClass();
CopyClass copyClass2 = (CopyClass) copyClass1.clone();
}
}
分别执行50次1百万次级的深度拷贝。取平均时间。可以得出下表:
次数 | CopyConstructor | clone |
---|---|---|
1 | 6ms | 16ms |
2 | 6ms | 18ms |
3 | 7ms | 21ms |
嗯,结论是没事少用clone()…