之所以写这篇文章,源自于组内的一些技术讨论。实际上,Effective Java的Item 37已经详细地讨论了Marker Interface。但是从整个Item的角度来看,其对于Marker Interface所提供的一系列优点及特殊特性实际上是持肯定态度的。因此很多人,包括我的同事,都将该条目中的一些结论当作是准则来去执行,却忽略了得到这些结论时的前提,进而导致了一定程度的误用。

  当然,我并不是在反对Effective Java的Item 37。说实话,我也没有这个资本。只是我个人在技术上略显保守,因此希望通过这篇文章阐述一下Marker Interface可能带来的一系列问题,进而使大家更为谨慎而且准确地使用Marker Interface。

Marker Interface简介

  或许有些读者并不了解什么是Marker Interface。那么首先让我们来看看JDK中Set接口的实现:

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

  细心的读者会发现,实际上Set较Collection没有添加任何接口函数。那为什么JDK还要为其定义一个额外的接口呢?

  相信您很快就能答出来:“这是因为Set中所包含的数据中不会有重复的元素,而Collection接口作为集合类型接口的根接口,其没有添加这种限制。”

  是的。JDK提供一个额外的Set接口的确就是出于这个目的。而且这种不添加任何新成员的接口实际上就是Marker Interface。而且在JDK中,Marker Interface还不少。另一个非常著名的Marker Interface就是Clonable接口:

 public interface Cloneable {
}

  只是这一次,Marker Interface所受到的礼遇并不相同:无论是在对Prototype模式的讲解中还是在其它日常讨论中,其都是作为反面教材来诠释什么是一个不良的设计。

硬币的正反面

  那Marker Interface到底是好还是不好呢?如果没有分析,我们就不会知道为什么Marker Interface在不同的情况下得到如此不同的评价,也更不会知道如何正确地使用Marker Interface。因此我们先不说结论,而是从接口Set及Clonable两个截然不同的情况来分析Marker Interface表现出如此差异的原因。

  正能量先行。我们先来分析Set这个Marker Interface表现良好的原因。当用户看到Set这个接口的时候,他首先想到的就是它是一个集合,而且该集合具有不会存在重复元素这样一个性质。在对该接口实例进行操作的时候,软件开发人员可以直接通过调用Set接口所继承过来的各个成员函数来操作它。这些接口所定义的操作需要由Set接口的实现类来定义。因此Set的这种不存在重复元素的性质实际上是由接口的实现类所保证的。如在添加一个元素的时候,我们不必担心当前是否该元素是否已经在集合中存在了:

 Set<Item> itemSet = …
itemSet.add(item);

  而对于其它类型的集合,如List,我们就需要检查元素是否已经在集合中存在,否则其内部将存在着对该元素的重复引用:

 List<Item> itemList = …
if (!itemList.contains(item)) {
itemList.add(item);
}

  反过来,另一个Marker Interface Clonable则是臭名昭著的。具体原因已经在Effective Java中的Item 17中已经讲得很清楚了。实际上,创建该接口的思路和创建Set接口的思路原本是一致的:该接口用来标示实现了该接口的类型是可以被拷贝的。其中的一个问题在于,Object类型的clone()函数是受保护的。从而使得用户代码不能调用Clonable接口的clone()函数。这样就要求用户通过其它方法来实现Clonable接口所表示的语义。进而在代码中产生了大量的如下代码:

 if (obj instanceof Clonable) {
……
} else {
……
}

  这样,如果一个实例实现了特定的接口,如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是比较常见的情况,但是也较容易出错。一个比较经典的示例仍然是基于长方形为正方形定义一个接口。假设一个系统中已经拥有了一个用来表示长方形的接口:

 public interface Rectangle {
void setWidth(double width);
void setHeight(double height);
double getArea();
}

  由于正方形是长方形的长和宽都相等的一种特殊情况,因此我们常常认为正方形是一种特殊的长方形。对于这种情况,软件开发人员就可能决定通过从长方形接口派生来定义一个正方形:

 public interface Square extends Rectangle {
}

  但是在使用过程中,他会别扭得要死。原因就是因为实际上对长方形所定义的接口,如setWidth(),setHeight()等对于正方形而言完全没有意义。正方形所需要的是能够设置它的边长。因此一个正确定义Marker Interface的前提就是原有接口中的各个成员对于Marker Interface所定义的概念仍然具有明确的意义。

  OK,相信您在看到长方形和正方形这个示例的时候首先想到的就是里氏替换原则(Liskov Substitution Principle)。但请不要使用里氏替换原则来判断一个Marker Interface的定义是否合适。这是因为里氏替换原则实际上是使用在对象之间的:如果S是T的子类型,那么S对象就应该能在不改变任何抽象属性的情况下替换所有的T对象。毕竟,无论如何我们创建的都应该是一个类型的实例,而不能直接创建接口的实例(基于匿名类的除外)。

  例如对于Set接口,如果我们将所有对Collection接口的使用都替换为对Set接口的使用,那么至少对下面的语句进行替换时会导致编译器报出编译错误:

 Collection<Item> itemCollection = new ArrayList<Item>();

  因此,使用里氏替换原则来判断一个Marker Interface是否合适实际上真没有太多意义,这在stackoverflow上也有颇多讨论。

Marker Interface vs. Annotation

  在前面的章节中已经提到过,Marker Interface表示实现该接口的类型具有特殊的性质。也就是说,Marker Interface是该类型的一个特性,也即是该类型的一个元数据。而在Java中,另一个可以用来表示类型元数据的Java组成是标记。在处理相似问题的情况下,不同的类库选择了不同的解决方案。例如Java中的序列化支持实际上是通过Serializable这个Marker Interface来完成的:

 public class Employee implements java.io.Serializable
{
public String name;
public String address;
public transient int SSN;
public int number;
}

  而在JPA中,用来对持久化到数据库这一功能的控制是通过标记来完成的:

 @Entity
