返回ImmutableMap还是Map更好?


90

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

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

考虑了一段时间后,我决定创建该地图后就没有理由对其进行修改。因此,我想返回一个ImmutableMap

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

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

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


2
我怀疑有人最终会将此问题标记为对争论过于开放。您可以输入更具体的问题,而不是“ WDYT”吗?然后“可以...吗?” 例如,“返回常规地图是否有任何问题或考虑?” 和“存在哪些替代方案?”

如果以后您决定应该实际返回可变映射该怎么办?
user253751 '16

3
@immibis大声笑,或与此相关的清单?
djechlin '16

3
您是否真的想将Guava依赖项烘烤到您的API中?在不破坏向后兼容性的情况下,您将无法进行更改。
user2357112支持Monica

1
我认为这个问题过于笼统,许多重要细节都遗漏了。例如,是否已经将番石榴作为(可见)依赖项。即使您已经拥有它,这些代码片段也是伪代码,并且不能传达您实际想要实现的目标。你当然不会return ImmutableMap.of(somethingElse)。取而代之的是,您将存储ImmutableMap.of(somethingElse)为字段,并仅返回此字段。所有这些都会影响此处的设计。
Marco13,2013年

Answers:


70
  • 如果您正在编写面向公众的API,并且不变性是设计的重要方面,那么我绝对可以通过使方法的名称清楚地表示返回的地图将是不变的,或者通过返回的具体类型来明确指出它的地图。在我看来,在javadoc中提及它还不够。

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

  • 如果您正在编写内部工具/库,则只返回一个简单的库就可以接受。 Map。人们会知道他们正在调用的代码的内部结构,或者至少可以轻松访问它们。

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


1
一个额外的接口,一个额外的抽象类,以及一个扩展此抽象类的类。对于OP这样简单的事情,这太麻烦了,这是方法应该返回Map接口类型还是ImmutableMap实现类型。
fps

20
如果您指的是Guava ImmutableMap,则Guava团队的建议是考虑ImmutableMap具有语义保证的接口,并应直接返回该类型。
Louis Wasserman

1
@LouisWasserman mm,没有看到问题在提供与Guava实现的链接。然后,我将删除该代码段,谢谢您
Dici

3
我同意Louis的观点,如果您打算返回不可变对象的映射,请确保返回对象最好是那种类型的。如果由于某种原因您想掩盖它是不可变类型的事实,请定义您自己的接口。将其保留为Map具有误导性。
WSimpson

1
@WSimpson我相信这就是我的回答。路易斯在谈论我添加的一些代码段,但我删除了。
Dici 2016年

38

您应该将其ImmutableMap作为返回类型。Map包含未通过实施支持的方法ImmutableMap(例如put),并标注@deprecatedImmutableMap

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

此高级警告比运行时异常更可取,因为它是您第一个暗示错误的提示。


1
我认为这是最明智的答案。Map接口是一个合同,ImmutableMap显然违反了其中的重要部分。在这种情况下,将Map用作返回类型没有意义。
TonioElGringo

@TonioElGringo那Collections.immutableMap又如何呢?
OrangeDog

@TonioElGringo请注意,接口文档docs.oracle.com/javase/7/docs/api/java/util/…中未将ImmutableMap未实现的方法明确标记为可选。因此,我认为返回Map(使用适当的javadocs)仍然有意义。即使如此,由于上述原因,我还是选择显式返回ImmutableMap。
AMT

3
@TonioElGringo它没有违反任何内容,那些方法是可选的。像in中一样List,无法保证List::add有效。尝试Arrays.asList("a", "b").add("c");。没有迹象表明它将在运行时失败,但是会失败。至少番石榴会让您警惕。
njzk2

14

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

您应该在javadocs中提到它。您知道,开发人员确实会阅读它们。

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

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

不过要注意,只有Map自身是不可变的,而不是其中包含的对象。


10
“没有开发人员会发布未经测试的代码。” 你想打赌吗?如果这句话是对的,那么公共软件中的错误(我的推测)将少得多。我什至不能确定“大多数开发人员……”是真的。是的,这令人失望。
汤姆(Tom)

1
好的,汤姆是对的,我不确定您要说的是什么,但是您实际写的内容显然是错误的。(还:推动性别包容)
djechlin

