91

Let's say I'm writing a method that should return a Map. For instance:

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

After thinking about it for a while, I've decided that there is no reason to modify this Map once it is created. Thus, I would like to return an ImmutableMap.

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

Should I leave the return type as a generic Map, or should I specify that I'm returning an ImmutableMap ?

From one side, this is exactly why interfaces were created for; to hide the implementation details.
On the other hand, if I'll leave it like this, other developers might miss the fact that this object is immutable. 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).

AMT
  • 1,016
  • 1
  • 7
  • 13
  • 2
    I suspect someone will eventually tag this question as too open for arguments. Can you put in more specific questions instead of "WDYT?" and "is it OK...?" For example, "are there any issues or considerations with returning a regular map?" and "what alternatives exist?" – ash Jun 29 '16 at 00:32
  • What if later you decide it should actually return a mutable map? – user253751 Jun 29 '16 at 03:41
  • 3
    @immibis lol, or a List for that matter? – djechlin Jun 29 '16 at 05:45
  • @ash I've edited the question as you suggested. 10x. – AMT Jun 29 '16 at 06:22
  • 3
    Do you really want to bake a Guava dependency into your API? You won't be able to change that without breaking backward compatibility. – user2357112 Jun 29 '16 at 17:18
  • I've actually used it just for the example. Anyway, at work, we are using Guava a lot, so it is not a concern. – AMT Jun 29 '16 at 17:54
  • Just like for `List`, people are (wrongly) often expecting the returned object to have the full implementation. And in fact, there is no way, except for a small indication in the javadoc, to guess that calling `put` may simply crash. I tend to return specific objects as much as possible, and add indication in the method's javadoc regarding the expectations one can have of your returned type. – njzk2 Jun 29 '16 at 19:05
  • Typically, the "optional method" indication breaks the liskov substitution principle. You can't use a `Map` and call `put` on it at all, since some implementation simply crash. – njzk2 Jun 29 '16 at 19:09
  • So technically, `Map` should be considered as Immutable already. – njzk2 Jun 29 '16 at 19:22
  • 1
    I think that the question is too sketchy, and many important details are missing. For example, whether you already have Guava as a (visible) dependency anyhow. Even if you already have it, the code snippets are pesudocode, and do not convey what you actually want to achieve. You would certainly **not** write `return ImmutableMap.of(somethingElse)`. Instead, you would store the `ImmutableMap.of(somethingElse)` as a field, and return only this field. All this influences the design here. – Marco13 Jun 29 '16 at 21:41

9 Answers9

70
  • 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.

gsamaras
  • 71,951
  • 46
  • 188
  • 305
Dici
  • 25,226
  • 7
  • 41
  • 82
  • 1
    One extra interface, one extra abstract class, and one class that extends this abstract class. This is too much hassle for such a simple thing as the OP is asking, which is if the method shoud return `Map` interface type or `ImmutableMap` implementation type. – fps Jun 29 '16 at 00:32
  • Mmm it's true that subclassing this would be cumbersome. Composition might be better than an abstract class here, but you would lose specific types such as `SortedMap` etc. Depending on how important immutability is in the API I also proposed cheap things such as an explicit name for the method – Dici Jun 29 '16 at 00:39
  • I'm upvoting even though I disagree because you are proposing alternatives and everything is fundamented. IMHO, the simpler the better, and a method that returns a map shouldn't force the users to not modify it. If they want to not modify it or make it thread/safe, then they can wrap the returned map with whatever implementation they want. If you return an immutable map but the user wants to add an entry (because it is useful for him), then he would need to wrap it just to add one entry. I see no point in an immutable map – fps Jun 29 '16 at 00:44
  • It's just a semantic thing... If you don't want the users to repaint the `House` in blue, you won't add the `House.paint(Color)` method. For a `Map`, we could actually return a completely separate, fully immutable type (that's what Scala does). However, if you want to allow them using utils written for general maps it's better to subclass `Map` but that comes to the cost of runtime errors not detectable at compile-time. Helping with the naming and/or the typing will just make the API nicer to work with, in my opinion. – Dici Jun 29 '16 at 00:50
  • IMHO, immutable has stronger semantic than simply unmodifiable. If you return an immutable object, the user can expect that this object will never change -- either by the user (unmodifiable in this case) or by the object that returned it. Here you are creating an interface that promises it's immutable, but implementations can break this promise very easily and how are you going to enforce that? – xiaofeng.li Jun 29 '16 at 00:55
  • 20
    If you are referring to Guava's `ImmutableMap`, the Guava team's advice is to consider `ImmutableMap` an interface with semantic guarantees, and that you should return that type directly. – Louis Wasserman Jun 29 '16 at 01:06
  • 1
    @LouisWasserman mm, didn't see the question was giving a link to the Guava implementation. I'll remove the snippet then, thank you – Dici Jun 29 '16 at 01:16
  • 3
    I agree with Louis, if you intention is to return a map that is an Immutable object, the ensure that return object is of that type preferably. If for some reason you want to obscure the fact that it is an immutable type, define your own interface. Leaving it as Map is misleading. – WSimpson Jun 29 '16 at 11:37
  • 1
    @WSimpson I believe that's what my answer is saying. Louis was talking about some snippet I had added, but I removed it. – Dici Jun 29 '16 at 12:04
  • 1
    @LouisWasserman: Naturally, the Guava team is not too concerned about APIs that couple themselves to Guava unnecessarily, since that coupling only becomes an issue for a library that ceases to use Guava. For the rest of us, Guava is an excellent *implementation* of a valuable design, but we're all ready to jump ship the instant the JDK catches up. ;-) – ruakh Jun 29 '16 at 23:37
  • @LouisWasserman Is a mistake then that Guava's `Sets.cartesianProduct` returns `Set>` with a note that both the set and the list are immutable? Per your advice it should return `ImmutableSet>` – Jirka Oct 06 '18 at 20:22
