謹慎使用Marker Interface

  之因此寫這篇文章,源自於組內的一些技術討論。實際上,Effective Java的Item 37已經詳細地討論了Marker Interface。可是從整個Item的角度來看,其對於Marker Interface所提供的一系列優勢及特殊特性其實是持確定態度的。所以不少人,包括個人同事,都將該條目中的一些結論看成是準則來去執行,卻忽略了獲得這些結論時的前提,進而致使了必定程度的誤用。html

  固然,我並非在反對Effective Java的Item 37。說實話,我也沒有這個資本。只是我我的在技術上略顯保守,所以但願經過這篇文章闡述一下Marker Interface可能帶來的一系列問題,進而使你們更爲謹慎並且準確地使用Marker Interface。java

 

Marker Interface簡介數據庫

  或許有些讀者並不瞭解什麼是Marker Interface。那麼首先讓咱們來看看JDK中Set接口的實現:函數

1 public interface Set<E> extends Collection<E> {
2 }

  細心的讀者會發現,實際上Set較Collection沒有添加任何接口函數。那爲何JDK還要爲其定義一個額外的接口呢?spa

  相信您很快就能答出來:「這是由於Set中所包含的數據中不會有重複的元素,而Collection接口做爲集合類型接口的根接口,其沒有添加這種限制。」設計

  是的。JDK提供一個額外的Set接口的確就是出於這個目的。並且這種不添加任何新成員的接口實際上就是Marker Interface。並且在JDK中,Marker Interface還很多。另外一個很是著名的Marker Interface就是Clonable接口:code

1 public interface Cloneable {
2 }

  只是這一次,Marker Interface所受到的禮遇並不相同:不管是在對Prototype模式的講解中仍是在其它平常討論中,其都是做爲反面教材來詮釋什麼是一個不良的設計。htm

 

硬幣的正反面對象

  那Marker Interface究竟是好仍是很差呢?若是沒有分析,咱們就不會知道爲何Marker Interface在不一樣的狀況下獲得如此不一樣的評價,也更不會知道如何正確地使用Marker Interface。所以咱們先不說結論,而是從接口Set及Clonable兩個大相徑庭的狀況來分析Marker Interface表現出如此差別的緣由。blog

  正能量先行。咱們先來分析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

相關文章
相關標籤/搜索