谨慎使用Marker Interface

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

商业转载请事先与我联系:[email protected]

时间: 2024-10-16 02:07:57

谨慎使用Marker Interface的相关文章

什么是Java Marker Interface(标记接口)

先看看什么是标记接口?标记接口有时也叫标签接口(Tag interface),即接口不包含任何方法.在Java里很容易找到标记接口的例子,比如JDK里的Serializable接口就是一个标记接口. 首先明确一点,Marker Interface(标记接口)决不是Java这门编程语言特有的,而是计算机科学中一种通用的设计理念. 我们看Wikipedia里对标记接口的定义. "The tag/ marker interface pattern is a design pattern in comp

Learn Java for Android Development Second Edition 笔记(六)- Interface

Interface Java里用interface关键字,引入一种没有具体实现的类型. Declaring Interfaces interface一般以大写字母开头,able单词结束,如下例子: interface Drawable { int RED = 1; // For simplicity, integer constants are used. These constants are int GREEN = 2; // not that descriptive, as you wil

Java中接口(Interface)的定义和使用

有关 Java 中接口的使用相信程序员们都知道,但是你们知不知道接口到底有什么用呢?毫无疑问,接口的重要性远比想象中重要.接下来我们便一起来学习Java中接口使用. Java接口是什么 Java接口是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的行为(功能). 接口(英语:Interface),在JAVA编程语言中是一个抽象类型(Abstract Type),它被用来要求类(Class)必须实现指

Java 基础 - ArrayList 源码

最近发现有些源码直接翻译英文注释就可以了,论学好英语的重要性,之后的源码就以翻译注释为主了 继承 这些个接口或类基本都认识,不多介绍,主要介绍 RandomAccess 接口: RandomAccess 接口也是一个空接口,作用可以看接口的注释: Marker interface used by List implementations to indicate that they support fast (generally constant time) random access. The

Java中的浅拷贝与深拷贝

Object中的clone方法: protected native Object clone() throws CloneNotSupportedException; 创建并返回此对象的一个副本.“副本”的准确含义可能依赖于对象的类.这样做的目的是,对于任何对象 x,表达式: x.clone() != x 为 true,表达式: x.clone().getClass() == x.getClass() 也为 true,但这些并非必须要满足的要求.一般情况下: x.clone().equals(x

深入Spring Boot:那些注入不了的Spring占位符(${}表达式)

Spring里的占位符 spring里的占位符通常表现的形式是: <bean id="dataSource" destroy-method="close" class="org.apache.commons.dbcp.BasicDataSource"> <property name="url" value="${jdbc.url}"/> </bean> 或者 @Confi

Java IO5:序列化与反序列化

一.序列化和反序列化的概念 把对象转换为字节序列的过程称为对象的序列化. 把字节序列恢复为对象的过程称为对象的反序列化. 对象的序列化主要有两种用途: 1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中: 2) 在网络上传送对象的字节序列. 在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存.比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些s

Solr4.8.0源码分析(14)之SolrCloud索引深入(1)

Solr4.8.0源码分析(14) 之 SolrCloud索引深入(1) 上一章节<Solr In Action 笔记(4) 之 SolrCloud分布式索引基础>简要学习了SolrCloud的索引过程,本节开始将通过阅读源码来深入学习下SolrCloud的索引过程. 1. SolrCloud的索引过程流程图 这里借用下<solrCloud Update Request Handling 更新索引流程>流程图: 由上图可以看出,SolrCloud的索引过程主要通过一个索引链过程来实

Akka(10): 分布式运算:集群-Cluster

Akka-Cluster可以在一部物理机或一组网络连接的服务器上搭建部署.用Akka开发同一版本的分布式程序可以在任何硬件环境中运行,这样我们就可以确定以Akka分布式程序作为标准的编程方式了. 在上面两篇讨论里我们介绍了Akka-Remoting.Akka-Remoting其实是一种ActorSystem之间Actor对Actor点对点的沟通协议.通过Akka-Remoting来实现一个ActorSystem中的一个Actor与另一个Actorsystem中的另一个Actor之间的沟通.在Re