返回一个ImmutableMap还是一个Map更好?

91

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

public Map<String, Integer> foo() {
  return new HashMap<String, Integer>();
}
在考虑了一段时间后,我决定创建的这个 Map 没有修改的必要。因此,我想返回一个 ImmutableMap。 ImmutableMap
public Map<String, Integer> foo() {
  return ImmutableMap.of();
}

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

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


2
我怀疑有人最终会认为这个问题过于开放,无法争论。你能否提出更具体的问题,而不是“你认为呢?”和“可以吗……?”例如,“返回常规地图是否存在任何问题或考虑因素?”和“有哪些替代方案?” - ash
3
@immibis 意思是“哈哈,或者说列表呢?” - djechlin
@ash 我已按照你的建议编辑了问题。十分感谢。 - AMT
3
你真的想在你的 API 中嵌入 Guava 依赖吗?如果这样做,就无法更改它而不破坏向后兼容性。 - user2357112
1
我认为这个问题太粗略了,缺少许多重要的细节。例如,您是否已经将Guava作为(可见的)依赖项。即使您已经拥有它,代码片段也是伪代码,不能传达您实际想要实现的内容。您肯定不会编写return ImmutableMap.of(somethingElse)。相反,您将把ImmutableMap.of(somethingElse)存储为字段,并仅返回此字段。所有这些都影响了这里的设计。 - Marco13
显示剩余5条评论
9个回答

70
  • 如果您正在编写面向公众的API,并且不可变性是设计中重要的方面,我建议通过方法名称清晰地表示返回的映射将是不可变的,或者返回映射的具体类型来明确说明它。在Javadoc中提及它在我看来是不够的。

    由于显然您正在使用Guava实现,因此我查看了文档,它是一个抽象类,因此在实际具体类型方面给您一些灵活性。

  • 如果您正在编写内部工具/库,则只返回普通的Map就变得更加可接受。人们将了解他们调用的代码的内部情况,或者至少可以轻松访问。

我的结论是明确的好处多,不要留下任何机会。


1
一个额外的接口,一个额外的抽象类,以及一个继承这个抽象类的类。对于 OP 所询问的如此简单的问题,这太麻烦了,即方法应该返回 Map 接口类型还是 ImmutableMap 实现类型。 - fps
20
如果您正在引用Guava的ImmutableMap,Guava团队的建议是将ImmutableMap视为具有语义保证的接口,并直接返回该类型。 - Louis Wasserman
1
@LouisWasserman 嗯,没有看到问题已经提供了Guava实现的链接。那我就把代码片段删掉,谢谢。 - Dici
3
我同意Louis的观点,如果你打算返回一个不可变的地图对象,则最好确保返回对象是Immutable类型。如果因某种原因你想要隐藏它是不可变类型的事实,则可以定义自己的接口。将其保留为Map会导致误解。 - WSimpson
1
@LouisWasserman:自然地,Guava团队并不太关心那些不必要地与Guava耦合的API,因为这种耦合只会成为一个问题,当一个库停止使用Guava时。对于我们其他人来说,Guava是一个有价值设计的出色实现,但是我们随时准备跳船,一旦JDK赶上了它。;-) - ruakh
显示剩余6条评论

40
你的返回类型应该是ImmutableMapMap 包含的方法在 ImmutableMap 的实现中不受支持(例如put),并且被标记为 @deprecated
使用已弃用的方法会导致编译器警告,大多数 IDE 在尝试使用已弃用的方法时也会发出警告。
这种高级警告比将运行时异常作为第一个提示错误更可取。

1
在我看来,这是最明智的答案。Map接口是一个契约,而ImmutableMap显然违反了其中一个重要部分。在这种情况下使用Map作为返回类型是没有意义的。 - TonioElGringo
@TonioElGringo,那么Collections.immutableMap呢? - OrangeDog
@TonioElGringo 请注意,ImmutableMap 没有实现的方法在接口文档中被明确标记为可选项 https://docs.oracle.com/javase/7/docs/api/java/util/Map.html#put(K,%20V)。因此,我认为返回一个 Map(带有适当的 javadocs)仍然是有意义的。尽管如此,由于上述原因,我选择显式地返回 ImmutableMap。 - AMT
3
@TonioElGringo,这并不违反任何规定,这些方法都是可选的。就像在List中一样,没有保证List::add是有效的。尝试使用Arrays.asList("a", "b").add("c")。没有迹象表明它会在运行时失败,但实际上它确实会失败。至少Guava会提前向你发出警告。 - njzk2

