【问题标题】:Is it better to return an ImmutableMap or a Map?返回 ImmutableMap 还是 Map 更好?
【发布时间】:2016-06-28 23:30:07
【问题描述】:

假设我正在编写一个应该返回 Map 的方法。例如:

public Map<String, Integer> foo() {
  return new HashMap<String, Integer>();
}

考虑了一会儿,我决定一旦创建了这个地图就没有理由修改它。因此,我想返回一个ImmutableMap

public Map<String, Integer> foo() {
  return ImmutableMap.of();
}

我应该将返回类型保留为通用 Map,还是应该指定返回 ImmutableMap ?

从一方面来看,这正是创建接口的原因;隐藏实现细节。
另一方面,如果我这样保留它,其他开发人员可能会错过这个对象是不可变的这一事实。因此,我不会实现不可变对象的主要目标。通过最小化可以更改的对象的数量来使代码更清晰。更糟糕的是,一段时间后,有人可能会尝试更改此对象,这将导致运行时错误(编译器不会对此发出警告)。

【问题讨论】:

  • 我怀疑有人最终会将这个问题标记为对争论过于开放。您能否提出更具体的问题而不是“WDYT”? “可以吗……?”例如,“返回常规地图是否有任何问题或注意事项?”以及“存在哪些替代方案?”
  • 如果以后你决定它应该返回一个可变映射怎么办?
  • @immibis 大声笑,或者一个列表?
  • 你真的想在你的 API 中加入 Guava 依赖吗?如果不破坏向后兼容性,您将无法更改它。
  • 我觉得这个问题太粗略了,很多重要的细节都遗漏了。例如,您是否已经将 Guava 作为(可见)依赖项。即使您已经拥有它,代码 sn-ps 也是伪代码,并不能传达您真正想要实现的目标。你肯定不会return ImmutableMap.of(somethingElse)。相反,您会将ImmutableMap.of(somethingElse) 存储为一个字段,并仅返回该字段。所有这些都会影响这里的设计。

标签: java design-patterns immutable-collections


【解决方案1】:
  • 如果您正在编写一个面向公众的 API,并且不可变性是您设计的一个重要方面,我肯定会通过方法名称明确表示返回的映射将是不可变的或返回地图的具体类型。在我看来,在 javadoc 中提及它是不够的。

    由于您显然使用的是 Guava 实现,因此我查看了文档,它是一个抽象类,因此它确实为您提供了一些实际的具体类型的灵活性。

  • 如果您正在编写内部工具/库,则只返回一个普通的 Map 会更容易接受。人们将了解他们正在调用的代码的内部结构,或者至少可以轻松访问它。

我的结论是,明确是好的,不要让事情碰运气。

【讨论】:

  • 一个额外的接口,一个额外的抽象类,以及一个扩展这个抽象类的类。对于 OP 所要求的这样一个简单的事情来说,这太麻烦了,即该方法是否应该返回 Map 接口类型或 ImmutableMap 实现类型。
  • 如果您指的是 Guava 的 ImmutableMap,Guava 团队的建议是考虑将 ImmutableMap 视为具有语义保证的接口,并且您应该直接返回该类型。
  • @LouisWasserman mm,没有看到问题是提供 Guava 实现的链接。那我把sn-p去掉,谢谢
  • 我同意 Louis,如果您打算返回一个不可变对象的映射,请确保返回的对象最好是该类型。如果出于某种原因您想掩盖它是不可变类型的事实,请定义您自己的接口。将其保留为 Map 具有误导性。
  • @WSimpson 我相信这就是我的回答。 Louis 说的是我添加的一些 sn-p,但我删除了它。
【解决方案2】:

您应该将ImmutableMap 作为您的返回类型。 Map 包含 ImmutableMap 的实现不支持的方法(例如 put)并在 ImmutableMap 中标记为 @deprecated

使用不推荐使用的方法会导致编译器警告,当人们尝试使用不推荐使用的方法时,大多数 IDE 都会发出警告。

此高级警告优于将运行时异常作为您第一个提示出现问题的提示。