@Table(name = "employee")
public class Employee {
@Column(name = "name", unique = false, nullable = false, length = 40)
private String name; @Column(name = "address", unique = false, nullable = false, length = 200)
private String address; @Column(name = "number", unique = false, nullable = false)
private int number; @Transient
private float percentageProcessed;
......
}

  随之而来的一个问题就是:我们应该在什么情况下使用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

谨慎使用Marker Interface的更多相关文章

  1. Java Marker Interface

    先看看什么是标记接口?标记接口有时也叫标签接口(Tag interface),即接口不包含任何方法. 在Java里很容易找到标记接口的例子,比如JDK里的Serializable接口就是一个标记接口. ...

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

    先看看什么是标记接口?标记接口有时也叫标签接口(Tag interface),即接口不包含任何方法.在Java里很容易找到标记接口的例子,比如JDK里的Serializable接口就是一个标记接口. ...

  3. Effective Java 37 Use marker interfaces to define types

    Marker interface is an interface that contains no method declarations, but merely designates (or &qu ...

  4. 《Beginning Java 7》 - 7 - abstract class 抽象类 和 interface 接口

    1. 抽象类: 为什么用抽象类: 一些 generic 的类本身并没有现实意义,所以不需要被实例化.比如动物,自然界没有动物这个物种,但却有无数的继承自动物的物种,那么动物本身可以是一个抽象类. 抽象 ...

  5. Java 接口(interface)的三种类型

    放入接口中的任何域(成员变量)都自动是 static 和 final 的: 1. 包含抽象方法的常规接口 2. 全部是常量的 接口类中的方法和属性不要添加任何修饰符号(public 也不需要). 因为 ...

  6. spring 装配

    spring 3种装配方式: 支持混合配置:不管使用JavaConfig还是使用XML进行装配,通常都会创建一个根配置(root configuration), 这个配置会将两个或更多的装配类和/或X ...

  7. 《Effective Java 第二版》读书笔记

    想成为更优秀,更高效程序员,请阅读此书.总计78个条目,每个对应一个规则. 第二章 创建和销毁对象 一,考虑用静态工厂方法代替构造器 二, 遇到多个构造器参数时要考虑用builder模式 /** * ...

  8. Spring常用注解之一

    Spring中的常用注解 @Component 把普通 pojo 实例化到 Spring 容器中,相当于配置文件中的 泛指各种组件,就是说当我们的类不属于各种归类的时候(不属于@Controller. ...

  9. 0031 Java学习笔记-梁勇著《Java语言程序设计-基础篇 第十版》英语单词

    第01章 计算机.程序和Java概述 CPU(Central Processing Unit) * 中央处理器 Control Unit * 控制单元 arithmetic/logic unit /ə ...

随机推荐

  1. ASP.NET Core 1.1 简介

    ASP.NET Core 1.1 于2016年11月16日发布.这个版本包括许多伟大的新功能以及许多错误修复和一般的增强.这个版本包含了多个新的中间件组件.针对Windows的WebListener服 ...

  2. RabbitMq应用一

    RabbitMq应用一 RabbitMQ的具体概念,百度百科一下,我这里说一下我的理解,如果有少或者不对的地方,欢迎纠正和补充. 一个项目架构,小的时候,一般都是传统的单一网站系统,或者项目,三层架构 ...

  3. 通过VMware的PowerCLI配置集群内指定主机的vMotion功能

    PowerCLI是VMware开发的基于微软(MSFT)的PowerShell的命令行管理vSphere的实现,因此在批量化操作方面CLI会减轻很多GUI环境下的繁琐重复劳作. 现有场景中有大量的物理 ...

  4. Div Vertical Menu ver5

    这个小功能,如果是算此次,已经是第5次修改了.可以从这里看到前4次:V1, http://www.cnblogs.com/insus/archive/2011/10/17/2215637.html V ...

  5. javascript 笔记!

    1.通过javascript向文档中输出文本 document是javascript的内置对象,代表浏览器的文档部分 document.write("Hello Javascript&quo ...

  6. Unicode 和 UTF-8 有何区别?

    Unicode符号范围 (一个字符两个字节)     | UTF-8编码方式 (十六进制)     | (二进制) —————————————————————– 这儿有四个字节从-----00 00 ...

  7. linux下使用shell 自动执行脚本文件

    以下实例本人在Centos6.5 64位操作系统中使用 一.定时复制文件 a.在/usr/local/wfjb_web_back目录下创建 tomcatBack.sh文件 文件内容: #将tomcat ...

  8. nginx代理https站点(亲测)

    nginx代理https站点(亲测) 首先,我相信大家已经搞定了nginx正常代理http站点的方法,下面重点介绍代理https站点的配置方法,以及注意事项,因为目前大部分站点有转换https的需要所 ...

  9. Windows下Nginx配置SSL实现Https访问(包含证书生成)

    Vincent.李   Windows下Nginx配置SSL实现Https访问(包含证书生成) Windows下Nginx配置SSL实现Https访问(包含证书生成) 首先要说明为什么要实现https ...

  10. Linux+apache+mono+asp.net安装教程

    Linux+apache+mono+asp.net安装教程(CentOS上测试的) 一.准备工作: 1.安装linux系统(CentOS,这个就不多讲了) 2.下载所需软件 http-2.4.4.ta ...