How To Clone Java Collections with Streams

By Adam McQuistan in Java  06/22/2019 Comment

java collection stream cloning

Introduction

This How To features using streams in the Java programming language to make "safe" deep clones of collections of objects.

The cloning (aka, copying) of objects in Java has been an arduous task since its inception. Partly due to the way the `Object#clone` method was designed then oddly paired with the `Cloneable` interface and also complicated by the way reference types, or objects, work in concert with collections in OOP based languages. However, I will not go into this topic as its already been covered elsewhere. See Java's Object Methods: clone() featured on StackAbuse or, take a look at Joshua Bloch's venerable Java best practices book: Effective Java 3rd Ed

Shallow and Deep Cloning in Java

There are two types of copying when it comes to collections of objects in Java, shallow and deep. Both have valid merits for their existence as well as differences. For demonstration purposes I will be using a minimal SuperHero class in this article which is shown below.

// SuperHero.java

package com.thecodinginterface;

class SuperHero {
  int stars;
  String name;

  // all fields constructor
  SuperHero(int stars, String name) {
    this.stars = stars;
    this.name = name;
  }

  // copy constructor
  SuperHero(SuperHero hero) {
    this(hero.stars, hero.name);
  }

  @Override
  public String toString() {
    return "<SuperHero: " + stars + ", " + name + ">";
  }
}

Below I create a few instances of the characters from the Disney/Pixar animated movie, The Incredibles and use them to make a java.util.List named originalHeros.

var violet = new SuperHero(0, "Violet");
var dash = new SuperHero(0, "Dash");
var mrIncredible = new SuperHero(0, "Mr. Incredible");
var elastigirl = new SuperHero(0, "Elastigirl");

// Original source list
List<SuperHero> originalHeros = Arrays.asList(
    new SuperHero[]{ violet, dash, mrIncredible, elastigirl }
);

Next up I make a shallow copy of this originalHeros collection and name it shallowCopy. A common way I like to make a shallow copy is by passing the list of objects I want to copy to the constructor of an ArrayList class like so.

List<SuperHero> shallowCopy = new ArrayList<>(originalHeros);

This creates a new collection which means I can modify things like the order and size of the shallowCopy list and those changes won't be reflected in the originalHeros list. Say for example, I swap violet with mrIncredible then add a new SuperHero "Jack-Jack".

shallowCopy.set(0, mrIncredible);
shallowCopy.set(2, violet);
var jackJack = new SuperHero(0, "Jack-Jack");
shallowCopy.add(jackJack);

Then if I were to display them side by side this is what I will see.

Original                            Shallow Copy                        
------------------------------      ------------------------------      
<SuperHero: 0, Violet>              <SuperHero: 0, Mr. Incredible>      
<SuperHero: 0, Dash>                <SuperHero: 0, Dash>                
<SuperHero: 0, Mr. Incredible>      <SuperHero: 0, Violet>              
<SuperHero: 0, Elastigirl>          <SuperHero: 0, Elastigirl>          
null                                <SuperHero: 0, Jack-Jack>  

However, what may not be completely obvious at first is that if you alter the contents of an object in just one of the lists that change, or changes, will be reflected in both copies of the collection. For example, if I add 2 stars to the mrIncredible object instance variable that change will be seen in both collections.

mrIncredible.stars += 2;

Note: I'm not using encapsulation (ie, getters and setters) on purpose for brevity.  This would not be following best practices for a real applicaiton.

Output below.

Original                            Shallow Copy                        
------------------------------      ------------------------------      
<SuperHero: 0, Violet>              <SuperHero: 2, Mr. Incredible>      
<SuperHero: 0, Dash>                <SuperHero: 0, Dash>                
<SuperHero: 2, Mr. Incredible>      <SuperHero: 0, Violet>              
<SuperHero: 0, Elastigirl>          <SuperHero: 0, Elastigirl>          
null                                <SuperHero: 0, Jack-Jack>   

Similarly, if I access the shallowCopy collection's item at index 3 (ie, the elastigirl object instance) and add three stars and display both collections the change is reflected in both lists again.

shallowCopy.get(3).stars += 3;

Displayed side by side again.

