Deep Copy a HashMap in Java and When a Shallow Copy Is Enough

Learn to deep copy a HashMap in Java with copy constructors, streams and serialization, and compare shallow copies with new HashMap, clone() and copyOf().

Shallow copy of a HashMap sharing the same list objects versus a deep copy with its own lists

A shallow copy of a HashMap is a new map that holds the same key and value objects as the original, and a deep copy also creates new copies of the mutable values, so a change to a value in one map does not show up in the other. The copy constructor new HashMap<>(original) makes a shallow copy, and to deep copy a HashMap, we write code that copies each value.

We copy a map when one part of an app must change entries without affecting another part, for example a draft of user settings that the user can still cancel, or a test that needs fresh data for each run.

The following example copies a map of playlists, where each value is a mutable list of songs, once shallowly and once deeply.

Map<String, List<String>> playlists = new HashMap<>();
playlists.put("gym", new ArrayList<>(List.of("warm-up")));

Map<String, List<String>> shallow = new HashMap<>(playlists);
Map<String, List<String>> deep = new HashMap<>();
playlists.forEach((name, songs) -> deep.put(name, new ArrayList<>(songs)));

playlists.get("gym").add("sprint");
List<String> inShallow = shallow.get("gym");          // [warm-up, sprint] (shared list)
List<String> inDeep = deep.get("gym");                // [warm-up] (own list)

Adding a song through the original map changes the shallow copy too, because both maps point to the same ArrayList. We start with the difference between the two kinds of copy, go through every way to make a shallow copy, and finish with deep copies for list values, mutable objects and serialization.

1. Shallow Copy vs Deep Copy of a HashMap

A HashMap stores references to its keys and values, not the objects themselves. A shallow copy creates a new map structure with new entries, but every entry points to the same key and value objects as the original. A deep copy goes one level further and creates new value objects too.

Shallow copy of a HashMap sharing the same list objects versus a deep copy with its own lists
A shallow copy shares the value objects with the original, and a deep copy gives each map its own values

In both cases, adding or removing an entry in the copy never affects the original, because each map has its own table of entries. The difference shows only when we change a value object in place, for example by calling add() on a list or a setter on an object.

ChangeShallow copyDeep copy
put() or remove() on the copyOriginal unchangedOriginal unchanged
Replace a value with put(key, newValue)Original unchangedOriginal unchanged
Change a mutable value in place (list.add(), a setter)Visible in both mapsVisible only in the changed map
Values are immutable (String, Integer, records of immutable fields)Safe, nothing to share wronglyNot needed

When the values are immutable, a shallow copy is all we need, and a deep copy only costs time and memory. A good HashMap key is always immutable, so no copy method copies keys.

2. Creating a Shallow Copy of a HashMap

The JDK has several ways to make a shallow copy. They differ in the type they return, in whether the copy is modifiable and in how they handle null, which matters more than the copying itself.

2.1. Using the Copy Constructor

The constructor HashMap(Map) creates a new HashMap with the same mappings as any source map. It is the recommended way to copy, because it is type-safe, needs no cast, and accepts a TreeMap or a Map.of() map as the source.

Map<String, Integer> plays = new HashMap<>(Map.of("intro", 3, "outro", 1));
Map<String, Integer> copy = new HashMap<>(plays);
copy.put("intro", 10);
Integer original = plays.get("intro");                // 3
Integer changed = copy.get("intro");                  // 10

To keep the order of a LinkedHashMap or a TreeMap, we call the constructor of the same class, such as new LinkedHashMap<>(original), because a HashMap copy has no defined order.

2.2. Using HashMap.clone()

The method HashMap.clone() returns a shallow copy, but its return type is Object, so we need an unchecked cast. It also exists only on the HashMap class, not on the Map interface, so it does not work on a variable declared as Map. The copy constructor does the same job without either problem, so we use clone() only in old code that already relies on it.

HashMap<String, Integer> plays = new HashMap<>(Map.of("intro", 3));
@SuppressWarnings("unchecked")
HashMap<String, Integer> cloned = (HashMap<String, Integer>) plays.clone();
boolean equal = cloned.equals(plays);                 // true
boolean sameObject = cloned == plays;                 // false

2.3. Using putAll()

The putAll() method copies all mappings into a map that already exists. We use it when the target map has a specific type or capacity, or when we copy several sources into one map, and later sources overwrite the values of keys that already exist.

Map<String, Integer> plays = Map.of("intro", 3, "outro", 1);
Map<String, Integer> target = new HashMap<>(Map.of("bonus", 7));
target.putAll(plays);
int size = target.size();                             // 3

2.4. Using Map.copyOf() for an Unmodifiable Copy

Since Java 10, Map.copyOf() returns an unmodifiable shallow copy. Changes to the original do not affect it, and any change to the copy throws an UnsupportedOperationException. It throws a NullPointerException when the source contains a null key or value.

Map<String, Integer> plays = new HashMap<>(Map.of("intro", 3));
Map<String, Integer> frozen = Map.copyOf(plays);
plays.put("outro", 1);
int frozenSize = frozen.size();                       // 1 (not affected)
Integer blocked = frozen.put("bonus", 7);             // UnsupportedOperationException

The method Collections.unmodifiableMap() looks similar but is not a copy. It returns a read-only view, so later changes to the original show through it. The immutable vs unmodifiable maps article covers that difference in detail.

2.5. Using a Stream

A stream over entrySet() with Collectors.toMap() copies the map and lets us filter or transform entries on the way. The collector throws a NullPointerException for a null value, which a HashMap allows, so the copy constructor is safer for a plain copy.

