这是最近关于 Guava 邮件列表的讨论 - 我的回答涉及到这个相当基本的问题。
http://groups.google.com/group/guava-discuss/browse_thread/thread/2d422600e7f87367/1e6c6a7b41c87aac
它的要点是:
当您希望您的对象被包装时,不要使用标记接口。 (嗯,这很笼统 - 如何您知道您的对象不会被客户端包装?)
例如,ArrayList。显然,它实现了RandomAccess。然后你决定为List 对象创建一个包装器。哎呀!现在,当你包装时,你必须检查被包装的对象,如果它是 RandomAccess,你创建的包装器应该也实现 RandomAccess!
这“很好”...如果您只有一个标记界面!但是如果被包装的对象可以是可序列化的呢?如果它是,比如说,“不可变的”(假设你有一个类型来表示它)怎么办?还是同步的? (同样的假设)。
正如我在对邮件列表的回答中还指出的那样,这种设计缺陷也体现在旧的 java.io 包中。假设您有一个接受InputStream 的方法。你会直接读它吗?如果它是一个昂贵的流,并且没有人愿意为您将其包装在 BufferedInputStream 中怎么办?哦,这很容易!你只需检查stream instanceof BufferedInputStream,如果没有,你自己包装!但不是。流可能在链的下游某处有缓冲,但您可能会得到它的包装器,它不是 BufferedInputStream 的实例。因此,“此流已缓冲”的信息丢失了(也许您必须悲观地浪费内存来再次缓冲它)。
如果您想正确做事,只需将功能建模为对象。考虑:
interface YourType {
Set<Capability> myCapabilities();
}
enum Capability {
SERIALIAZABLE,
SYNCHRONOUS,
IMMUTABLE,
BUFFERED //whatever - hey, this is just an example,
//don't throw everything in of course!
}
编辑: 应该注意的是,我使用枚举只是为了方便。可以有一个接口Capability 和一组实现它的开放对象(可能是多个枚举)。
因此,当您包装这些对象时,您将获得一组功能,并且您可以轻松决定保留哪些功能、删除哪些功能、添加哪些功能。
这个确实,显然,有它的缺点,所以它只在你真正感受到包装器隐藏功能的痛苦的情况下使用标记接口。例如,假设您编写的一段代码采用 List,但它必须是 RandomAccess AND 可序列化的。使用通常的方法,这很容易表达:
<T extends List<Integer> & RandomAccess & Serializable> void method(T list) { ... }
但在我描述的方法中,你所能做的就是:
void method(YourType object) {
Preconditions.checkArgument(object.getCapabilities().contains(SERIALIZABLE));
Preconditions.checkArgument(object.getCapabilities().contains(RANDOM_ACCESS));
...
}
我真的希望有一种比这两种方法都更令人满意的方法,但从前景来看,它似乎不可行(至少不会导致组合类型爆炸)。
编辑:另一个缺点是,如果没有明确的type每个功能,我们没有自然的地方来放置表达此功能提供的方法的方法。这在本次讨论中并不太重要,因为我们讨论的是 marker 接口,即不通过其他方法表达的功能,但为了完整起见,我提到它。
PS:顺便说一下,如果你浏览一下 Guava 的集合代码,你可以真正感受到这个问题造成的痛苦。是的,一些优秀的人试图将它隐藏在好的抽象背后,但潜在的问题仍然是痛苦的。