@汤姆,我想你错过了这个答案的讽刺
意味

10

如果我将其保留下来,其他开发人员可能会错过这个对象是不可变的事实

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

不过,您还有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

    当必须处理
    并发时,通常使用写时复制收集。但是,由于CopyOnWriteMap在可变操作(例如,添加,删除)上创建了内部数据结构的副本,因此该概念也将为您提供帮助。

    在这种情况下,您需要在映射周围使用一个精简包装,将所有方法调用委托给基础映射(可变操作除外)。如果调用了变异操作,它将创建基础映射的副本,所有其他调用都将委派给该副本。

    如果您希望某些客户端将修改地图,则应使用此方法。

    可悲的是Java没有这样的CopyOnWriteMap。但是您可能会找到第三方或自己实施。

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


8

绝对返回一个ImmutableMap,理由是:

  • 方法签名(包括返回类型)应该是自记录的。评论就像客户服务:如果您的客户需要依靠它们,则您的主要产品有缺陷。
  • 什么是接口还是类仅在扩展或实现它时才相关。给定一个实例(对象),在99%的时间内,客户端代码将不知道或不在乎某个对象是接口还是类。首先,我假设ImmutableMap是一个接口。只有单击链接后,我才意识到这是一堂课。

5

这取决于类本身。番石榴的ImmutableMap目的并不是成为可变类的一成不变的观点。如果您的类是不可变的,并且具有某种基本上是an的结构ImmutableMap,则将其设为return类型ImmutableMap。但是,如果您的班级是可变的,那就不要。如果您有这个:

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

番石榴每次都会复制地图。太慢了 但是如果internalMap已经是ImmutableMap,那就完全可以了。

如果不将类限制为ImmutableMapreturn,则可以Collections.unmodifiableMap这样返回:

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

请注意,这是地图的不变视图。如果internalMap更改,的缓存副本也将更改Collections.unmodifiableMap(internalMap)。但是,对于吸气剂我还是比较喜欢。


1
这些都是很好的要点,但是为了完整起见,我喜欢这个答案,它解释了unmodifiableMap,该引用仅由引用的持有者无法修改-这是一个视图,支持图的持有者可以修改支持图,然后视图改变。因此,这是一个重要的考虑因素。
davidbak

@如davidback所评论,在使用时要非常小心Collections.unmodifiableMap。该方法将视图返回到原始Map,而不是创建单独的不同的新Map对象。因此,引用原始Map的任何其他代码都可以对其进行修改,然后假定无法修改的Map的持有者将学习到可以在其下方进行修改的方法。我自己被这个事实所困扰。我学会了返回地图副本或使用Guava ImmutableMap
罗勒·布尔克

5

这并不能回答确切的问题,但是仍然值得考虑是否应该返回地图。如果映射是不可变的,则要提供的主要方法基于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));
}

-1

不可变地图是地图的一种。因此,保留Map的返回类型是可以的。

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


-1

可以说这是一个见解,但是更好的主意是为Map类使用接口。该接口不需要明确地说出它是不可变的,但是如果您在接口中不公开父类的任何setter方法,则消息将是相同的。

看一下以下文章:

安迪·吉布森


1
不必要的包装纸被认为是有害的。
djechlin '16

没错,我也不会在这里使用包装器,因为接口就足够了。
WSimpson

我有一种感觉,如果这是正确的做法,那么ImmutableMap已经可以实现ImmutableMapInterface了,但是我对此并不了解。
djechlin '16

关键不是Guava开发人员的意图,而是这个开发人员想要封装体系结构而不公开使用ImmutableMap的事实。一种实现方法是使用接口。如您所指出的,使用包装器是不必要的,因此不建议使用。不希望返回Map,因为这会产生误导。因此,似乎目标是封装,那么接口是最佳选择。
WSimpson

返回不扩展(或实现)Map接口的内容的缺点是,如果不进行转换,则无法将其传递给期望a的任何API函数Map。这包括其他类型地图的各种复制构造函数。除此之外,如果仅通过界面“隐藏”了可变性,则用户始终可以通过强制转换和破坏代码来解决此问题。
绿巨人
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.