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.

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.
| Method | Result | Notes |
|---|---|---|
| (ArrayList<T>) list.clone() | Modifiable ArrayList | Needs a cast, works only on ArrayList |
| new ArrayList<>(list) | Modifiable ArrayList | Works with any Collection, no cast |
| copy.addAll(list) | Adds to an existing list | Keeps the elements already in copy |
| List.copyOf(list) | Unmodifiable list | Java 10+, throws NullPointerException on null elements |
| list.stream().toList() | Unmodifiable list | Java 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 !!
Collections.copy doesn’t work for me.
Please elaborate.
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());
}
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 .