第37条:用标记接口定义类型
标记接口(marker interface)是没有包含方法声明的接口,而只是指明(或者“标明”)一个类实现了具有某种属性的接口。例如,考虑Serializable
接口(见第11章)。通过实现这个接口,类表明它的实例可以被写到ObjectOutputStream
(或者“被序列化”)。
你可能听说过标记注解(见第35条)使得标记接口过时了。这种断言是不正确的。标记接口有两点胜过标记注解。首先,也是最重要的一点是,标记接口定义的类型是有被标记类的实例实现的;标记注解则没有定义这样的类型。这个类型允许你在编译时捕捉在使用标记注解的情况下要到运行时才能捕捉到的错误。
就Serializable
标记接口而言,如果它的参数没有实现该接口,ObjectOutputStream.write(Object)
方法将会失败。令人不解的是,ObjectOutputStream
API 的创建者在声明write
方法时并没有利用Serializable
接口。该方法的参数类型应该为Serializable
而非Object
。因此,试着在没有实现Serializable
的对象上调用ObjectOutputStream.write
,只会在运行时失败,但也并不一定如此。
标记接口胜过标记注解的另一个有点事,它们可以更加精确地进行锁定。如果注解类型利用@Target(ElementType.TYPE)
声明,它就可以被应用到任何类或者接口。假设有一个标记只适用于特殊接口的实现。如果将它定义成一个标记接口,就可以用它将唯一的接口扩展成它适用的接口。
Set
接口可以说就是这种有限制的标记接口(restricted marker interface)。它只适用于Collection
子类型,但是它不会添加除了Collection
方法的契约,包括add
、equals
和hashCode
。但是很容易想象只适用于某种特殊接口的子类型的标记接口,它没有改进接口的任何方法的契约。这种标记接口可以描述整个对象的某个约束条件,或者表明实例能够利用其他某个类的方法进行处理(就想Serializable
接口表明实例可以通过ObjectOutputStream
进行处理一样)。
标记注解胜过标记接口的最大优点在于,它可以通过默认的方式添加一个或者多个注解类型元素,给一被使用的注解类型添加更多的信息[JLS,9.6]。随着时间的推移,简单的标记注解类型可以演变成更加丰富的注解类型。这种演变对于标记接口而言则是不可能的,因为它通常不可能在实现接口之后再给它添加方法(见第18条)。
标记注解的另一个优点在于,它们是更大的注解机制的一部分。因此,标记注解在那些支持注解作为编程元素之一的框架中同样具有一致性。
那么什么时候应该使用标记注解,什么时候应该使用标记接口呢?很显然,如果标记是应用到任何程序元素而不是类或者接口,就必须使用注解,因为只有类和接口可以用来实现或者扩展接口。如果标记只应用给类和接口,就要问问自己:我要编写一个还是多个只接受有这种标记的方法呢?如果是这种情况,就应该优先使用标记接口而非注解。这样你就可以用接口作为相关方法的参数类型,它真正可以为你提供编译时进行类型检查的好处。
如果你对第一个问题的答案是否定的,就要再问问自己:我要永远限制这个标记只用于特殊接口的元素吗?如果是,最好将标记定义成该接口的一个子接口。如果这两个问题的答案都是否定的,或许就应该使用标记注解。
总而言之,标记接口和标记注解都各有用处。如果要定义一个任何新方法都不会与之关联的类型,标记接口就是最好的选择。如果想要标记程序元素而非类和接口,考虑到未来可能要给标记添加更多的信息,或者标记要适合于已经广泛使用了注解类型的框架,那么标记注解就是正确的选择。如果你发现自己在编写的是目标位ElementType.TYPE
的标记注解类型,就要花点时间考虑清楚,它是否真的应该为注解类型,想想标记接口是否会更加合适呢。
从某种意义上说,本条目与第19条中“如果不想定义类型就不要使用接口”的说法相反。本条目最接近的意思是说:如果想要定义类型,一定要使用接口。