69

I need to make a copy of HashMap<Integer, List<MySpecialClass> > but when I change something in the copy I want the original to stay the same. i.e when I remove something from the List<MySpecialClass> from the copy it stays in the List<MySpecialClass> in the original.

If I understand it correctly, these two methods create just shallow copy which is not what I want:

mapCopy = new HashMap<>(originalMap);
mapCopy = (HashMap) originalMap.clone();

Am I right?

Is there a better way to do it than just iterate through all the keys and all the list items and copy it manually?

Mathis
  • 691
  • 1
  • 6
  • 8

7 Answers7

47

This does need iteration unfortunately. But it's pretty trivial with Java 8 streams:

mapCopy = map.entrySet().stream()
    .collect(Collectors.toMap(e -> e.getKey(), e -> List.copyOf(e.getValue())))
sprinter
  • 27,148
  • 6
  • 47
  • 78
  • 2
    I'm not that familiar with streams. When I tried your solution it gives an error : non-static method getKey() cannot be referenced from a static context, same with getValue(), how to edit it so it works? Thanks – Mathis Feb 03 '15 at 00:01
  • 1
    Unfortunately I'm not at a compiler at the moment so I haven't had a chance to test it. Try this and if it doesn't work I'll remove the answer until I've had a chance to test it. – sprinter Feb 03 '15 at 00:49
  • 1
    In a general context, your solution works. The non-static method error seems to be related to the static context of the method of the OP´s problem. – Ben Mar 21 '17 at 15:02
  • 3
    Wouldn't `getValue()` just be passing a reference though? – Adam Hughes Aug 28 '17 at 12:08
  • `List.copyOf()` returns a unmodifiable List. In case of copies should `new ArrayList()` be preferred. – akop Oct 29 '19 at 12:39
  • Code snippet has syntax errors, `ArrayList` is a raw type, etc. – Koenigsberg Aug 05 '20 at 09:35
40

You're right that a shallow copy won't meet your requirements. It will have copies of the Lists from your original map, but those Lists will refer to the same List objects, so that a modification to a List from one HashMap will appear in the corresponding List from the other HashMap.

There is no deep copying supplied for a HashMap in Java, so you will still have to loop through all of the entries and put them in the new HashMap. But you should also make a copy of the List each time also. Something like this:

public static HashMap<Integer, List<MySpecialClass>> copy(
    HashMap<Integer, List<MySpecialClass>> original)
{
    HashMap<Integer, List<MySpecialClass>> copy = new HashMap<Integer, List<MySpecialClass>>();
    for (Map.Entry<Integer, List<MySpecialClass>> entry : original.entrySet())
    {
        copy.put(entry.getKey(),
           // Or whatever List implementation you'd like here.
           new ArrayList<MySpecialClass>(entry.getValue()));
    }
    return copy;
}

If you want to modify your individual MySpecialClass objects, and have the changes not be reflected in the Lists of your copied HashMap, then you will need to make new copies of them too.

rgettman
  • 176,041
  • 30
  • 275
  • 357
  • 1
    In the for loop declaration, shoudn't there be original.entrySet() instead of copy.entrySet() ? – Mathis Feb 02 '15 at 23:56
  • 1
    @user3394494 Yes, you're right. I guess that's what happens when I type the code quickly. Modified. – rgettman Feb 02 '15 at 23:57
  • Be aware that this solution will not work as expected, if multiple `keys` in your `Map` reference the same `value` object! Using above method the clone will have new objects for each `value`. Actually, I came here looking for a better option than using a second `Map` from original `values` to cloned `values` during the clone process in such cases. – Johnson Jul 12 '19 at 08:59
25

Serialize to json and deserialize afterwards:

Map<String, Object> originalMap = new HashMap<>();
String json = new Gson().toJson(originalMap);
Map<String, Object> mapCopy = new Gson().fromJson(
    json, new TypeToken<Map<String, Object>>() {}.getType());

For special classes you might need to write a custom deserializer.

neu242
  • 15,796
  • 20
  • 79
  • 114
  • 7
    I guess that the downvotes were about the performance penalty of this technique, but IMO this is a legit answer, despite its performance costs. – Ron Klein Nov 11 '18 at 06:51
  • Perfect for my test code (where I don't care about performance), of course you can use any json serializer such as `com.fasterxml.jackson.databind.ObjectMapper` : `Map mapCopy = mapper.readValue(mapper.writeValueAsString(originalMap), new TypeReference>() {})` – james.cookie Nov 30 '18 at 16:10
  • 1
    This is useful in cases where your data is really deeply referenced. – Ajeetkumar Jan 16 '20 at 11:33
  • This is a real deep copy, guarantees that none of your embedded objects will point to the same reference – raspacorp Apr 07 '21 at 21:04
  • Didn't put a downvote but imho this is pretty bad solution as you have to use external dependency and also you're doing conversion which is not needed (if we say performance is not needed). – horvoje Jul 16 '22 at 08:23
  • This solution has a problem because Gson will serializes keys using the toString method. For example, toJson for Map> will create a JSON string that has string keys, not numeric. And then the serialization will lead to having Map>. – treaz Sep 15 '22 at 17:30
10

Just use Apache Commons Lang - SerializationUtils.clone();

Something like these:

    Map<String, List<MySpecialClass>> originalMap = new HashMap();
    HashMap<String, List<MySpecialClass>> deepCopy = SerializationUtils.clone(new HashMap<>(originalMap));

Also make sure MySpecialClass implements Serializable interface.

public class MySpecialClass implements Serializable{}
2

I am not sure about Java 8, but if you are using Java version 11 or above, then the below code will give you a deep copy of Map. If data inside it is of primitively type

HashMap<String, Integer> map = new HashMap<>();
HashMap<String, Integer> deepCopiedMap = new HashMap<>(map);
vishal patel
  • 125
  • 1
  • 7
  • 1
    are you sure this won't give you a shallow copy? – treaz Sep 15 '22 at 17:23
  • 1
    Yes I have tested it multiple times. It returns deep copy of HashMap. – vishal patel Sep 16 '22 at 06:04
  • 6
    Indeed it will work for Integer (and probably for other primitive wrappers), but it won't work for other classes. Here's an example of what I mean: https://gist.github.com/horiaconstantin-cpi/e65db961a360fa637cbcd7dc2ab761a1 So, it's a shallow copy. On the other hand, SerializationUtils.clone will not change the values of the original map. – treaz Sep 17 '22 at 07:48
1

You are making a copy of the HashMap itself, so changing the HashMap copy will not change the original HashMap (i.e. adding or removing entries), but because the objects you have stored are not primitive types, the List that you retrieve with a given key will be the same whether retrieved from the first or the second Map.

Thus, there is still only ONE copy of that list, referenced by both maps: changing the List changes it no matter which reference you use to access it.

If you want the actual List to be a separate copy, you will have to do as you said: iterate over the entry set of the HashMap and create a copy of each List manually, adding it to the new map as you go.

If there is a better way than that, I don't know what it is.

Brian
  • 336
  • 1
  • 9
0

This answer was good in case you were asking for an immutable copy, but the question indicates you are going to change the list values.

So in this case you need to create a new list for each of the map values as follows:

mapCopy = map.entrySet().stream()
            .collect(Collectors.toMap(Map.Entry::getKey, e -> new ArrayList<>(e.getValue())));
Kayvan Tehrani
  • 3,070
  • 2
  • 32
  • 46