Map<String, Integer> plays = Map.of("intro", 3, "outro", 1, "skit", 0);
Map<String, Integer> played = plays.entrySet().stream().filter(e -> e.getValue() > 0).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
int playedSize = played.size();                       // 2

All five ways share the value objects with the original, and they differ in the result type, modifiability and null handling.

MethodResult typeModifiablenull keys or values
new HashMap<>(map)HashMapYesAllowed
hashMap.clone()Object, needs a castYesAllowed
target.putAll(map)Type of targetYesDepends on target
Map.copyOf(map)Unmodifiable MapNoNullPointerException
stream().collect(toMap(…))HashMap in practice, not guaranteedYesnull values throw NullPointerException

3. How to Deep Copy a HashMap

A settings screen in a music app loads the user’s preferences into a map whose values are lists, such as the genres for each mood. The screen edits a copy, and the original stays untouched until the user clicks Save. A shallow copy breaks that, because adding a genre to the copy’s list also adds it to the original, so the screen needs a deep copy.

3.1. Copying a Map With List Values

For a map of lists, a deep copy creates a new list for each value. The stream version passes a copy function as the value mapper of toMap(), and the loop version does the same with forEach().

Map<String, List<String>> moods = new HashMap<>();
moods.put("calm", new ArrayList<>(List.of("jazz", "ambient")));

Map<String, List<String>> viaStream = moods.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> new ArrayList<>(e.getValue())));
Map<String, List<String>> viaLoop = new HashMap<>();
moods.forEach((mood, genres) -> viaLoop.put(mood, new ArrayList<>(genres)));

viaStream.get("calm").add("lofi");
List<String> original = moods.get("calm");            // [jazz, ambient]
List<String> edited = viaStream.get("calm");          // [jazz, ambient, lofi]

The new ArrayList copies the list, but not the elements inside it. Here the elements are String objects, which are immutable, so the copy is fully independent. A list of mutable objects needs the elements copied too.

3.2. Copying a Map With Mutable Value Objects

When the values are our own mutable classes, each class needs a way to copy itself. A copy constructor is the clearest option, because it is plain Java, it works with final fields and it lists every field that is copied.

class Track {
    private final String title;
    private int plays;

    Track(String title, int plays) {
        this.title = title;
        this.plays = plays;
    }

    Track(Track other) {
        this(other.title, other.plays);
    }

    void play() {
        plays++;
    }

    int plays() {
        return plays;
    }
}
Map<String, Track> library = new HashMap<>();
library.put("t1", new Track("Intro", 3));

Map<String, Track> shallow = new HashMap<>(library);
Map<String, Track> deep = library.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> new Track(e.getValue())));

library.get("t1").play();
int shallowPlays = shallow.get("t1").plays();         // 4 (same Track object)
int deepPlays = deep.get("t1").plays();               // 3 (own Track object)

A record whose fields are immutable never needs this step. Records have no setters, so a shallow copy of a Map<String, TrackRecord> is already safe. For nested maps, such as a map of maps, the copy function copies the inner map the same way, one level per nesting level.

3.3. Deep Copy With Serialization

Serialization writes the whole object graph to bytes and reads it back as new objects, so it copies every level at once. The method SerializationUtils.clone() from Apache Commons Lang 3.21.0 does that in one call, and the in-memory serialization article shows the plain Java version.

HashMap<String, ArrayList<String>> moods = new HashMap<>();
moods.put("calm", new ArrayList<>(List.of("jazz")));
HashMap<String, ArrayList<String>> copy = SerializationUtils.clone(moods);
copy.get("calm").add("lofi");
List<String> original = moods.get("calm");            // [jazz]

Serialization has real costs, so we use it only for deep graphs that have no copy methods.

  • Every class in the graph must implement Serializable, otherwise the call throws a SerializationException.
  • It is much slower than a copy constructor, because it converts the whole graph to bytes and back.
  • Fields marked transient are not copied and come back as null or zero.
  • A JSON round trip with Gson or Jackson works without Serializable, but it needs the exact target type, because Gson, for example, reads every number in a Map<String, Object> back as a Double.

4. HashMap Copy FAQs

Developers who copy maps in Java often ask which copy methods are deep and which one to prefer.

4.1. Is new HashMap<>(map) a Deep Copy?

No. The copy constructor creates new entries, but the keys and values are the same objects as in the original. It behaves like a deep copy only when the values are immutable, such as String, Integer or records with immutable fields.

4.2. Does HashMap.clone() Copy the Keys and Values?

No. The keys and values themselves are not cloned. The clone has its own entry table, so adding or removing entries does not affect the original, but both maps share every key and value object.

4.3. Is Map.copyOf() a Deep Copy?

No. It is an unmodifiable shallow copy. We cannot add, remove or replace entries in it, but a mutable value inside it, such as an ArrayList, can still change, and the change is visible through the original map.

Map<String, List<String>> moods = new HashMap<>();
moods.put("calm", new ArrayList<>(List.of("jazz")));
Map<String, List<String>> frozen = Map.copyOf(moods);
frozen.get("calm").add("lofi");
List<String> original = moods.get("calm");            // [jazz, lofi]

4.4. Which Is Faster, clone() or the Copy Constructor?

For a HashMap, both copy every entry once, so they take time proportional to the size of the map. The difference between them is too small to choose on, so we pick the copy constructor for its type safety.

5. Conclusion

A shallow copy of a HashMap gives us a new map that shares its value objects with the original. The copy constructor is the best way to make one, Map.copyOf() gives an unmodifiable version, and clone() works but needs a cast. When the values are immutable, a shallow copy is all we need.

A deep copy is for maps with mutable values, such as lists or our own classes. We copy each value with a copy constructor or new ArrayList<>(…) in a stream or a loop, and keep serialization for large object graphs that have no copy methods.

6. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.