謹(jǐn)慎使用Marker Interface
之所以寫這篇文章,源自于組內(nèi)的一些技術(shù)討論。實(shí)際上,Effective Java的Item 37已經(jīng)詳細(xì)地討論了Marker Interface。但是從整個(gè)Item的角度來(lái)看,其對(duì)于Marker Interface所提供的一系列優(yōu)點(diǎn)及特殊特性實(shí)際上是持肯定態(tài)度的。因此很多人,包括我的同事,都將該條目中的一些結(jié)論當(dāng)作是準(zhǔn)則來(lái)去執(zhí)行,卻忽略了得到這些結(jié)論時(shí)的前提,進(jìn)而導(dǎo)致了一定程度的誤用。
當(dāng)然,我并不是在反對(duì)Effective Java的Item 37。說(shuō)實(shí)話,我也沒(méi)有這個(gè)資本。只是我個(gè)人在技術(shù)上略顯保守,因此希望通過(guò)這篇文章闡述一下Marker Interface可能帶來(lái)的一系列問(wèn)題,進(jìn)而使大家更為謹(jǐn)慎而且準(zhǔn)確地使用Marker Interface。
Marker Interface簡(jiǎn)介
或許有些讀者并不了解什么是Marker Interface。那么首先讓我們來(lái)看看JDK中Set接口的實(shí)現(xiàn):
1 public interface Set<E> extends Collection<E> { 2 }
細(xì)心的讀者會(huì)發(fā)現(xiàn),實(shí)際上Set較Collection沒(méi)有添加任何接口函數(shù)。那為什么JDK還要為其定義一個(gè)額外的接口呢?
相信您很快就能答出來(lái):“這是因?yàn)镾et中所包含的數(shù)據(jù)中不會(huì)有重復(fù)的元素,而Collection接口作為集合類型接口的根接口,其沒(méi)有添加這種限制。”
是的。JDK提供一個(gè)額外的Set接口的確就是出于這個(gè)目的。而且這種不添加任何新成員的接口實(shí)際上就是Marker Interface。而且在JDK中,Marker Interface還不少。另一個(gè)非常著名的Marker Interface就是Clonable接口:
1 public interface Cloneable { 2 }
只是這一次,Marker Interface所受到的禮遇并不相同:無(wú)論是在對(duì)Prototype模式的講解中還是在其它日常討論中,其都是作為反面教材來(lái)詮釋什么是一個(gè)不良的設(shè)計(jì)。
硬幣的正反面
那Marker Interface到底是好還是不好呢?如果沒(méi)有分析,我們就不會(huì)知道為什么Marker Interface在不同的情況下得到如此不同的評(píng)價(jià),也更不會(huì)知道如何正確地使用Marker Interface。因此我們先不說(shuō)結(jié)論,而是從接口Set及Clonable兩個(gè)截然不同的情況來(lái)分析Marker Interface表現(xiàn)出如此差異的原因。
正能量先行。我們先來(lái)分析Set這個(gè)Marker Interface表現(xiàn)良好的原因。當(dāng)用戶看到Set這個(gè)接口的時(shí)候,他首先想到的就是它是一個(gè)集合,而且該集合具有不會(huì)存在重復(fù)元素這樣一個(gè)性質(zhì)。在對(duì)該接口實(shí)例進(jìn)行操作的時(shí)候,軟件開(kāi)發(fā)人員可以直接通過(guò)調(diào)用Set接口所繼承過(guò)來(lái)的各個(gè)成員函數(shù)來(lái)操作它。這些接口所定義的操作需要由Set接口的實(shí)現(xiàn)類來(lái)定義。因此Set的這種不存在重復(fù)元素的性質(zhì)實(shí)際上是由接口的實(shí)現(xiàn)類所保證的。如在添加一個(gè)元素的時(shí)候,我們不必?fù)?dān)心當(dāng)前是否該元素是否已經(jīng)在集合中存在了:
1 Set<Item> itemSet = … 2 itemSet.add(item);
而對(duì)于其它類型的集合,如List,我們就需要檢查元素是否已經(jīng)在集合中存在,否則其內(nèi)部將存在著對(duì)該元素的重復(fù)引用:
1 List<Item> itemList = … 2 if (!itemList.contains(item)) { 3 itemList.add(item); 4 }
反過(guò)來(lái),另一個(gè)Marker Interface Clonable則是臭名昭著的。具體原因已經(jīng)在Effective Java中的Item 17中已經(jīng)講得很清楚了。實(shí)際上,創(chuàng)建該接口的思路和創(chuàng)建Set接口的思路原本是一致的:該接口用來(lái)標(biāo)示實(shí)現(xiàn)了該接口的類型是可以被拷貝的。其中的一個(gè)問(wèn)題在于,Object類型的clone()函數(shù)是受保護(hù)的。從而使得用戶代碼不能調(diào)用Clonable接口的clone()函數(shù)。這樣就要求用戶通過(guò)其它方法來(lái)實(shí)現(xiàn)Clonable接口所表示的語(yǔ)義。進(jìn)而在代碼中產(chǎn)生了大量的如下代碼:
1 if (obj instanceof Clonable) { 2 …… 3 } else { 4 …… 5 }
這樣,如果一個(gè)實(shí)例實(shí)現(xiàn)了特定的接口,如Clonable,我們就對(duì)它進(jìn)行特殊的處理。這正是Marker Interface被大量誤用的一種情況:通過(guò)判斷一個(gè)實(shí)例是否實(shí)現(xiàn)了特定Marker Interface來(lái)決定對(duì)其進(jìn)行處理的邏輯。這種對(duì)Marker Interface進(jìn)行使用的代碼實(shí)際上破壞了封裝性:Marker Interface實(shí)例無(wú)法通過(guò)成員函數(shù)等方法控制外部系統(tǒng)對(duì)實(shí)例的使用方式。反過(guò)來(lái),實(shí)現(xiàn)了Marker Interface的類型到底是被如何處理的則是由用戶代碼決定的。而Marker Interface僅僅是建議用戶代碼對(duì)其進(jìn)行操作。也就是說(shuō),Marker Interface擁有了它的使用者相關(guān)的信息,因此其與當(dāng)前系統(tǒng)中的使用者在邏輯上是相互耦合的,從而使得實(shí)現(xiàn)了Marker Interface的類型無(wú)法在其它系統(tǒng)中重用。
而這也就是Effective Java的Item 37所強(qiáng)調(diào)的:通過(guò)Marker Interface來(lái)定義一個(gè)類型。我們知道,在定義一個(gè)類型的時(shí)候,我們不僅僅需要指定表示該類型所需要的數(shù)據(jù),更為重要的則是為該類型抽象出用于操作該類型的接口。這些接口規(guī)定了該類型的操作方式,從而隔離了該類型的內(nèi)部實(shí)現(xiàn)和用戶代碼。如果我們需要在這些接口之外通過(guò)判斷是否是特定類型來(lái)執(zhí)行特殊的處理,那么也就表示該Marker Interface所定義的類型從語(yǔ)義上來(lái)講是并不合適的。
而且從上面對(duì)Set接口以及Clonable接口的比較中可以看出,如果就像Effective Java的Item 37一樣通過(guò)Marker Interface來(lái)定義類型,那么對(duì)類型進(jìn)行定義的方式主要分為兩種:從一個(gè)接口派生以使得Marker Interface擁有較父接口多出的特殊性質(zhì)。而如果Marker Interface沒(méi)有一個(gè)父接口,那么其應(yīng)該是Object類所具有的一種特殊性質(zhì),并可以通過(guò)Object類所提供的各個(gè)組成來(lái)按該性質(zhì)進(jìn)行操作,就像Serializable接口那樣。
從一個(gè)接口派生來(lái)定義Marker Interface是比較常見(jiàn)的情況,但是也較容易出錯(cuò)。一個(gè)比較經(jīng)典的示例仍然是基于長(zhǎng)方形為正方形定義一個(gè)接口。假設(shè)一個(gè)系統(tǒng)中已經(jīng)擁有了一個(gè)用來(lái)表示長(zhǎng)方形的接口:
1 public interface Rectangle { 2 void setWidth(double width); 3 void setHeight(double height); 4 double getArea(); 5 }
由于正方形是長(zhǎng)方形的長(zhǎng)和寬都相等的一種特殊情況,因此我們常常認(rèn)為正方形是一種特殊的長(zhǎng)方形。對(duì)于這種情況,軟件開(kāi)發(fā)人員就可能決定通過(guò)從長(zhǎng)方形接口派生來(lái)定義一個(gè)正方形:
1 public interface Square extends Rectangle { 2 }
但是在使用過(guò)程中,他會(huì)別扭得要死。原因就是因?yàn)閷?shí)際上對(duì)長(zhǎng)方形所定義的接口,如setWidth(),setHeight()等對(duì)于正方形而言完全沒(méi)有意義。正方形所需要的是能夠設(shè)置它的邊長(zhǎng)。因此一個(gè)正確定義Marker Interface的前提就是原有接口中的各個(gè)成員對(duì)于Marker Interface所定義的概念仍然具有明確的意義。
OK,相信您在看到長(zhǎng)方形和正方形這個(gè)示例的時(shí)候首先想到的就是里氏替換原則(Liskov Substitution Principle)。但請(qǐng)不要使用里氏替換原則來(lái)判斷一個(gè)Marker Interface的定義是否合適。這是因?yàn)槔锸咸鎿Q原則實(shí)際上是使用在對(duì)象之間的:如果S是T的子類型,那么S對(duì)象就應(yīng)該能在不改變?nèi)魏纬橄髮傩缘那闆r下替換所有的T對(duì)象。畢竟,無(wú)論如何我們創(chuàng)建的都應(yīng)該是一個(gè)類型的實(shí)例,而不能直接創(chuàng)建接口的實(shí)例(基于匿名類的除外)。
例如對(duì)于Set接口,如果我們將所有對(duì)Collection接口的使用都替換為對(duì)Set接口的使用,那么至少對(duì)下面的語(yǔ)句進(jìn)行替換時(shí)會(huì)導(dǎo)致編譯器報(bào)出編譯錯(cuò)誤:
1 Collection<Item> itemCollection = new ArrayList<Item>();
因此,使用里氏替換原則來(lái)判斷一個(gè)Marker Interface是否合適實(shí)際上真沒(méi)有太多意義,這在stackoverflow上也有頗多討論。
Marker Interface vs. Annotation
在前面的章節(jié)中已經(jīng)提到過(guò),Marker Interface表示實(shí)現(xiàn)該接口的類型具有特殊的性質(zhì)。也就是說(shuō),Marker Interface是該類型的一個(gè)特性,也即是該類型的一個(gè)元數(shù)據(jù)。而在Java中,另一個(gè)可以用來(lái)表示類型元數(shù)據(jù)的Java組成是標(biāo)記。在處理相似問(wèn)題的情況下,不同的類庫(kù)選擇了不同的解決方案。例如Java中的序列化支持實(shí)際上是通過(guò)Serializable這個(gè)Marker Interface來(lái)完成的:
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中,用來(lái)對(duì)持久化到數(shù)據(jù)庫(kù)這一功能的控制是通過(guò)標(biāo)記來(lái)完成的:
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 }
隨之而來(lái)的一個(gè)問(wèn)題就是:我們應(yīng)該在什么情況下使用Marker Interface,又在什么情況下使用標(biāo)記呢?了解何時(shí)使用的前提就是了解兩者之間的優(yōu)劣。由于兩者是完全不同的兩種語(yǔ)法結(jié)構(gòu),因此它們之間的區(qū)別就顯得非常明顯:
首先從Marker Interface說(shuō)起。該方法較標(biāo)記的好處則在于,通過(guò)instanceof就直接能探測(cè)一個(gè)實(shí)例是否是一個(gè)特定接口的實(shí)例,而標(biāo)記則需要通過(guò)反射等方法來(lái)判斷特定實(shí)例上是否有特定的標(biāo)記。除了這個(gè)原因之外,對(duì)一個(gè)實(shí)例是否實(shí)現(xiàn)了某個(gè)接口可以在編譯時(shí)就可以進(jìn)行檢查,而一個(gè)實(shí)例是否有某個(gè)標(biāo)記則在運(yùn)行時(shí)才能進(jìn)行。在使用instanceof的時(shí)候,實(shí)際上我們是在探測(cè)某個(gè)實(shí)例是否是某個(gè)類型。因此對(duì)于Marker Interface來(lái)說(shuō),其首先需要有一定的實(shí)際意義。
標(biāo)記較Marker Interface的好處則在于:其粒度更細(xì)。可以說(shuō),Marker Interface只能施行在類型上,而標(biāo)記則可以施行在多種類型組成上,因此Marker Interface實(shí)際上是作為整體行為的一種考慮,而標(biāo)記則更注重具體細(xì)節(jié)。一個(gè)定義良好的細(xì)粒度API可以提供更大的靈活性。而且相較于接口,標(biāo)記的后續(xù)發(fā)展能力更強(qiáng),畢竟在一個(gè)接口中添加一個(gè)成員函數(shù)是一個(gè)非常麻煩的事情。
其實(shí)Marker Interface以及標(biāo)記之間擁有如此大的混淆的很大一部分原因則是兩者在功能上有重復(fù),而且在Java演化過(guò)程中出現(xiàn)的時(shí)機(jī)并不相同,導(dǎo)致在一些地方仍然擁有Marker Interface的不正當(dāng)使用。實(shí)際上,像Clonable這種值得商榷的Marker Interface在JDK中還有很多很多。之所以在JDK里面會(huì)出現(xiàn)那么多的Marker Interface,其中一個(gè)原因也是因?yàn)镴ava對(duì)標(biāo)記的支持比較晚的緣故。
轉(zhuǎn)載請(qǐng)注明原文地址并標(biāo)明轉(zhuǎn)載:http://www.rzrgm.cn/loveis715/p/5094367.html
商業(yè)轉(zhuǎn)載請(qǐng)事先與我聯(lián)系:silverfox715@sina.com

浙公網(wǎng)安備 33010602011771號(hào)