40

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.

Synesso
  • 37,610
  • 35
  • 136
  • 207
  • 1
    This is in my opinion the most sensible answer. The Map interface is a contract, and ImmutableMap is clearly breaching an important part of it. Using Map as a return type in this case does not make sense. – TonioElGringo Jun 29 '16 at 08:55
  • @TonioElGringo what about `Collections.immutableMap` then? – OrangeDog Jun 29 '16 at 15:16
  • @TonioElGringo please note that the methods that the ImmutableMap don't implement are explicitly marked as optional in the interface's documentation https://docs.oracle.com/javase/7/docs/api/java/util/Map.html#put(K,%20V). Thus, I think it can still make sense to return a Map (with proper javadocs). Even though, I choose to return the ImmutableMap explicitly, because of the reasons discussed above. – AMT Jun 29 '16 at 18:18
  • 3
    @TonioElGringo it is not breaching anything, those methods are optional. Like in `List`, there is no guarantee that `List::add` is valid. Try `Arrays.asList("a", "b").add("c");`. There is no indication that it will fail at runtime, yet it does. At least Guava gives you a heads up. – njzk2 Jun 29 '16 at 19:03
14

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.

tkausl
  • 13,686
  • 2
  • 33
  • 50
  • 10
    *"No developer publishes his code untested."* Do you want to bet on this? There would be far less (my presumption) bugs in public software, if this sentence would be true. I wouldn't even be sure of "most developers ..." would be true. And yes, that's disappointing. – Tom Jun 29 '16 at 01:37
  • 1
    Okay, Tom is right, I'm not sure what you're trying to say, but what you actually wrote is clearly false. (Also: nudge for gender inclusiveness) – djechlin Jun 29 '16 at 05:47
  • @Tom I guess you missed the sarcasm in this answer – Alma Alma Jul 01 '16 at 00:13
10

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.

René Link
  • 48,224
  • 13
  • 108
  • 140
8

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.
Benito Ciaro
  • 1,718
  • 12
  • 25
5

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.

Justin
  • 24,288
  • 12
  • 92
  • 142
  • 1
    These are good points, but for completeness, I like [this answer](http://stackoverflow.com/a/22636750/751579) which explains that the `unmodifiableMap` is only unmodifiable by the holder of the reference - it's a view and the holder of the backing map can modify the backing map and then the view changes. So that's an important consideration. – davidbak Jun 29 '16 at 17:56
  • @As davidback commented, be very careful about using [`Collections.unmodifiableMap`](http://docs.oracle.com/javase/7/docs/api/java/util/Collections.html#unmodifiableMap(java.util.Map)). That method returns a *view* onto the original Map rather than creating a separate distinct new map object. So any other code referencing the original Map can modify it, and then the holder of the supposedly unmodifiable map learns the hard-way that the map can indeed be modified from under them. I have been bit by that fact myself. I learned to either return a copy of the Map or use Guava `ImmutableMap`. – Basil Bourque Jun 30 '16 at 00:50
5

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));
}
Solubris
  • 3,603
  • 2
  • 22
  • 37
-1

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.

toro
  • 194
  • 5
-1

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

WSimpson
  • 460
  • 4
  • 7
  • 1
    Unnecessary wrappers considered harmful. – djechlin Jun 29 '16 at 05:50
  • You are right, I wouldn't use a wrapper here either, as the interface is sufficient. – WSimpson Jun 29 '16 at 11:32
  • I have a feeling if this were the right thing to do then ImmutableMap would implement an ImmutableMapInterface already, but I don't understand this technically. – djechlin Jun 29 '16 at 13:43
  • The point is not what the Guava developers intended, it is the fact that this developer would like to encapsulate the architecture and not expose the use of ImmutableMap. One way to do this would be to use an interface. Using a wrapper as you pointed out isn't necessary and therefore not recommended. Returning Map is undesirable as it is misleading. So it seems that if the goal is encapsulation, then an Interface is the best option. – WSimpson Jun 29 '16 at 17:50
  • The disadvantage of returning something that does not extend (or implement) the `Map` interface is that you can not pass it to any API function expecting a `Map` without casting it. This includes all kinds of copy-constructors of other types of maps. In addition to that, if the mutability is only "hidden" by the interface, your users can always work around this by casting and break your code that way. – Hulk Jun 30 '16 at 07:25