【讨论】:

  • 这是我认为最明智的答案。 Map 接口是一个契约,ImmutableMap 显然违反了它的重要部分。在这种情况下使用 Map 作为返回类型是没有意义的。
  • @TonioElGringo 那么Collections.immutableMap 呢?
  • @TonioElGringo 请注意,ImmutableMap 未实现的方法在接口文档docs.oracle.com/javase/7/docs/api/java/util/… 中明确标记为可选。因此,我认为返回 Map (使用适当的 javadocs)仍然有意义。尽管如此,由于上述原因,我还是选择显式返回 ImmutableMap。
  • @TonioElGringo 它没有破坏任何东西,这些方法是可选的。与List 一样,不能保证List::add 有效。试试Arrays.asList("a", "b").add("c");。没有迹象表明它会在运行时失败,但确实如此。至少 Guava 给了你一个提示。
【解决方案3】:

另一方面,如果我将其保留为这样,其他开发人员可能会错过这个对象是不可变的这一事实。

您应该在 javadocs 中提及这一点。开发人员确实会阅读它们,你知道的。

因此,我不会实现不可变对象的主要目标;使 通过最小化可以更改的对象的数量,使代码更清晰。 更糟糕的是,一段时间后,有人可能会尝试更改此对象, 这将导致运行时错误(编译器不会发出警告 关于它)。

没有开发人员发布未经测试的代码。当他测试它时,他会抛出一个异常,他不仅看到了原因,还看到了他试图写入不可变映射的文件和行。

请注意,只有 Map 本身是不可变的,而不是它包含的对象。

【讨论】:

  • “没有开发者会发布未经测试的代码。” 你想赌这个吗?如果这句话是真的,那么公共软件中的(我的假设)错误将会少得多。我什至不确定“大多数开发人员......”是否正确。是的,这令人失望。
  • 好吧,汤姆是对的,我不确定你想说什么,但你实际写的显然是错误的。 (另外:推动性别包容性)
  • @Tom 我猜你错过了这个答案中的讽刺
【解决方案4】:

如果我就这样保留它,其他开发人员可能会错过这个对象是不可变的这一事实

没错,但其他开发人员应该测试他们的代码并确保它被覆盖。

不过,您还有 2 个选项可以解决这个问题:

  • 使用 Javadoc

    @return a immutable map
    
  • 选择一个描述性的方法名称

    public Map<String, Integer> getImmutableMap()
    public Map<String, Integer> getUnmodifiableEntries()
    

    对于具体的用例,您甚至可以更好地命名方法。例如

    public Map<String, Integer> getUnmodifiableCountByWords()
    

你还能做什么?!

你可以返回一个

  • 复制

    private Map<String, Integer> myMap;
    
    public Map<String, Integer> foo() {
      return new HashMap<String, Integer>(myMap);
    }
    

    如果您预计很多客户端会修改地图并且只要地图只包含少量条目,则应使用此方法。

  • CopyOnWriteMap

    copy on write collections 通常在你必须处理的时候使用
    并发。但这个概念也会在你的情况下帮助你,因为 CopyOnWriteMap 在可变操作(例如添加、删除)上创建内部数据结构的副本。

    在这种情况下,您需要一个围绕您的映射的瘦包装器,它将所有方法调用委托给底层映射,除了可变操作。如果调用了可变操作,它会创建底层映射的副本,并且所有进一步的调用都将委托给该副本。

    如果您预计某些客户端会修改地图,则应使用此方法。

    遗憾的是 java 没有这样的CopyOnWriteMap。但你可能会找到第三方或自己实施。

最后你应该记住,地图中的元素可能仍然是可变的。

