返回ImmutableMap或Map更好吗?

AMT*_*AMT 88 java design-patterns immutable-collections

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

public Map<String, Integer> foo() {
  return new HashMap<String, Integer>();
}
Run Code Online (Sandbox Code Playgroud)

在考虑了一段时间之后,我决定在创建Map之后没有理由修改它.因此,我想返回一个ImmutableMap.

public Map<String, Integer> foo() {
  return ImmutableMap.of();
}
Run Code Online (Sandbox Code Playgroud)

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

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

Dic*_*ici 68

  • 如果你正在编写一个面向公众的API,并且不变性是你设计的一个重要方面,我肯定会通过让方法的名称清楚地表明返回的地图是不可变的或通过返回具体的类型来明确它.地图.在我看来,在javadoc中提到它是不够的.

    由于您显然正在使用Guava实现,我查看了doc并且它是一个抽象类,因此它确实为您提供了实际的具体类型的灵活性.

  • 如果您正在编写内部工具/库,那么返回普通文件就更容易接受了Map.人们会知道他们正在调用的代码的内部,或者至少可以轻松访问它.

我的结论是明确是好的,不要把事情搞得一团糟.

  • 如果您指的是Guava的`ImmutableMap`,那么Guava团队的建议是将`ImmutableMap`视为具有语义保证的接口,并且您应该直接返回该类型. (19认同)
  • 我同意路易斯,如果你打算返回一个不可变对象的地图,最好确保返回对象属于那种类型.如果由于某种原因您想要模糊它是一个不可变类型的事实,请定义您自己的接口.将它留作地图是误导性的. (3认同)

Syn*_*sso 36

你应该有ImmutableMap你的退货类型.Map包含ImmutableMap(例如put)的实现不支持的方法,并标记@deprecatedImmutableMap.

使用弃用的方法将导致编译器警告,并且大多数IDE将在人们尝试使用已弃用的方法时发出警告.

这个高级警告比运行时异常更可取,因为你的第一个暗示是出错了.

  • @TonioElGringo它没有违反任何东西,这些方法是可选的.就像在`List`中一样,不能保证`List :: add`是有效的.尝试`Arrays.asList("a","b").add("c");`.没有迹象表明它会在运行时失败,但确实如此.至少Guava给你一个提醒. (3认同)

tka*_*usl 16

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

你应该在javadocs中提到它.你知道,开发人员会阅读它们.

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

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

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

  • *"没有开发人员发布未经测试的代码."*你想打赌吗?如果这句话是真的,那么公共软件中的错误就会少得多(我推测).我甚至不确定"大多数开发人员......"都是真的.是的,那令人失望. (10认同)

Ren*_*ink 10

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

这是事实,但其他开发人员应该测试他们的代码并确保它被覆盖.

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

你还能做什么?!

你可以退货

  • 复制

    private Map<String, Integer> myMap;
    
    public Map<String, Integer> foo() {
      return new HashMap<String, Integer>(myMap);
    }
    
    Run Code Online (Sandbox Code Playgroud)

    如果您希望许多客户端修改地图并且只要地图只包含少量条目,则应使用此方法.

  • CopyOnWriteMap

    当你必须处理
    并发时,通常会使用写集合上的副本.但是这个概念也会在你的情况下帮助你,因为CopyOnWriteMap在一个变异操作上创建了一个内部数据结构的副本(例如add,remove).

    在这种情况下,您需要一个围绕地图的薄包装器,它将所有方法调用委托给底层映射,但变量操作除外.如果调用了一个mutative操作,它会创建一个底层映射的副本,所有进一步的调用都将被委托给该副本.

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

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

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


Ben*_*aro 8

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

  • 方法签名(包括返回类型)应该是自我记录的.评论就像客户服务:如果您的客户需要依赖它们,那么您的主要产品就是有缺陷的.
  • 无论是接口还是类,只有在扩展或实现它时才有意义.给定一个实例(对象),99%的客户端代码将无法知道或关心某些东西是接口还是类.我首先假设ImmutableMap是一个接口.只有在我点击链接后才知道它是一个类.


Jus*_*tin 5

这取决于班级本身.Guava ImmutableMap并不打算成为一个可变类的不可变视图.如果你的类是不可变的并且有一些基本上是a的结构ImmutableMap,那么创建返回类型ImmutableMap.但是,如果您的课程是可变的,请不要.如果你有这个:

public ImmutableMap<String, Integer> foo() {
    return ImmutableMap.copyOf(internalMap);
}
Run Code Online (Sandbox Code Playgroud)

番石榴每次都会复制地图.那很慢.但如果internalMap已经是一个ImmutableMap,那就完全没问题了.

如果你不限制你的班级返回ImmutableMap,那么你可以Collections.unmodifiableMap这样返回:

public Map<String, Integer> foo() {
    return Collections.unmodifiableMap(internalMap);
}
Run Code Online (Sandbox Code Playgroud)

请注意,这是一个不可变的地图视图.如果internalMap更改,则缓存副本也会如此Collections.unmodifiableMap(internalMap).然而,我仍然喜欢吸气剂.


Sol*_*ris 5

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

public Integer fooOf(String key) {
    return map.get(key);
}
Run Code Online (Sandbox Code Playgroud)

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

public Stream<Map.Entry<String, Integer>> foos() {
    map.entrySet().stream()
}
Run Code Online (Sandbox Code Playgroud)

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

public Optional<Integer> fooOf(String key) {
    return Optional.ofNullable(map.get(key));
}
Run Code Online (Sandbox Code Playgroud)