返回 ImmutableMap 还是 Map 更好?

假设我正在编写一个返回 地图的方法,例如:

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

经过一段时间的思考,我已经决定没有理由在创建这个 Map 之后修改它。因此,我想返回一个 不变地图

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

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

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

18687 次浏览

On the other hand, if I'll leave it like this, other developers might miss the fact that this object is immutable.

You should mention that in the javadocs. Developers do read them, you know.

Thus, I won't achieve a major goal of immutable objects; to make the code more clear by minimizing the number of objects that can change. Even worst, after a while, someone might try to change this object, and this will result in a runtime error (The compiler will not warn about it).

No developer publishes his code untested. And when he does test it, he gets an Exception thrown where he not only sees the reason but also the file and line where he tried to write to an immutable map.

Do note though, only the Map itself will be immutable, not the objects it contains.

Immutable Map is a type of Map. So leaving the return type of Map is okay.

To ensure that the users do not modify the returned object, the documentation of the method can describe the characteristics of the returned object.

  • If you are writing a public-facing API and that immutability is an important aspect of your design, I would definitely make it explicit either by having the name of the method clearly denotes that the returned map will be immutable or by returning the concrete type of the map. Mentioning it in the javadoc is not enough in my opinion.

    Since you're apparently using the Guava implementation, I looked at the doc and it's an abstract class so it does give you a bit of flexibility on the actual, concrete type.

  • If you are writing an internal tool/library, it becomes much more acceptable to just return a plain Map. People will know about the internals of the code they are calling or at least will have easy access to it.

My conclusion would be that explicit is good, don't leave things to chance.

This is arguably a matter of opinion, but the better idea here is to use an interface for the map class. This interface doesn't need to explicitly say that it is immutable, but the message will be the same if you don't expose any of the setter methods of the parent class in the interface.

Take a look at the following article:

andy gibson

Definitely return an ImmutableMap, justification being:

  • The method signature (including return type) should be self-documenting. Comments are like customer service: if your clients need to rely on them, then your primary product is defective.
  • Whether something is an interface or a class is only relevant when extending or implementing it. Given an instance (object), 99% of the time client code will not know or care whether something is an interface or a class. I assumed at first that ImmutableMap was an interface. Only after I clicked the link did I realize it is a class.

You should have ImmutableMap as your return type. Map contains methods that are not supported by the implementation of ImmutableMap (e.g. put) and are marked @deprecated in ImmutableMap.

Using deprecated methods will result in a compiler warning & most IDEs will warn when people attempt to use the deprecated methods.

This advanced warning is preferable to having runtime exceptions as your first hint that something is wrong.

if I'll leave it like this, other developers might miss the fact that this object is immutable

That's true, but other developers should test their code and ensure that it is covered.

Nevertheless you have 2 more options to solve this:

  • Use Javadoc

    @return a immutable map
    
  • Chose a descriptive method name

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

    For a concrete use case you can even name the methods better. E.g.

    public Map<String, Integer> getUnmodifiableCountByWords()
    

What else can you do?!

You can return a

  • copy

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

    This approach should be used if you expect that a lot of clients will modify the map and as long as the map only contains a few entries.

  • CopyOnWriteMap

    copy on write collections are usually used when you have to deal with
    concurrency. But the concept will also help you in your situation, since a CopyOnWriteMap creates a copy of the internal data structure on a mutative operation (e.g. add, remove).

    In this case you need a thin wrapper around your map that delegates all method invocations to the underlying map, except the mutative operations. If a mutative operation is invoked it creates a copy of the underlying map and all further invocations will be delegated to this copy.

    This approach should be used if you expect that some clients will modify the map.

    Sadly java does not have such a CopyOnWriteMap. But you might find a third party or implement it by yourself.

At last you should keep in mind that the elements in your map might still be mutable.

It depends on the class itself. Guava's ImmutableMap isn't intended to be an immutable view into a mutable class. If your class is immutable and has some structure that is basically an ImmutableMap, then make the return type ImmutableMap. However, if your class is mutable, don't. If you have this:

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

Guava will copy the map every time. That's slow. But if internalMap was already an ImmutableMap, then it's totally fine.

If you don't restrict your class to returning ImmutableMap, then you could instead return Collections.unmodifiableMap like so:

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

Note that this is an immutable view into the map. If internalMap changes, so will a cached copy of Collections.unmodifiableMap(internalMap). I still prefer it for getters, however.

This is not answering the exact question, but it is still worth considering whether a map should be returned at all. If the map is immutable, then the primary method to be provided is based on the get(key):

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

This makes the API much tighter. If a map is actually required, this could be left up to the client of the API by providing a stream of entries:

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

Then the client can make its own immutable or mutable map as it needs, or add the entries to its own map. If the client needs to know if the value exists, optional can be returned instead:

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