【讨论】:

    【解决方案5】:

    肯定返回一个 ImmutableMap,理由是:

    • 方法签名(包括返回类型)应该是自记录的。评论就像客户服务:如果您的客户需要依赖他们,那么您的主要产品就有缺陷。
    • 某个东西是接口还是类仅在扩展或实现时才相关。给定一个实例(对象),99% 的时间客户端代码不会知道或关心某物是接口还是类。起初我认为 ImmutableMap 是一个接口。点开链接后我才知道这是一门课。

    【讨论】:

      【解决方案6】:

      这取决于类本身。 Guava 的ImmutableMap 并非旨在成为可变类的不可变视图。如果您的类是不可变的并且具有一些基本上是ImmutableMap 的结构,则将返回类型设为ImmutableMap。但是,如果您的类是可变的,请不要这样做。如果你有这个:

      public ImmutableMap<String, Integer> foo() {
          return ImmutableMap.copyOf(internalMap);
      }
      

      Guava 每次都会复制地图。那很慢。但如果internalMap 已经是ImmutableMap,那就完全没问题了。

      如果您不将班级限制为返回 ImmutableMap,则可以改为返回 Collections.unmodifiableMap,如下所示:

      public Map<String, Integer> foo() {
          return Collections.unmodifiableMap(internalMap);
      }
      

      请注意,这是地图中不可变的视图。如果internalMap 更改,Collections.unmodifiableMap(internalMap) 的缓存副本也会更改。不过,我仍然更喜欢它作为吸气剂。

      【讨论】:

      • 这些都是好点,但为了完整起见,我喜欢this answer,它解释了unmodifiableMap 只能由引用的持有者修改 - 这是一个视图,支持地图的持有者可以修改后备地图,然后视图发生变化。所以这是一个重要的考虑因素。
      • @正如 davidback 评论的那样,使用 Collections.unmodifiableMap 时要非常小心。该方法返回一个 view 到原始地图上,而不是创建一个单独的不同的新地图对象。因此,引用原始地图的任何其他代码都可以修改它,然后假定不可修改的地图的持有者知道地图确实可以从它们下面修改。我自己也被这个事实所困扰。我学会了要么返回地图副本,要么使用 Guava ImmutableMap
      【解决方案7】:

      这并没有回答确切的问题,但仍然值得考虑是否应该返回地图。如果地图是不可变的,那么提供的主要方法是基于get(key):

      public Integer fooOf(String key) {
          return map.get(key);
      }
      

      这使得 API 更加紧凑。如果实际需要地图,则可以通过提供条目流将其留给 API 的客户端:

      public Stream<Map.Entry<String, Integer>> foos() {
          map.entrySet().stream()
      }
      

      然后客户端可以根据需要制作自己的不可变或可变映射,或者将条目添加到自己的映射中。如果客户端需要知道该值是否存在,则可以返回 optional:

      public Optional<Integer> fooOf(String key) {
          return Optional.ofNullable(map.get(key));
      }
      

      【讨论】:

        【解决方案8】:

        Immutable Map 是 Map 的一种。所以保留 Map 的返回类型是可以的。

        为保证用户不修改返回对象,方法的文档可以描述返回对象的特征。

        【讨论】:

          【解决方案9】:

          这可以说是一个见仁见智的问题,但这里更好的想法是使用地图类的接口。这个接口不需要显式的说它是不可变的,但是如果你不在接口中暴露任何父类的setter方法,消息还是一样的。

          看看下面的文章:

          andy gibson

          【讨论】:

          • 不必要的包装被认为是有害的。
          • 你说得对,我这里也不会用wrapper,接口就够了。
          • 我有一种感觉,如果这是正确的做法,那么 ImmutableMap 已经实现了 ImmutableMapInterface,但我从技术上不理解这一点。
          • 重点不是 Guava 开发者的本意,而是这个开发者想要封装架构而不暴露 ImmutableMap 的使用。一种方法是使用接口。正如您所指出的那样使用包装器是不必要的,因此不推荐。返回 Map 是不可取的,因为它具有误导性。所以看起来如果目标是封装,那么接口是最好的选择。
          • 返回不扩展(或实现)Map 接口的东西的缺点是,如果不强制转换它,就不能将它传递给任何期望Map 的 API 函数。这包括其他类型地图的各种复制构造函数。除此之外,如果可变性只是被界面“隐藏”了,你的用户总是可以通过强制转换和破坏你的代码来解决这个问题。
          猜你喜欢
          • 1970-01-01
          • 2011-05-21
          • 2018-01-02
          • 1970-01-01
          • 2011-10-21
          • 2017-08-08
          • 1970-01-01
          • 2019-10-31
          • 1970-01-01
          相关资源
          最近更新 更多