之因此写这篇文章,源自于组内的一些技术讨论。实际上,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