How to copy HashMap (not shallow copy) in Java
Asked Answered
I

7

74

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?

Impair answered 2/2, 2015 at 23:7 Comment(1)
Using cloning library saved the day for me! github.com/kostaskougios/cloningTeen
T
39

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.

Testosterone answered 2/2, 2015 at 23:16 Comment(3)
In the for loop declaration, shoudn't there be original.entrySet() instead of copy.entrySet() ?Impair
@user3394494 Yes, you're right. I guess that's what happens when I type the code quickly. Modified.Testosterone
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.Burtis
R
54

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())))
Rumba answered 2/2, 2015 at 23:25 Comment(6)
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? ThanksImpair
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.Rumba
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.Jordain
Wouldn't getValue() just be passing a reference though?Devereux
List.copyOf() returns a unmodifiable List. In case of copies should new ArrayList() be preferred.Alcaraz
Code snippet has syntax errors, ArrayList is a raw type, etc.Kerstinkerwin
T
39

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.

Testosterone answered 2/2, 2015 at 23:16 Comment(3)
In the for loop declaration, shoudn't there be original.entrySet() instead of copy.entrySet() ?Impair
@user3394494 Yes, you're right. I guess that's what happens when I type the code quickly. Modified.Testosterone
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.Burtis
C
26

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.

Convolute answered 6/9, 2017 at 12:2 Comment(6)
I guess that the downvotes were about the performance penalty of this technique, but IMO this is a legit answer, despite its performance costs.Geotropism
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<String, Object> mapCopy = mapper.readValue(mapper.writeValueAsString(originalMap), new TypeReference<Map<String, Object>>() {})Exorcism
This is useful in cases where your data is really deeply referenced.Forgot
This is a real deep copy, guarantees that none of your embedded objects will point to the same referenceJocelin
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).Colorblind
This solution has a problem because Gson will serializes keys using the toString method. For example, toJson for Map<String, Map<Long,Object>> will create a JSON string that has string keys, not numeric. And then the serialization will lead to having Map<String, Map<String,Object>>.Igor
Y
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{}
Yarmouth answered 20/5, 2021 at 12:0 Comment(0)
P
2

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.

Piscator answered 2/2, 2015 at 23:15 Comment(0)
W
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);
Wiliness answered 13/8, 2022 at 13:32 Comment(3)
are you sure this won't give you a shallow copy?Igor
Yes I have tested it multiple times. It returns deep copy of HashMap.Wiliness
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: gist.github.com/horiaconstantin-cpi/… So, it's a shallow copy. On the other hand, SerializationUtils.clone will not change the values of the original map.Igor
P
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())));
Pretend answered 15/2, 2023 at 18:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.