標籤:
之所以寫這篇文章,源自於組內的一些技術討論。實際上,Effective Java的Item 37已經詳細地討論了Marker Interface。但是從整個Item的角度來看,其對於Marker Interface所提供的一系列優點及特殊特性實際上是持肯定態度的。因此很多人,包括我的同事,都將該條目中的一些結論當作是準則來去執行,卻忽略了得到這些結論時的前提,進而導致了一定程度的誤用。
當然,我並不是在反對Effective Java的Item 37。說實話,我也沒有這個資本。只是我個人在技術上略顯保守,因此希望通過這篇文章闡述一下Marker Interface可能帶來的一系列問題,進而使大家更為謹慎而且準確地使用Marker Interface。
Marker Interface簡介
或許有些讀者並不瞭解什麼是Marker Interface。那麼首先讓我們來看看JDK中Set介面的實現:
1 public interface Set<E> extends Collection<E> {2 }
細心的讀者會發現,實際上Set較Collection沒有添加任何介面函數。那為什麼JDK還要為其定義一個額外的介面呢?
相信您很快就能答出來:“這是因為Set中所包含的資料中不會有重複的元素,而Collection介面作為集合類型介面的根介面,其沒有添加這種限制。”
是的。JDK提供一個額外的Set介面的確就是出於這個目的。而且這種不添加任何新成員的介面實際上就是Marker Interface。而且在JDK中,Marker Interface還不少。另一個非常著名的Marker Interface就是Clonable介面:
1 public interface Cloneable {2 }
只是這一次,Marker Interface所受到的禮遇並不相同:無論是在對Prototype模式的講解中還是在其它日常討論中,其都是作為反面教材來詮釋什麼是一個不良的設計。
硬幣的正反面
那Marker Interface到底是好還是不好呢?如果沒有分析,我們就不會知道為什麼Marker Interface在不同的情況下得到如此不同的評價,也更不會知道如何正確地使用Marker Interface。因此我們先不說結論,而是從介面Set及Clonable兩個截然不同的情況來分析Marker Interface表現出如此差異的原因。
正能量先行。我們先來分析Set這個Marker Interface表現良好的原因。當使用者看到Set這個介面的時候,他首先想到的就是它是一個集合,而且該集合具有不會存在重複元素這樣一個性質。在對該介面執行個體進行操作的時候,軟體開發人員可以直接通過調用Set介面所繼承過來的各個成員函數來操作它。這些介面所定義的操作需要由Set介面的實作類別來定義。因此Set的這種不存在重複元素的性質實際上是由介面的實作類別所保證的。如在添加一個元素的時候,我們不必擔心當前是否該元素是否已經在集合中存在了:
1 Set<Item> itemSet = …2 itemSet.add(item);
而對於其它類型的集合,如List,我們就需要檢查元素是否已經在集合中存在,否則其內部將存在著對該元素的重複引用:
1 List<Item> itemList = …2 if (!itemList.contains(item)) {3 itemList.add(item);4 }
反過來,另一個Marker Interface Clonable則是臭名昭著的。具體原因已經在Effective Java中的Item 17中已經講得很清楚了。實際上,建立該介面的思路和建立Set介面的思路原本是一致的:該介面用來標示實現了該介面的類型是可以被拷貝的。其中的一個問題在於,Object類型的clone()函數是受保護的。從而使得使用者代碼不能調用Clonable介面的clone()函數。這樣就要求使用者通過其它方法來實現Clonable介面所表示的語義。進而在代碼中產生了大量的如下代碼:
1 if (obj instanceof Clonable) {2 ……3 } else {4 ……5 }
這樣,如果一個執行個體實現了特定的介面,如Clonable,我們就對它進行特殊的處理。這正是Marker Interface被大量誤用的一種情況:通過判斷一個執行個體是否實現了特定Marker Interface來決定對其進行處理的邏輯。這種對Marker Interface進行使用的代碼實際上破壞了封裝性:Marker Interface執行個體無法通過成員函數等方法控制外部系統對執行個體的使用方式。反過來,實現了Marker Interface的類型到底是被如何處理的則是由使用者代碼決定的。而Marker Interface僅僅是建議使用者代碼對其進行操作。也就是說,Marker Interface擁有了它的使用者相關的資訊,因此其與當前系統中的使用者在邏輯上是相互耦合的,從而使得實現了Marker Interface的類型無法在其它系統中重用。
而這也就是Effective Java的Item 37所強調的:通過Marker Interface來定義一個類型。我們知道,在定義一個類型的時候,我們不僅僅需要指定表示該類型所需要的資料,更為重要的則是為該類型抽象出用於操作該類型的介面。這些介面規定了該類型的操作方式,從而隔離了該類型的內部實現和使用者代碼。如果我們需要在這些介面之外通過判斷是否是特定類型來執行特殊的處理,那麼也就表示該Marker Interface所定義的類型從語義上來講是並不合適的。
而且從上面對Set介面以及Clonable介面的比較中可以看出,如果就像Effective Java的Item 37一樣通過Marker Interface來定義類型,那麼對類型進行定義的方式主要分為兩種:從一個介面派生以使得Marker Interface擁有較父介面多出的特殊性質。而如果Marker Interface沒有一個父介面,那麼其應該是Object類所具有的一種特殊性質,並可以通過Object類所提供的各個組成來按該性質進行操作,就像Serializable介面那樣。
從一個介面派生來定義Marker Interface是比較常見的情況,但是也較容易出錯。一個比較經典的樣本仍然是基於長方形為正方形定義一個介面。假設一個系統中已經擁有了一個用來表示長方形的介面:
1 public interface Rectangle {2 void setWidth(double width);3 void setHeight(double height);4 double getArea();5 }
由於正方形是長方形的長和寬都相等的一種特殊情況,因此我們常常認為正方形是一種特殊的長方形。對於這種情況,軟體開發人員就可能決定通過從長方形介面派生來定義一個正方形:
1 public interface Square extends Rectangle {2 }
但是在使用過程中,他會彆扭得要死。原因就是因為實際上對長方形所定義的介面,如setWidth(),setHeight()等對於正方形而言完全沒有意義。正方形所需要的是能夠設定它的邊長。因此一個正確定義Marker Interface的前提就是原有介面中的各個成員對於Marker Interface所定義的概念仍然具有明確的意義。
OK,相信您在看到長方形和正方形這個樣本的時候首先想到的就是裡氏替換原則(Liskov Substitution Principle)。但請不要使用裡氏替換原則來判斷一個Marker Interface的定義是否合適。這是因為裡氏替換原則實際上是使用在對象之間的:如果S是T的子類型,那麼S對象就應該能在不改變任何抽象屬性的情況下替換所有的T對象。畢竟,無論如何我們建立的都應該是一個類型的執行個體,而不能直接建立介面的執行個體(基於匿名類的除外)。
例如對於Set介面,如果我們將所有對Collection介面的使用都替換為對Set介面的使用,那麼至少對下面的語句進行替換時會導致編譯器報出編譯錯誤:
1 Collection<Item> itemCollection = new ArrayList<Item>();
因此,使用裡氏替換原則來判斷一個Marker Interface是否合適實際上真沒有太多意義,這在stackoverflow上也有頗多討論。
Marker Interface vs. Annotation
在前面的章節中已經提到過,Marker Interface表示實現該介面的類型具有特殊的性質。也就是說,Marker Interface是該類型的一個特性,也即是該類型的一個中繼資料。而在Java中,另一個可以用來表示類型中繼資料的Java組成是標記。在處理相似問題的情況下,不同的類庫選擇了不同的解決方案。例如Java中的序列化支援實際上是通過Serializable這個Marker Interface來完成的:
1 public class Employee implements java.io.Serializable2 {3 public String name;4 public String address;5 public transient int SSN;6 public int number;7 }
而在JPA中,用來對持久化到資料庫這一功能的控制是通過標記來完成的:
1 @Entity 2 @Table(name = "employee") 3 public class Employee { 4 @Column(name = "name", unique = false, nullable = false, length = 40) 5 private String name; 6 7 @Column(name = "address", unique = false, nullable = false, length = 200) 8 private String address; 9 10 @Column(name = "number", unique = false, nullable = false)11 private int number;12 13 @Transient14 private float percentageProcessed;15 ......16 }
隨之而來的一個問題就是:我們應該在什麼情況下使用Marker Interface,又在什麼情況下使用標記呢?瞭解何時使用的前提就是瞭解兩者之間的優劣。由於兩者是完全不同的兩種文法結構,因此它們之間的區別就顯得非常明顯:
首先從Marker Interface說起。該方法較標記的好處則在於,通過instanceof就直接能探測一個執行個體是否是一個特定介面的執行個體,而標記則需要通過反射等方法來判斷特定執行個體上是否有特定的標記。除了這個原因之外,對一個執行個體是否實現了某個介面可以在編譯時間就可以進行檢查,而一個執行個體是否有某個標記則在運行時才能進行。在使用instanceof的時候,實際上我們是在探測某個執行個體是否是某個類型。因此對於Marker Interface來說,其首先需要有一定的實際意義。
標記較Marker Interface的好處則在於:其粒度更細。可以說,Marker Interface只能施行在類型上,而標記則可以施行在多種類型組成上,因此Marker Interface實際上是作為整體行為的一種考慮,而標記則更注重具體細節。一個定義良好的細粒度API可以提供更大的靈活性。而且相較於介面,標記的後續發展能力更強,畢竟在一個介面中添加一個成員函數是一個非常麻煩的事情。
其實Marker Interface以及標記之間擁有如此大的混淆的很大一部分原因則是兩者在功能上有重複,而且在Java演化過程中出現的時機並不相同,導致在一些地方仍然擁有Marker Interface的不正當使用。實際上,像Clonable這種值得商榷的Marker Interface在JDK中還有很多很多。之所以在JDK裡面會出現那麼多的Marker Interface,其中一個原因也是因為Java對標記的支援比較晚的緣故。
轉載請註明原文地址並標明轉載:http://www.cnblogs.com/loveis715/p/5094367.html
商業轉載請事先與我聯絡:[email protected]
謹慎使用Marker Interface