之所以寫這篇文章,源自於組內的一些技術討論。實際上,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.Serializable 2 { 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 @Transient 14 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
商業轉載請事先與我聯系:silverfox715@sina.com