java中僅有的創建對象的兩種方式:①.使用new操作符創建對象;②.使用clone方法復制對象。由于clone方法將最終將調用JVM中的原生方法完成復制,所以一般使用clone方法復制對象要比新建一個對象然后逐一進行元素復制效率要高。
淺拷貝與深拷貝
在java中基本數據類型是按值傳遞的,而對象是按引用傳遞的。所以當調用對象的clone方法進行對象復制時將涉及深拷貝和淺拷貝的概念。
淺拷貝是指拷貝對象時僅僅拷貝對象本身(包括對象中的基本變量),而不拷貝對象包含的引用指向的對象。深拷貝不僅拷貝對象本身,而且拷貝對象包含的引用指向的所有對象。通過clone方法復制對象時,若不對clone()方法進行改寫,則調用此方法得到的對象為淺拷貝。
例如:淺拷貝
public class Stack implements Cloneable {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object o) {
ensureCapacity();
elements[size++] = o;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 【避免內存泄漏】
return result;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
// 實現clone方法,淺拷貝
@Override
protected Stack clone() throws CloneNotSupportedException {
return (Stack) super.clone();
}
}
深拷貝:
//深拷貝
@Override
protected Stack clone() throws CloneNotSupportedException {
Stack result = (Stack) super.clone();
result.elements = elements.clone(); //對elements元素進行拷貝(引用或基本數據類型)
return result;
}
其原理圖:
深拷貝與淺拷貝的原理
注意:
- 由于java5.0后引入了協變返回類型(covariant return type)實現(基于泛型),即覆蓋方法的返回類型可以是被覆蓋方法的返回類型的子類型,所以clone方法可以直接返回Stack類型,而不用返回Object類型,然后客戶端再強轉。
- 在數組上調用clone返回的數組,其編譯時類型與被克隆數組的類型相同。
- 若elements域是final的,深拷貝不能正常工作。因為clone架構與引用可變對象的final域的正常用法是不兼容的。
- 若elements數組中的元素是引用類型,則此方法僅僅是對引用的拷貝,元素指向的還是原來的對象
還應該注意,數組的clone,僅僅復制的是數組中的元素,即若數組中元素為引用類型,僅僅復制引用。若clone的對象中含有鏈表,則應單獨對鏈表進行循環復制。例如,一個內部包含一個散列桶數組的散列表,其數組中每個元素都指向一個獨立的鏈表。此時僅僅使用上面的方法就是不完全拷貝。
代碼:
public class HashTable implements Cloneable {
private static final int CAPACITY = 10;
//散列桶數組,數組中元素指向由Entry對象組成的鏈表(指向鏈表第一個Entry)
private Entry[] buckerts = new Entry[CAPACITY];
public void put(Object key, Object value) {
int index = key.hashCode() % CAPACITY;
Entry e = buckerts[index];
buckerts[index] = new Entry(key,value,e);
}
@Override
public HashTable clone() throws CloneNotSupportedException {
HashTable result = (HashTable)super.clone();
result.buckerts = buckerts.clone(); //僅僅復制了對鏈表的引用。
return result;
}
//輕量級單鏈表
private static class Entry {
final Object key;
Object value;
Entry next;
Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
}
}
原理圖:
不完全拷貝
雖然被克隆對象有自己的散列桶數組,但數組引用的鏈表與原對象是一樣的。數組的clone方法,僅僅拷貝了對鏈表的引用,而沒有復制鏈表中的元素。
改進代碼:
@Override
public HashTable clone() throws CloneNotSupportedException {
HashTable result = (HashTable)super.clone();
result.buckerts = buckerts.clone();
for(int i=0; i<buckerts.length; i++) {
result.buckerts[i] = buckerts[i].deepCopy();
}
return result;
}
//輕量級單鏈表
private static class Entry {
final Object key;
Object value;
Entry next;
Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
//遞歸實現鏈表復制
Entry deepCopy() {
return new Entry(key,value,next == null ? null : next.deepCopy());
}
}
在內部類Entry中的深度拷貝方法遞歸的調用自身,以完成鏈表的拷貝。雖然這種方法比較簡潔,但如果鏈表很長,有可能會導致棧溢出。可以使用迭代代替遞歸實現鏈表的復制。代碼如下:
//迭代實現鏈表復制
Entry deepCopy() {
Entry result = new Entry(key, value, next);
for(Entry e = result; e.next != null; e = e.next) {
e.next = new Entry(e.next.key, e.next.value, e.next.next);
}
return result;
}
實現clone方法的步驟:
- 首先調用父類的super.clone方法(父類必須實現clone方法),這個方法將最終調用Object的中native型的clone方法完成淺拷貝
- 對類中的引用類型進行單獨拷貝
- 檢查clone中是否有不完全拷貝(例如,鏈表),進行額外的復制
參考
- Effective java教材
- Java中的深拷貝(深復制)和淺拷貝(淺復制)
- 詳解Java中的clone方法 -- 原型模式