Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必不少人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到如今已經將近8年的時間,但隨着Java 6,7,8,甚至9的發佈,Java語言發生了深入的變化。
在這裏第一時間翻譯成中文版。供你們學習分享之用。java
Cloneable接口的目的是做爲一個mixin接口(條目 20),公佈這樣的類容許克隆。不幸的是,它沒有達到這個目的。它的主要缺點是缺乏clone方法,而Object的clone方法是受保護的。你不能,不借助反射(條目 65),僅僅由於它實現了Cloneable接口,就調用對象上的 clone 方法。即便是反射調用也可能失敗,由於不能保證對象具備可訪問的 clone方法。儘管存在許多缺陷,該機制在合理的範圍內使用,因此理解它是值得的。這個條目告訴你如何實現一個行爲良好的 clone方法,在適當的時候討論這個方法,並提出替代方案。數組
既然Cloneable接口不包含任何方法,那它用來作什麼? 它決定了Object的受保護的clone 方法實現的行爲:若是一個類實現了Cloneable接口,那麼Object的clone方法將返回該對象的逐個屬性(field-by-field)拷貝;不然會拋出CloneNotSupportedException
異常。這是一個很是反常的接口使用,而不該該被效仿。 一般狀況下,實現一個接口用來表示能夠爲客戶作什麼。但對於Cloneable接口,它會修改父類上受保護方法的行爲。安全
雖然規範並無說明,但在實踐中,實現Cloneable接口的類但願提供一個正常運行的公共 clone方法。爲了實現這一目標,該類及其全部父類必須遵循一個複雜的、不可執行的、稀疏的文檔協議。由此產生的機制是脆弱的、危險的和不受語言影響的(extralinguistic):它建立對象而不須要調用構造方法。性能優化
clone方法的通用規範很薄弱的。 如下內容是從 Object 規範中複製出來的:app
建立並返回此對象的副本。 「複製(copy)」的確切含義可能取決於對象的類。 通常意圖是,對於任何對象x,表達式x.clone() != x
返回 true,而且x.clone().getClass() == x.getClass()
也返回 true,但它們不是絕對的要求,但一般狀況下,x.clone().equals(x)
返回 true,固然這個要求也不是絕對的。ide
根據約定,這個方法返回的對象應該經過調用super.clone
方法得到的。 若是一個類和它的全部父類(Object除外)都遵照這個約定,狀況就是如此,x.clone().getClass() == x.getClass()
。性能
根據約定,返回的對象應該獨立於被克隆的對象。 爲了實現這種獨立性,在返回對象以前,可能須要修改由super.clone返回的對象的一個或多個屬性。學習
這種機制與構造方法鏈(chaining)很類似,只是它沒有被強制執行;若是一個類的clone方法返回一個經過調用構造方法得到而不是經過調用super.clone的實例,那麼編譯器不會抱怨,可是若是一個類的子類調用了super.clone,那麼返回的對象包含錯誤的類,從而阻止子類 clone 方法正常執行。若是一個類重寫的 clone 方法是有 final 修飾的,那麼這個約定能夠被安全地忽略,由於子類不須要擔憂。可是,若是一個final類有一個不調用super.clone的clone方法,那麼這個類沒有理由實現Cloneable接口,由於它不依賴於Object的clone實現的行爲。優化
假設你但願在一個類中實現Cloneable接口,它的父類提供了一個行爲良好的 clone方法。首先調用super.clone。 獲得的對象將是原始的徹底功能的複製品。 在你的類中聲明的任何屬性將具備與原始屬性相同的值。 若是每一個屬性包含原始值或對不可變對象的引用,則返回的對象可能正是你所須要的,在這種狀況下,不須要進一步的處理。 例如,對於條目 11中的PhoneNumber
類,狀況就是這樣,可是請注意,不可變類永遠不該該提供clone方法,由於這隻會浪費複製。 有了這個警告,如下是PhoneNumber
類的clone方法:ui
// Clone method for class with no references to mutable state @Override public PhoneNumber clone() { try { return (PhoneNumber) super.clone(); } catch (CloneNotSupportedException e) { throw new AssertionError(); // Can't happen } }
爲了使這個方法起做用,PhoneNumber
的類聲明必須被修改,以代表它實現了Cloneable接口。 雖然Object類的clone方法返回Object類,可是這個clone方法返回PhoneNumber
類。 這樣作是合法和可取的,由於Java支持協變返回類型。 換句話說,重寫方法的返回類型能夠是重寫方法的返回類型的子類。 這消除了在客戶端轉換的須要。 在返回以前,咱們必須將Object的super.clone的結果強制轉換爲PhoneNumber
,但保證強制轉換成功。
super.clone的調用包含在一個try-catch塊中。 這是由於Object聲明瞭它的clone方法來拋出CloneNotSupportedException
異常,這是一個檢查時異常。 因爲PhoneNumber
實現了Cloneable接口,因此咱們知道調用super.clone會成功。 這裏引用的須要代表CloneNotSupportedException
應該是未被檢查的(條目 71)。
若是對象包含引用可變對象的屬性,則前面顯示的簡單clone實現多是災難性的。 例如,考慮條目 7中的Stack類:
public class Stack { private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { this.elements = new Object[DEFAULT_INITIAL_CAPACITY]; } public void push(Object e) { ensureCapacity(); elements[size++] = e; } public Object pop() { if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; // Eliminate obsolete reference return result; } // Ensure space for at least one more element. private void ensureCapacity() { if (elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1); } }
假設你想讓這個類能夠克隆。 若是clone方法僅返回super.clone()調用的對象,那麼生成的Stack實例在其size 屬性中具備正確的值,但elements屬性引用與原始Stack實例相同的數組。 修改原始實例將破壞克隆中的不變量,反之亦然。 你會很快發現你的程序產生了無心義的結果,或者拋出NullPointerException
異常。
這種狀況永遠不會發生,由於調用Stack類中的惟一構造方法。 實際上,clone方法做爲另外一種構造方法; 必須確保它不會損壞原始對象,而且能夠在克隆上正確創建不變量。 爲了使Stack上的clone方法正常工做,它必須複製stack 對象的內部。 最簡單的方法是對元素數組遞歸調用clone方法:
// Clone method for class with references to mutable state @Override public Stack clone() { try { Stack result = (Stack) super.clone(); result.elements = elements.clone(); return result; } catch (CloneNotSupportedException e) { throw new AssertionError(); } }
請注意,咱們沒必要將elements.clone的結果轉換爲Object[]數組。 在數組上調用clone會返回一個數組,其運行時和編譯時類型與被克隆的數組相同。 這是複製數組的首選習語。 事實上,數組是clone 機制的惟一有力的用途。
還要注意,若是elements屬性是final的,則之前的解決方案將不起做用,由於克隆將被禁止向該屬性分配新的值。 這是一個基本的問題:像序列化同樣,Cloneable體系結構與引用可變對象的final 屬性的正常使用不兼容,除非可變對象能夠在對象和其克隆之間安全地共享。 爲了使一個類能夠克隆,可能須要從一些屬性中移除 final修飾符。
僅僅遞歸地調用clone方法並不老是足夠的。 例如,假設您正在爲哈希表編寫一個clone方法,其內部包含一個哈希桶數組,每一個哈希桶都指向「鍵-值」對鏈表的第一項。 爲了提升性能,該類實現了本身的輕量級單鏈表,而沒有使用java內部提供的java.util.LinkedList:
public class HashTable implements Cloneable { private Entry[] buckets = ...; 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; } } ... // Remainder omitted }
假設你只是遞歸地克隆哈希桶數組,就像咱們爲Stack所作的那樣:
// Broken clone method - results in shared mutable state! @Override public HashTable clone() { try { HashTable result = (HashTable) super.clone(); result.buckets = buckets.clone(); return result; } catch (CloneNotSupportedException e) { throw new AssertionError(); } }
雖然被克隆的對象有本身的哈希桶數組,可是這個數組引用與原始數組相同的鏈表,這很容易致使克隆對象和原始對象中的不肯定性行爲。 要解決這個問題,你必須複製包含每一個桶的鏈表。 下面是一種常見的方法:
// Recursive clone method for class with complex mutable state public class HashTable implements Cloneable { private Entry[] buckets = ...; 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; } // Recursively copy the linked list headed by this Entry Entry deepCopy() { return new Entry(key, value, next == null ? null : next.deepCopy()); } } @Override public HashTable clone() { try { HashTable result = (HashTable) super.clone(); result.buckets = new Entry[buckets.length]; for (int i = 0; i < buckets.length; i++) if (buckets[i] != null) result.buckets[i] = buckets[i].deepCopy(); return result; } catch (CloneNotSupportedException e) { throw new AssertionError(); } } ... // Remainder omitted }
私有類HashTable.Entry已被擴充以支持「深度複製」方法。 HashTable上的clone方法分配一個合適大小的新哈希桶數組,迭代原來哈希桶數組,深度複製每一個非空的哈希桶。 Entry上的deepCopy方法遞歸地調用它本身以複製由頭節點開始的整個鏈表。 若是哈希桶不是太長,這種技術很聰明而且工做正常。可是,克隆鏈表不是一個好方法,由於它爲列表中的每一個元素消耗一個棧幀(stack frame)。 若是列表很長,這很容易致使堆棧溢出。 爲了防止這種狀況發生,能夠用迭代來替換deepCopy中的遞歸:
// Iteratively copy the linked list headed by this Entry Entry deepCopy() { Entry result = new Entry(key, value, next); for (Entry p = result; p.next != null; p = p.next) p.next = new Entry(p.next.key, p.next.value, p.next.next); return result; }
克隆複雜可變對象的最後一種方法是調用super.clone,將結果對象中的全部屬性設置爲其初始狀態,而後調用更高級別的方法來從新生成原始對象的狀態。 以HashTable爲例,bucket屬性將被初始化爲一個新的bucket數組,而且 put(key, value)方法(未示出)被調用用於被克隆的哈希表中的鍵值映射。 這種方法一般產生一個簡單,合理的優雅clone方法,其運行速度不如直接操縱克隆內部的方法快。 雖然這種方法是乾淨的,但它與整個Cloneable體系結構是對立的,由於它會盲目地重寫構成體系結構基礎的逐個屬性對象複製。
與構造方法同樣,clone 方法絕對不能夠在構建過程當中,調用一個能夠重寫的方法(條目 19)。若是 clone 方法調用一個在子類中重寫的方法,則在子類有機會在克隆中修復它的狀態以前執行該方法,極可能致使克隆和原始對象的損壞。所以,咱們在前面討論的 put(key, value)方法應該時 final 或 private 修飾的。(若是時 private 修飾,那麼大概是一個非 final 公共方法的輔助方法)。
Object 類的 clone方法被聲明爲拋出CloneNotSupportedException異常,但重寫方法時不須要。 公共clone方法應該省略throws子句,由於不拋出檢查時異常的方法更容易使用(條目 71)。
在爲繼承設計一個類時(條目 19),一般有兩種選擇,但不管選擇哪種,都不該該實現 Clonable 接口。你能夠選擇經過實現正確運行的受保護的 clone方法來模仿Object的行爲,該方法聲明爲拋出CloneNotSupportedException異常。 這給了子類實現Cloneable接口的自由,就像直接繼承Object同樣。 或者,能夠選擇不實現工做的 clone方法,並經過提供如下簡併clone實現來阻止子類實現它:
// clone method for extendable class not supporting Cloneable @Override protected final Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); }
還有一個值得注意的細節。 若是你編寫一個實現了Cloneable的線程安全的類,記得它的clone方法必須和其餘方法同樣(條目 78)須要正確的同步。 Object 類的clone方法是不一樣步的,因此即便它的實現是使人滿意的,也可能須要編寫一個返回super.clone()的同步clone方法。
回顧一下,實現Cloneable的全部類應該重寫公共clone方法,而這個方法的返回類型是類自己。 這個方法應該首先調用super.clone,而後修復任何須要修復的屬性。 一般,這意味着複製任何包含內部「深層結構」的可變對象,並用指向新對象的引用來代替原來指向這些對象的引用。雖然這些內部拷貝一般能夠經過遞歸調用clone來實現,但這並不老是最好的方法。 若是類只包含基本類型或對不可變對象的引用,那麼極可能是沒有屬性須要修復的狀況。 這個規則也有例外。 例如,表示序列號或其餘惟一ID的屬性即便是基本類型的或不可變的,也須要被修正。
這麼複雜是否真的有必要?不多。 若是你繼承一個已經實現了Cloneable接口的類,你別無選擇,只能實現一個行爲良好的clone方法。 不然,一般你最好提供另外一種對象複製方法。 對象複製更好的方法是提供一個複製構造方法或複製工廠。 複製構造方法接受參數,其類型爲包含此構造方法的類,例如,
// Copy constructor public Yum(Yum yum) { ... };
複製工廠相似於複製構造方法的靜態工廠:
// Copy factory public static Yum newInstance(Yum yum) { ... };
複製構造方法及其靜態工廠變體與Cloneable/clone相比有許多優勢:它們不依賴風險很大的語言外的對象建立機制;不要求遵照那些不太明確的慣例;不會與final 屬性的正確使用相沖突; 不會拋出沒必要要的檢查異常; 並且不須要類型轉換。
此外,複製構造方法或複製工廠能夠接受類型爲該類實現的接口的參數。 例如,按照慣例,全部通用集合實現都提供了一個構造方法,其參數的類型爲Collection或Map。 基於接口的複製構造方法和複製工廠(更適當地稱爲轉換構造方法和轉換工廠)容許客戶端選擇複製的實現類型,而不是強制客戶端接受原始實現類型。 例如,假設你有一個HashSet,而且你想把它複製爲一個TreeSet。 clone方法不能提供這種功能,但使用轉換構造方法很容易:new TreeSet<>(s)
。
考慮到與Cloneable接口相關的全部問題,新的接口不該該繼承它,新的可擴展類不該該實現它。 雖然實現Cloneable接口對於final類沒有什麼危害,但應該將其視爲性能優化的角度,僅在極少數狀況下才是合理的(條目67)。 一般,複製功能最好由構造方法或工廠提供。 這個規則的一個明顯的例外是數組,它最好用 clone方法複製。