14
另一方面,如果我将其保留为此状态,则其他开发人员可能会忽略此对象是不可变的事实。你应该在javadocs中提到这一点。开发人员会阅读它们的,你知道的。因此,我将无法实现不可变对象的主要目标,即通过最小化可以更改的对象数量来使代码更清晰。更糟糕的是,过一段时间,有人可能会尝试更改此对象,这将导致运行时错误(编译器不会发出警告)。请注意,只有Map本身是不可变的,其包含的对象不是。

10
“没有开发者会发布未经测试的代码。”你想打赌吗?如果这句话是真的,公共软件中就会有更少的错误(我假设)。我甚至不确定“大多数开发者…”是否正确。是的,这令人失望。 - Tom
1
好的,汤姆是正确的,我不确定你想说什么,但你实际写的内容显然是错误的。(另外:提醒一下要包容性别) - djechlin
@Tom 我猜你没注意到这个回答中的讽刺意味 - Alma Alma

10

如果我把它留这样,其他开发者可能会忽略这个对象是不可变的事实。

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

尽管如此,你还有两个解决选项:

  • 使用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

    虽然这并没有回答确切的问题,但仍值得考虑是否应该返回地图。如果地图是不可变的,则应基于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));
    }
    

    5

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

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

    Guava每次都会复制Map,这很慢。但是如果internalMap已经是一个ImmutableMap,那就完全没问题。

    如果您不限制类只返回ImmutableMap,那么可以使用Collections.unmodifiableMap代替返回:

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

    请注意,这是地图中不可变的视图。如果internalMap发生变化,则Collections.unmodifiableMap(internalMap)的缓存副本也会发生变化。但是,我仍然更喜欢将其用于获取器。

    1
    这些是很好的观点,但为了完整起见,我喜欢这个答案,它解释了unmodifiableMap只能被引用持有者修改 - 它是一个视图,支持映射的持有者可以修改支持映射,然后视图会发生变化。所以这是一个重要的考虑因素。 - davidbak
    正如davidback所评论的那样,使用Collections.unmodifiableMap时要非常小心。该方法返回一个视图,而不是创建一个单独的新映射对象。因此,引用原始映射的任何其他代码都可以修改它,然后假定不可修改的映射的持有者会从中学习到这个事实。我自己也被这个事实所困扰。我学会了要么返回Map的副本,要么使用Guava的ImmutableMap - Basil Bourque

    -1

    Immutable Map 是一种 Map 类型。因此,保留 Map 的返回类型是可以的。

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


    -1
    这可能是一个观点问题,但更好的想法是为地图类使用接口。该接口不需要明确说明它是不可变的,但如果您在接口中不公开父类的任何setter方法,则消息将是相同的。
    请看以下文章: andy gibson

    1
    不必要的包装被认为是有害的。 - djechlin
    你是正确的,我也不会在这里使用包装器,因为接口已经足够了。 - WSimpson
    我有一种感觉,如果这是正确的做法,那么ImmutableMap应该已经实现了一个ImmutableMapInterface,但从技术上讲,我不理解。 - djechlin
    重点不在于Guava开发人员的意图,而在于这位开发人员希望封装架构并不暴露ImmutableMap的使用事实。一种方法是使用接口。正如您所指出的,使用包装器是不必要的,因此不建议使用。返回Map是不可取的,因为它会产生误导。因此,如果目标是封装,则接口是最佳选择。 - WSimpson
    返回不扩展(或实现)“Map”接口的内容的缺点是,您无法将其传递给期望“Map”的任何API函数而不进行强制转换。这包括其他类型映射的所有复制构造函数。除此之外,如果可变性仅由接口“隐藏”,则用户始终可以通过强制转换来解决此问题,并以此方式破坏您的代码。 - Hulk

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接