Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java unmodifiableMap can be replaced with 'Map.copyOf' call

Tags:

java

I'm new to Java and I recently learnt that somtimes it's important to deepcopy a Collection and make an unmodifiable view of it so that the data inside remains safe and unchanged. When I try to practice this(unmodifiableMap2), I get a warning from IDEA that

unmodifiableMap Can be replaced with 'Map.copyOf' call

That's weird for me because I think unmodifiableMap is not only a copy of the underlying map. Besides, when I try to create the same unmodifiableMap in another way(unmodifiableMap1), the warning doesn't pop up!

How should I understand this behavior of IDEA ?

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class test {
    public static void main(String[] args) {
        Map<Integer, Integer> map = new HashMap<>();
        map.put(1,1);
        map.put(2,2);

        Map<Integer, Integer> map1 = new HashMap<>(map);

        Map<Integer, Integer> unmodifiableMap1 = Collections.unmodifiableMap(map1);

        Map<Integer, Integer> unmodifiableMap2 = Collections.unmodifiableMap(new HashMap<>(map););

    }
}

enter image description here

like image 567
Rafael Avatar asked Jan 10 '21 21:01

Rafael


2 Answers

Map.copyOf() makes a copy of the given Map instance, but it requires that no value in the map is null. Usually, this is the case, but it is not a strict requirement for a Map in general.

java.util.Collections.unmodifiableMap() just wraps a reference to the given Map instance. This means that the receiver is unable to modify the map, but modifications to the original map (that one that was the argument to unmodifiableMap()) are visible to the receiver.

Assuming we have two threads, one iterates over the unmodifiable map, while the other modifies the original one. As a result, you may get a ConcurrentModificationException for an operation on the unmodifiable map … not funny to debug that thing!

This cannot happen with the copy created by Map.copyOf(). But this has a price: with a copy, you need two times the amount of memory for the map (roughly). For really large maps, this may cause memory shortages up to an OutOfMemoryError. Also not fun to debug!

In addition, just wrapping the existing map is presumably much faster than copying it.

So there is no best solution in general, but for most scenarios, I have a preference for using Map.copyOf() when I need an unmodifiable map.


The sample in the question did not wrap the original Map instance, but it makes a copy before wrapping it (either in a line of its own, or on the fly). This eliminates the potential problem with the 'under-the-hood' modification, but may bring the memory issue.

From my experience so far, Map.copyOf( map ) looks to be more efficient than Collections.unmodifiableMap( new HashMap( map ) ).


By the way: Map.copyOf() returns a map that resembles a HashMap; when you copy a TreeMap with it, the sort order gets lost, while the wrapping with unmodifiableMap() keeps the underlying Map implementation and therefore also the sort order. So when this is important, you can use Collections.unmodifiableMap( new TreeMap( map ) ), while Map.copyOf() does not work here.

like image 195
tquadrat Avatar answered Sep 21 '22 15:09

tquadrat


An unmodifiable map using an existing reference to a map is perfectly fine, and there are many reasons you might want to do this.

Consider this class:

class Foo {
    private final Map<String, String> fooMap = new HashMap<>();

    // some methods which mutate the map

    public Map<String, String> getMap() {
        return Collections.unmodifiableMap(fooMap);
    }
}

What this class does is provide a read-only view of the map it encapsulates. The class can be sure that clients who consume the map cannot alter it, they can just see its contents. They will also be able to see any updates to the entries if they keep hold of the reference for some time.

If we had tried to expose a read-only view by copying the map, it would take time and memory to perform the copy and the client would not see any changes because both maps are then distinct instances - the source and the copy.

However in the case of this:

Collections.unmodifiableMap(new HashMap<>(map));

You are first copying the map into a new hash map and then passing that copy into Collections.unmodifiableMap. The result is effectively constant. You do not have a reference to the copy you created with new HashMap<>(map), and nor can you get one*.

If what you want is a constant map, then Map.copyOf is a more concise way of achieving that, so IntelliJ suggests you should use that instead.

In the first case, since the reference to the map already exists, IntelliJ cannot make the same inference about your intent so it gives no such suggestion.

You can see the IntelliJ ticket for this feature if you like, though it doesn't explain why the two are essentially equivalent, just that they are.


* well, you probably could via reflection, but IntelliJ is assuming that you won't

like image 41
Michael Avatar answered Sep 18 '22 15:09

Michael