用有意义的类名屏蔽Java集合的好坏方法?


46

最近,我习惯于用人类友好的类名“掩盖” Java集合。一些简单的例子:

// Facade class that makes code more readable and understandable.
public class WidgetCache extends Map<String, Widget> {
}

要么:

// If you saw a ArrayList<ArrayList<?>> being passed around in the code, would you
// run away screaming, or would you actually understand what it is and what
// it represents?
public class Changelist extends ArrayList<ArrayList<SomePOJO>> {
}

一位同事向我指出,这是一种不好的做法,并引入了延迟/潜伏期,并且是一种面向对象的反模式。我能理解它带来了很小的性能开销,但无法想象它是多么重要。所以我问:这是好事还是坏事,为什么?


11
比这简单得多。这是不好的做法,因为我想您正在扩展那些Java基本JDK集合的实现。在Java中,您只能扩展一个类,因此在进行扩展时必须考虑和设计更多。在Java中,请谨慎使用扩展。
InformedA 2014年

ChangeListextends因为List是接口,所以编译将在处中断implements。@randomA什么你想象忽略了一点,因为这个错误的
蚊蚋

@gnat这一点没有错,我假设他正在扩展implementationie HashMapTreeMapand他有错别字。
知悉2014年

4
这是一个坏习惯。坏坏坏。不要这样 每个人都知道Map <String,Widget>是什么。但是WidgetCache?现在我需要打开WidgetCache.java,我需要记住WidgetCache只是一张地图。每当我发布新版本时,我都必须检查您是否尚未向WidgetCache添加新内容。天哪,永远不要这样做。
Miles Rout 2014年

“如果在代码中看到ArrayList <ArrayList <?>>被传递,那么您会大声尖叫……吗?” 不,我对嵌套的通用集合非常满意。您也应该如此。
凯文·克鲁姆维德

Answers:


75

延迟/延迟?我打电话给BS。这种做法的开销应该完全为零。(编辑:注释中已经指出,实际上,这可能会抑制HotSpot VM执行的优化。我对VM的实施了解不足,无法确认或否认这一点。我的评论基于C ++虚拟功能的实现。)

有一些代码开销。您必须从所需的基类创建所有构造函数,并转发它们的参数。

我本身也不认为它是反模式。但是,我确实将其视为错失的机会。与其创建一个只为重命名而派生基类的类,不如创建一个包含该集合并提供特定于案例的改进接口的类?小部件缓存是否应该真正提供地图的完整界面?还是应该提供专门的界面?

此外,对于集合而言,该模式根本无法与使用接口而不是实现的一般规则一起使用-也就是说,在简单的集合代码中,您将创建一个HashMap<String, Widget>,然后将其分配给type变量Map<String, Widget>。您WidgetCache不能扩展Map<String, Widget>,因为这是一个接口。它不能是扩展基本接口的接口,因为HashMap<String, Widget>它没有实现该接口,任何其他标准集合也没有。虽然可以将其HashMap<String, Widget>设为扩展类,但随后必须将变量声明为WidgetCacheMap<String, Widget>,第一个使您无法灵活替换其他集合(可能是某些ORM的延迟加载集合),而第二种则使这一点失败了上课。

这些对策中的一些也适用于我提议的专业课。

这些都是要考虑的点。它可能是正确的选择,也可能不是正确的选择。无论哪种情况,您的同事提供的参数都是无效的。如果他认为这是一种反模式,则应命名。


1
请注意,尽管很有可能会产生一些性能影响,但这并不会那么可怕(在最坏的情况下,缺少一些内联优化并获得额外的vcall -在最内层的循环中不是很好,但是可能还不错)。
Voo

5
这个非常好的答案告诉每个人“不要用性能开销来判断决定” –此处下面的唯一讨论是“ HotSpot如何处理此问题,在某些情况下(占所有案例的99.9%)有不相关的性能影响”,您甚至不愿意阅读比第一句话更多的东西吗?
布朗

16
+1表示“不是创建一个仅出于重命名而派生基类的类,而是创建一个包含该集合并提供特定于案例的改进接口的类呢?”
user11153 2014年

6
说明这个答案要点实例:该文档public class Properties extends Hashtable<Object,Object>java.util说:“因为从Hashtable中,认沽和中的putAll方法属性继承可以应用到Properties对象它们的使用是严格禁止的,因为它们允许调用方插入其键条目。或值不是字符串。”。组成本来会干净得多。
Patricia Shanahan 2014年

3
@DocBrown不,此答案声称对性能没有任何影响。如果您发表有力的声明,则最好能够提供支持,否则人们可能会大声疾呼。评论(针对答案)的全部重点是指出不正确的事实或假设或添加有用的注释,但当然不是要恭喜别人,这就是投票系统的目的。
Voo 2014年

25

根据IBM的说法,这实际上是一种反模式。这些类似于“ typedef”的类称为伪类型。

这篇文章比我解释的要好得多,但是如果链接断开,我将尝试对其进行总结:

  • 任何期望a的代码WidgetCache都无法处理Map<String, Widget>
  • 当使用多个软件包时,这些伪类型是“病毒”的,它们会导致不兼容,而基本类型(只是一个愚蠢的Map <...>)在所有软件包中都适用。
  • 伪类型通常是具体的,它们不实现特定的接口,因为它们的基类仅实现通用版本。

