Java ArrayList clone(): Shallow Copy vs Deep Copy

Java ArrayList clone() creates a shallow copy. Learn how it differs from a deep copy and how to deep copy a list with copy constructors and streams.

Shallow copy where two lists point to the same Ingredient objects, next to a deep copy where each list has its own Ingredient objects

The method ArrayList.clone() creates a shallow copy, which is a new list that holds references to the same element objects as the original. Adding or removing elements in an ArrayList clone does not affect the original, but changing the state of an element, for example with a setter, is visible through both lists. For fully independent lists, we create a deep copy, where every element is copied as well.

We copy lists when one part of the code must change data without affecting another part, such as editing a draft of a saved order, scaling a recipe, or returning internal data from a class without letting callers modify it.

The following example copies a list of mutable Ingredient objects (defined in section 1) with clone() and with a deep copy, and changes the first element of each copy.

ArrayList<Ingredient> recipe = new ArrayList<>(List.of(new Ingredient("flour", 200)));
ArrayList<Ingredient> shallow = (ArrayList<Ingredient>) recipe.clone();
List<Ingredient> deep = recipe.stream().map(Ingredient::new).toList();
shallow.getFirst().setGrams(400);                  // recipe = [flour:400]
deep.getFirst().setGrams(100);                     // recipe = [flour:400]

The change through shallow reached the original list, whereas the change through deep did not. We look at what clone() copies, compare it with the other shallow copy methods, and build deep copies with copy constructors, nested fields and Cloneable.

1. ArrayList clone() Creates a Shallow Copy

An ArrayList stores its elements in an internal array of references. The method clone() creates a new ArrayList and copies that array with Arrays.copyOf(), so the copy gets its own array, but each slot points to the same object as in the original.

Shallow copy where two lists point to the same Ingredient objects, next to a deep copy where each list has its own Ingredient objects
A shallow copy shares the element objects, a deep copy duplicates them

We model recipe ingredients with a small mutable class Ingredient. It has a copy constructor, which creates a new Ingredient with the same field values as another one.

class Ingredient {
    private final String name;
    private int grams;

    Ingredient(String name, int grams) {
        this.name = name;
        this.grams = grams;
    }

    Ingredient(Ingredient other) {
        this(other.name, other.grams);
    }

    int getGrams() { return grams; }

    void setGrams(int grams) { this.grams = grams; }

    @Override
    public String toString() { return name + ":" + grams; }
}

The two lists change independently in size, because each one has its own array. The elements are shared, so a setter call through either list changes the one object both lists point to.

ArrayList<Ingredient> recipe = new ArrayList<>(List.of(new Ingredient("flour", 200), new Ingredient("milk", 300)));
ArrayList<Ingredient> copy = (ArrayList<Ingredient>) recipe.clone();
copy.add(new Ingredient("salt", 5));               // recipe = [flour:200, milk:300]
copy.get(1).setGrams(250);                         // recipe = [flour:200, milk:250]
boolean sameObject = copy.get(0) == recipe.get(0); // true

The method clone() returns Object, so we need a cast, and the compiler warns about an unchecked cast. It is also defined on ArrayList, not on List, so it does not work on a variable of type List.

2. Other Ways to Create a Shallow Copy

The JDK offers several ways to make a shallow copy, and all of them copy references only. They differ in the type of the result and whether we can modify it.

MethodResultNotes
(ArrayList<T>) list.clone()Modifiable ArrayListNeeds a cast, works only on ArrayList
new ArrayList<>(list)Modifiable ArrayListWorks with any Collection, no cast
copy.addAll(list)Adds to an existing listKeeps the elements already in copy
List.copyOf(list)Unmodifiable listJava 10+, throws NullPointerException on null elements
list.stream().toList()Unmodifiable listJava 16+, allows null elements
List<String> spices = new ArrayList<>(List.of("salt", "pepper"));
List<String> editable = new ArrayList<>(spices);   // [salt, pepper]
List<String> readOnly = List.copyOf(spices);       // [salt, pepper]
boolean added = readOnly.add("cumin");             // UnsupportedOperationException

The constructor new ArrayList<>(list) is the better default over clone(). It needs no cast, accepts any collection and states the result type in the code.

The method Collections.copy(dest, src) is a different tool. It overwrites the first elements of an existing list and does not grow it, so a destination that is smaller than the source throws IndexOutOfBoundsException. It also copies references only, so it never creates a deep copy.

List<String> source = List.of("salt", "sugar");
List<String> empty = new ArrayList<>();
Collections.copy(empty, source);                   // IndexOutOfBoundsException: Source does not fit in dest
List<String> dest = new ArrayList<>(List.of("x", "y", "z"));
Collections.copy(dest, source);                    // dest = [salt, sugar, z]

3. When a Shallow Copy Is Enough

A shallow copy is all we need when the elements are immutable. Nobody can change a String, an Integer, a LocalDate or a record with immutable components, so sharing them between two lists is safe.

record Step(int number, String text) {}
List<Step> steps = new ArrayList<>(List.of(new Step(1, "mix"), new Step(2, "bake")));
List<Step> draft = new ArrayList<>(steps);
draft.set(1, new Step(2, "fry"));                  // steps = [Step[number=1, text=mix], Step[number=2, text=bake]]