Original                            Shallow Copy                        
------------------------------      ------------------------------      
<SuperHero: 0, Violet>              <SuperHero: 2, Mr. Incredible>      
<SuperHero: 0, Dash>                <SuperHero: 0, Dash>                
<SuperHero: 2, Mr. Incredible>      <SuperHero: 0, Violet>              
<SuperHero: 3, Elastigirl>          <SuperHero: 3, Elastigirl>          
null                                <SuperHero: 0, Jack-Jack>

Many readers familiar with the Java language (or perhaps OOP in general) will not be surprised by the above behavior but, I wanted to be sure that there was a equal understanding as I move into the concept of deep copying next. In the case where you want to copy a collection of objects and not have any side effects from altering an object in one collection being propagating to other copies a deep copy is required.

In essence, what making a deep copy comes down to is creating a new collection where each object in the copy is also a fresh new object with its own memory space. There are two ways to do this: (i) utilize a properly implemented override of the `Object#clone` method and, (ii) explicitly instantiate a new instance then assign the data to be copied from the original instance to the new instance. The second method is the preferable way but, I refer the reader to the sources referenced in the "To Learn More About this Topic" section a more thorough explanation.

Using Java Streams for Making Clones of Collections

There are many ways to populate a collection instance (ie, List) with new object instances. Below is an example of using the stream API utilizing a lambda function in conjunction with the map stream method.

List<SuperHero> deepCopy = originalHeros.stream()
                              .map(hero -> new SuperHero(hero))
                              .collect(Collectors.toList());

Now if I modify an original instance variable like adding another 2 stars to elastigirl...

elastigirl.stars += 2;

Then displaying the originalHeros, shallowCopy, and deepCopy collection items side by side only the originalHeros and shallowCopy instance exhibit the changes.

Original                            Shallow Copy                        Deep Copy                           
------------------------------      ------------------------------      ------------------------------      
<SuperHero: 0, Violet>              <SuperHero: 2, Mr. Incredible>      <SuperHero: 0, Violet>              
<SuperHero: 0, Dash>                <SuperHero: 0, Dash>                <SuperHero: 0, Dash>                
<SuperHero: 2, Mr. Incredible>      <SuperHero: 0, Violet>              <SuperHero: 2, Mr. Incredible>      
<SuperHero: 5, Elastigirl>          <SuperHero: 5, Elastigirl>          <SuperHero: 3, Elastigirl>          
null                                <SuperHero: 0, Jack-Jack>           null                 

For comparison sake I will now show a more traditional, or perhaps legacy, method for creating a deep copy of a collection. Both will accomplish the same task but, in my opinion, the stream implementation is a bit more succinct and has a modern functional appeal.

List<SuperHero> legacyDeepCopy = new ArrayList<>();
for (SuperHero originalHero : originalHeros) {
    legacyDeepCopy.add(new SuperHero(originalHero));
}

However, I can actually make the stream example slightly more succinct by using a method reference to the new operator of the SuperHero class as follows.

List<SuperHero> deepCopy2 = originalHeros.stream()
                              .map(SuperHero::new)
                              .collect(Collectors.toList());

An entire runnable program, main method included, is listed below for reference (minus the SuperHero class from above).

// App.java
package com.thecodinginterface;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.StringJoiner;
import java.util.stream.Collectors;