在本文中,他们提出了以下技巧,可以在不使用伪类型的情况下简化生活:

public static <K,V> Map<K,V> newHashMap() {
    return new HashMap<K,V>(); 
}

Map<Socket, Future<String>> socketOwner = Util.newHashMap();

由于自动类型推断而起作用。

(我通过这个相关的堆栈溢出问题来到了这个答案)


13
newHashMap钻石经营者现在不解决需求吗?
svick 2014年

绝对正确,忘记了这一点。我通常不使用Java。
Roy T. 2014年

我希望语言允许有一个变量类型的世界,该变量类型保留对其他类型的实例的引用,但仍可以支持编译时检查,以便WidgetCache可以将类型的值分配给类型的变量WidgetCacheMap<String,Widget>不使用强制类型转换,但是在那里是一种方法,其中的静态方法WidgetCache可以在执行任何所需的验证之后引用Map<String,Widget>并以类型返回它WidgetCache。这种功能对泛型尤其有用,因为泛型不必进行类型擦除(因为...
supercat

...这些类型只会在编译器的脑海中存在)。对于许多可变类型,有两种常见的引用字段类型:一种持有对可能被突变的实例的唯一引用,另一种持有对任何人都不允许进行突变的实例的可共享引用。能够为这两种字段赋予不同的名称将是有帮助的,因为它们需要不同的使用模式,但是它们都应持有对相同类型的对象实例的引用。
超级猫

5
对于Java为什么需要类型别名,这是一个很好的论据。
GlenPeterson

13

对性能的影响最多将限于vtable查找,这很可能已经引起了。这不是反对它的正当理由。

这种情况非常普遍,大多数静态类型的编程语言都对别名类型具有特殊的语法,通常称为a typedef。Java可能没有复制那些,因为它最初没有参数化类型。扩展类不是理想的,这是由于Sebastian在回答中很好地说明了原因,但是对于Java的有限语法来说,这可能是一个合理的解决方法。

Typedef具有许多优点。它们在更适当的抽象级别上以更好的名称更清楚地表达了程序员的意图。它们更易于搜索以进行调试或重构。考虑在各处使用a WidgetCache,而不是查找a的特定用途Map。它们更容易更改,例如,如果以后您发现需要一个LinkedHashMap甚至是您自己的自定义容器,就可以更改它们。


构造函数的开销也很小。
Stephen C

11

正如其他人已经提到的那样,我建议您使用组合而不是继承,这样您就可以只公开真正需要的方法,并且名称要与预期的用例相匹配。您班上的用户真的需要知道那WidgetCache是一张地图吗?并能随心所欲地处理它吗?还是他们只需要知道这是小部件的缓存?

我的代码库中的示例类,提供了您所描述的类似问题的解决方案:

public class Translations {

    private Map<Locale, Properties> translations = new HashMap<>();

    public void appendMessage(Locale locale, String code, String message) {
        /* code */
    }

    public void addMessages(Locale locale, Properties messages) {
        /* code */
    }

    public String getMessage(Locale locale, String code) {
        /* code */
    }

    public boolean localeExists(Locale locale) {
        /* code */
    }
}

您可以在内部看到它只是“一张地图”,但公共界面未显示此信息。它具有“程序员友好”的方法,例如appendMessage(Locale locale, String code, String message)用于插入新条目的更简单,更有意义的方法。而class的用户不能这样做,例如translations.clear(),因为Translationsnot extend Map

(可选)您始终可以将一些所需的方法委派给内部使用的地图。


6

我将其视为有意义的抽象的示例。一个好的抽象有两个特点:

  1. 它隐藏了与使用它的代码无关的实现细节。

  2. 它只是根据需要而复杂。

通过扩展,您可以公开父级的整个接口,但是在许多情况下,其中的许多接口可能会更好地隐藏起来,因此您需要执行Sebastian Redl建议的操作,并主张继承优先于继承,并添加父级实例作为您的自定义课程的私人成员。任何对您的抽象有意义的接口方法都可以轻松地委派给(对于您而言)内部集合。

至于性能影响,始终建议先优化代码的可读性,如果怀疑性能影响,请分析代码以比较这两种实现。


4

+1此处的其他答案。我还要补充一点,域驱动设计(DDD)社区实际上认为这是非常好的做法。他们主张您的域及其交互应具有语义域含义,而不是基础数据结构。A Map<String, Widget>可能是一个缓存,但也可能是其他东西,您在My Not So Humble Opinion(IMNSHO)中正确完成的操作是对集合表示的内容进行建模,在本例中为缓存。

我将添加一个重要的编辑,因为围绕底层数据结构的域类包装器可能还应该具有其他成员变量或函数,这些成员变量或函数真正使它成为具有交互作用的域类,而不仅仅是数据结构(如果只有Java具有值类型) ,我们将在Java 10中提供它们-保证!)

有趣的是,看到Java 8的流将对所有这些产生什么影响,我可以想象,也许某些公共接口将更喜欢处理(插入通用Java原语或String)流而不是Java对象。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.