To change a record in the copy, we replace it with a new record, as the set() call shows, and the original list keeps the old one. Designing elements as immutable classes is the simplest way to avoid deep copies, as described in making a Java class immutable.

4. Creating a Deep Copy with a Copy Constructor

For mutable elements, we create a deep copy by copying every element into a new object. A copy constructor or a static factory method such as Ingredient.copyOf() does the work for one element, and a stream applies it to the whole list.

A recipe app shows why. The stored recipe for two people is cached and shared by all users. When a user scales it to four people, the app doubles the grams in a copy, and a shallow copy would double the cached recipe for every other user too.

List<Ingredient> cached = List.of(new Ingredient("flour", 200), new Ingredient("milk", 300));
List<Ingredient> scaled = cached.stream()
        .map(Ingredient::new)
        .collect(Collectors.toCollection(ArrayList::new));
scaled.forEach(i -> i.setGrams(i.getGrams() * 2));  // scaled = [flour:400, milk:600]
String original = cached.toString();               // "[flour:200, milk:300]"

We use Collectors.toCollection(ArrayList::new) because the scaled list may change later. When the copy should be read-only, toList() returns an unmodifiable list.

5. Deep Copy of Nested Mutable Fields

A deep copy is only as deep as the copy constructor. When an element holds another mutable object, such as a list, the copy constructor must copy that field too, otherwise the inner list is still shared.

class Meal {
    private final String title;
    private final List<Ingredient> items;

    Meal(String title, List<Ingredient> items) {
        this.title = title;
        this.items = new ArrayList<>(items);
    }

    Meal(Meal other) {
        this.title = other.title;
        this.items = other.items.stream().map(Ingredient::new).collect(Collectors.toCollection(ArrayList::new));
    }

    List<Ingredient> items() { return items; }

    @Override
    public String toString() { return title + items; }
}
List<Meal> menu = List.of(new Meal("pancakes", List.of(new Ingredient("flour", 200))));
List<Meal> menuCopy = menu.stream().map(Meal::new).toList();
menuCopy.getFirst().items().getFirst().setGrams(50);
String before = menu.toString();                   // "[pancakes[flour:200]]"
String after = menuCopy.toString();                // "[pancakes[flour:50]]"

The same idea works for maps, which we cover in shallow and deep copies of a HashMap.

6. Deep Copy with clone() and Cloneable

Older code makes elements copyable by implementing Cloneable and overriding clone(). The method Object.clone() copies all fields one to one, so the class must still copy each mutable field by hand, as explained in the object cloning guide.

class Tray implements Cloneable {
    private int[] slots;

    Tray(int... slots) { this.slots = slots; }

    int slot(int i) { return slots[i]; }

    void fill(int i, int value) { slots[i] = value; }

    @Override
    public Tray clone() {
        try {
            Tray copy = (Tray) super.clone();
            copy.slots = slots.clone();
            return copy;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);
        }
    }
}
List<Tray> trays = List.of(new Tray(1, 2));
List<Tray> trayCopies = trays.stream().map(Tray::clone).toList();
trayCopies.getFirst().fill(0, 9);
int untouched = trays.getFirst().slot(0);          // 1

The Cloneable approach has real drawbacks. The field slots cannot be final, because clone() must reassign it, and a subclass that forgets to override clone() shares its mutable fields again. Copy constructors have neither problem, so we prefer them in new code.

A third option serializes the list and reads it back, for example with Java serialization or a JSON library such as Jackson. It copies any depth without copy constructors, but it is slower, because it writes and parses every object, and it needs every class to be serializable, as shown in deep cloning with serialization.

7. ArrayList Copy FAQs

Copying lists raises the same few questions again and again, mostly about what each method copies.

7.1. Is ArrayList clone() a deep copy?

No. The method clone() copies the internal array of references, so both lists point to the same element objects. Only adding, removing or replacing elements is independent.

7.2. Does List.copyOf() or new ArrayList<>(list) make a deep copy?

No. Both copy references only, like clone(). The method List.copyOf() also makes the list unmodifiable, but the elements inside can still change if they are mutable.

7.3. Should we use clone() or the copy constructor for an ArrayList?

We prefer the constructor new ArrayList<>(list). Both copy the internal array, but the constructor needs no cast, causes no unchecked warning and works when the variable type is List or any other Collection.

7.4. How do we copy an ArrayList into an existing ArrayList?

We call target.addAll(source) to append the elements, or target.clear() before it to replace the content. The method Collections.copy() works only when the target already has at least as many elements as the source.

8. Conclusion

The method ArrayList.clone() and the other shallow copy methods give the new list its own array but share the element objects. For immutable elements such as strings and records, a shallow copy with new ArrayList<>(list) or List.copyOf() is all we need.

For mutable elements, we build a deep copy with a copy constructor and stream().map(Type::new), and we copy nested mutable fields inside that constructor. The Cloneable approach still works, but copy constructors are simpler and safer for new code.

9. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. I should say in the following line, you not need to have .clone() at the end.

    while(iterator.hasNext()){
    employeeListClone.add((Employee) iterator.next().clone());
    }

  2. Hi Lokesh,

    Running the Employee class in section 3.1 for deep clone, the section in which you are making a clone of dob (this.dob.clone()) will the clone method be visible .

Comments are closed.

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.