public class App {
    public static void main(String[] args) {
      var violet = new SuperHero(0, "Violet");
      var dash = new SuperHero(0, "Dash");
      var mrIncredible = new SuperHero(0, "Mr. Incredible");
      var elastigirl = new SuperHero(0, "Elastigirl");

      // Original source list
      List<SuperHero> originalHeros = Arrays.asList(
        new SuperHero[]{ violet, dash, mrIncredible, elastigirl }
      );

      List<SuperHero> shallowCopy = new ArrayList<>(originalHeros);

      shallowCopy.set(0, mrIncredible);
      shallowCopy.set(2, violet);
      var jackJack = new SuperHero(0, "Jack-Jack");
      shallowCopy.add(jackJack);

      display("Original", "Shallow Copy");
      display("-".repeat(30), "-".repeat(30));

      for (int i = 0; i < shallowCopy.size(); i++) {
        SuperHero heroFromOrig = getHeroOrNull(originalHeros, i);
        SuperHero heroFromShallowCopy = getHeroOrNull(shallowCopy, i);
        display(heroFromOrig, heroFromShallowCopy);
      }

      mrIncredible.stars += 2;

      display("Original", "Shallow Copy");
      display("-".repeat(30), "-".repeat(30));

      for (int i = 0; i < shallowCopy.size(); i++) {
        SuperHero heroFromOrig = getHeroOrNull(originalHeros, i);
        SuperHero heroFromShallowCopy = getHeroOrNull(shallowCopy, i);
        display(heroFromOrig, heroFromShallowCopy);
      }

      shallowCopy.get(3).stars += 3;

      display("Original", "Shallow Copy");
      display("-".repeat(30), "-".repeat(30));

      for (int i = 0; i < shallowCopy.size(); i++) {
        SuperHero heroFromOrig = getHeroOrNull(originalHeros, i);
        SuperHero heroFromShallowCopy = getHeroOrNull(shallowCopy, i);
        display(heroFromOrig, heroFromShallowCopy);
      }

      List<SuperHero> deepCopy = originalHeros.stream()
                                    .map(hero -> new SuperHero(hero))
                                    .collect(Collectors.toList());

      elastigirl.stars += 2;

      display("Original", "Shallow Copy", "Deep Copy");
      display("-".repeat(30), "-".repeat(30), "-".repeat(30));

      for (int i = 0; i < shallowCopy.size(); i++) {
        SuperHero heroFromOrig = getHeroOrNull(originalHeros, i);
        SuperHero heroFromShallowCopy = getHeroOrNull(shallowCopy, i);
        SuperHero heroFromDeepCopy = getHeroOrNull(deepCopy, i);
        display(heroFromOrig, heroFromShallowCopy, heroFromDeepCopy);
      }

      List<SuperHero> legacyDeepCopy = new ArrayList<>();
      for (SuperHero originalHero : originalHeros) {
        legacyDeepCopy.add(new SuperHero(originalHero));
      }

      display("Original", "Shallow Copy", "Legacy Deep Copy");
      display("-".repeat(30), "-".repeat(30), "-".repeat(30));

      for (int i = 0; i < shallowCopy.size(); i++) {
        SuperHero heroFromOrig = getHeroOrNull(originalHeros, i);
        SuperHero heroFromShallowCopy = getHeroOrNull(shallowCopy, i);
        SuperHero heroFromDeepCopy = getHeroOrNull(legacyDeepCopy, i);
        display(heroFromOrig, heroFromShallowCopy, heroFromDeepCopy);
      }

      List<SuperHero> deepCopy2 = originalHeros.stream()
                                      .map(SuperHero::new)
                                      .collect(Collectors.toList());


      display("Original", "Shallow Copy", "Deep Copy2");
      display("-".repeat(30), "-".repeat(30), "-".repeat(30));

      for (int i = 0; i < shallowCopy.size(); i++) {
        SuperHero heroFromOrig = getHeroOrNull(originalHeros, i);
        SuperHero heroFromShallowCopy = getHeroOrNull(shallowCopy, i);
        SuperHero heroFromDeepCopy = getHeroOrNull(deepCopy2, i);
        display(heroFromOrig, heroFromShallowCopy, heroFromDeepCopy);
      }
    }

    public static void display(Object... items) {
      StringJoiner fmt = new StringJoiner(" ");
      for (int i = 0; i < items.length; i++) { 
        fmt.add("%-35s");
      }

      fmt.add("\n");
      System.out.printf(fmt.toString(), items);
    }

    public static SuperHero getHeroOrNull(List<SuperHero> heros, int i) {
      return i < heros.size() ? heros.get(i) : null;
    }
}

Resources for Learning More About Java

Conclusion

At theCodingInterface.com, "How To..." articles are short code snippet-like content pieces that aregenerally accompanied with only brief explanations focusing on the use of code to convey a specific topic. This How To focussed on an application of the java.util.stream API to deep clone collections of objects. For more detailed explanations please have a look at the referenced material linked within the Learn More About this Topic section.

As always, thanks for reading and don't be shy about commenting or critiquing below.

Share with friends and colleagues

[[ likes ]] likes

Community favorites for Java

theCodingInterface