首先查看下面一段代碼,我指出了問題代碼的所在,讀者先自己思考一下這段代碼會有什麼問題。
這是用clone方法完整拷貝一個二項堆(BinomialHeap)結構的代碼。二項堆中包含一個內部類BinomialHeapEntry,這個內部類的對象即二項堆中的每一個結點,除了包含結點對應的關鍵字外,還記錄父節點parent,下一個兄弟結點sibling和第一個孩子結點child三個指針。二項堆的根表通過每棵二項樹根節點的sibling指針鏈接。
cloneBinomialTree(BinomialHeapEntry root)方法遞歸拷貝一個根節點(含根節點)下的整個二項樹,clone方法中遍歷根表中每個樹根結點,拷貝對應的二項樹,然後將新的head指針賦給拷貝後的新堆。
public class BinomialHeap<E> implements Cloneable {
transient BinomialHeapEntry head; // 根表中的第一個元素
int size; // 整個二項堆的大小(結點數)
private class BinomialHeapEntry {
E element;
transient BinomialHeapEntry parent = null, child = null,
sibling = null;
transient int degree = 0;
private BinomialHeapEntry(E element) {
this.element = element;
}
// 獲得孩子結點的迭代器
private Iterable<BinomialHeapEntry> children {...}
}
@Override
public BinomialHeap<E> clone() {
// TODO Auto-generated method stub
BinomialHeap<E> clone;
try {
clone = (BinomialHeap<E>) super.clone();
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
throw new InternalError();
}
BinomialHeapEntry newHead = null, curRoot = null;
// 遍歷根表
Iterator<HeapEntry<E>> rootIt = this.roots().iterator();
while(rootIt.hasNext()){
BinomialHeapEntry root = (BinomialHeapEntry) rootIt.next();
// 拷貝根節點下的整棵二項樹<br> <span >// BUG引入的地方</span>
BinomialHeapEntry newRoot = cloneBinomialTree(root);
if(curRoot == null){
newHead = newRoot;
} else {
curRoot.sibling = newRoot;
}
curRoot = newRoot;
}
clone.head = newHead;
return clone;
}
// 拷貝根節點(含根節點)下的整棵子樹
private BinomialHeapEntry cloneBinomialTree(BinomialHeapEntry root){
BinomialHeapEntry newRoot = new BinomialHeapEntry(root.element());
BinomialHeapEntry curChild = null;
// 遍歷根節點的所有孩子結點
Iterator<HeapEntry<E>> childIt = root.children().iterator();
while(childIt.hasNext()){
BinomialHeapEntry child = (BinomialHeapEntry) childIt.next();
// 遞歸拷貝孩子節點下的子樹
BinomialHeapEntry newChild = cloneBinomialTree(child);
newChild.parent = newRoot;
if(curChild == null){
newRoot.child = newChild;
} else {
curChild.sibling = newChild;
}
curChild = newChild;
}
newRoot.degree = root.degree;
return newRoot;
}
}
先回顧一下Java內部類的一些知識,非靜態的Java內部類作為外部類的一個成員,只能通過外部類的對象來訪問,要創建一個內部類對象,也需要通過外部類對象來創建,即:outerObject.new InnerClass()。這時,所創建的內部類對象會產生名稱為this$0的一個字段,該字段保存的是創建這個內部類對象的外部類對象引用,即剛才的outerObject。這個引用使得一個內部類對象可以自由的訪問外部類的成員變量。
在回顧上面二項堆拷貝的代碼,在調用cloneBinomialTree方法拷貝二項樹時,我們試圖通過new BinomialHeapEntry()來創建一個新的結點,把新的結點作為新二項堆中的結點,但事實上我們實際調用的是this.new BinomialHeapEntry(),也就是說創建的新結點中,this$0是指向老的二項堆(this)而不是新的正在拷貝的二項堆(clone)。這使得我們得到拷貝的新二項堆之後,通過新二項堆的任一結點訪問和修改整個堆的信息時,修改的都是老的二項堆,最後產生了一系列奇怪的結果。
要想修復這個bug很簡單,把clone方法中的BinomialHeapEntry newRoot = cloneBinomialTree(root)即紅色注釋下的語句,修改為BinomialHeapEntry newRoot = clone.cloneBinomialTree(root)。這樣cloneBinomialTree方法中創建的結點都是通過clone對象創建,問題也就解決了。
問題其實比較簡單,這確實是以後在編程中值得注意的一點:拷貝內部類對象時考慮清楚拷貝後的對象this$0字段是否指向的是你希望指向的外部類對象。
Java 8 中 HashMap 的性能提升 http://www.linuxidc.com/Linux/2014-04/100868.htm
Java 8 的 Nashorn 引擎 http://www.linuxidc.com/Linux/2014-03/98880.htm
Java 8簡明教程 http://www.linuxidc.com/Linux/2014-03/98754.htm
Java通過java.io.FileInputStream讀取txt文本 http://www.linuxidc.com/Linux/2014-05/102047.htm