Análisis de Estrategias de Copia de Colecciones en Java: Profunda vs. Superficial

La manipulación de colecciones de objetos en Java a menudo implica la necesidad de crear copias. Sin embargo, no todas las "copias" se comportan de la misma manera, especialmente cuando se trata de objetos anidados. Es crucial entender la diferencia entre una copia superficial (shallow copy) y una copia profunda (deep copy) para evitar efectos secundarios inesperados en la lógica de nuestra aplicación.

Una copia superficial duplica la estructura de la colección, pero los elementos dentro de la colección copiada son referencias a los mismos objetos que se encuentran en la colección original. Si los objetos contenidos son mutables, los cambios en una colección afectarán a la otra.

Una copia profunda, en cambio, crea una colección completamente nueva y duplica recursivamente todos los objetos contenidos en ella, incluidos los objetos anidados. Esto asegura que la colección copiada sea completamente independiente de la original.

A continuación, exploraremos seis métodos comunes para copiar colecciones en Java y evaluaremos si realizan una copia superficial, una copia profunda parcial o una copia profunda completa.

Clases de Apoyo para la Experimentación

Para ilustrar el comportamiento de copia, utilizaremos dos clases sencillas con una relación de anidación. Es importante que estas clases implementen Serializable si planeamos utilizar métodos de serialización/deserialización para la copia profunda.

import java.io.Serializable;
import java.util.List;

public class ItemDetails implements Serializable {
    private String itemId;
    private List<ItemProperty> itemProperties;

    // Constructores, getters y setters
    public ItemDetails() {}

    public String getItemId() { return itemId; }
    public void setItemId(String itemId) { this.itemId = itemId; }

    public List<ItemProperty> getItemProperties() { return itemProperties; }
    public void setItemProperties(List<ItemProperty> itemProperties) { this.itemProperties = itemProperties; }

    @Override
    public String toString() {
        return "ItemDetails{itemId='" + itemId + "', itemProperties=" + itemProperties + '}';
    }
}

public class ItemProperty implements Serializable {
    private String propertyName;

    // Constructores, getters y setters
    public ItemProperty() {}

    public String getPropertyName() { return propertyName; }
    public void setPropertyName(String propertyName) { this.propertyName = propertyName; }

    @Override
    public String toString() {
        return "ItemProperty{propertyName='" + propertyName + "'}";
    }
}

Métodos Compartidos para Pruebas

Utilizaremos los siguientes métodos auxiliaers para generar datos, modificar la colección original y verificar el resultado de la copia.

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

// Importaciones para Spring BeanUtils, Hutool, Fastjson
import org.springframework.beans.BeanUtils;
import cn.hutool.core.bean.BeanUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;

public class CollectionCopyUtils {

    /**
     * Genera una lista inicial de objetos ItemDetails para la prueba.
     * @return Una lista de ItemDetails.
     */
    static List<ItemDetails> createInitialData() {
        List<ItemDetails> dataList = new ArrayList<>();
        ItemDetails item = new ItemDetails();
        item.setItemId("P001");

        List<ItemProperty> properties = new ArrayList<>();
        ItemProperty prop = new ItemProperty();
        prop.setPropertyName("Color: Red");
        properties.add(prop);
        item.setItemProperties(properties);
        dataList.add(item);
        return dataList;
    }

    /**
     * Modifica los valores en la lista original para observar el comportamiento de la copia.
     * Se modifica una propiedad de la capa superior y una propiedad del objeto anidado.
     * @param originalList La lista de ItemDetails original a modificar.
     */
    static void modifyOriginalData(List<ItemDetails> originalList) {
        // Modificar una propiedad de la capa superior
        originalList.get(0).setItemId("P002_MODIFICADO");
        // Modificar una propiedad del objeto anidado
        originalList.get(0).getItemProperties().get(0).setPropertyName("Size: Large_MODIFICADO");
    }

    /**
     * Evalúa si la lista copiada refleja una copia profunda o superficial.
     * Verifica si los valores originales permanecen intactos en la copia después de modificar el original.
     * @param copiedList La lista de ItemDetails copiada a evaluar.
     */
    static void evaluateCopyResult(List<ItemDetails> copiedList) {
        boolean firstLevelDeep = copiedList.get(0).getItemId().equals("P001");
        boolean secondLevelDeep = copiedList.get(0).getItemProperties().get(0).getPropertyName().equals("Color: Red");
        System.out.println("¿Es la primera capa una copia profunda?: " + firstLevelDeep);
        System.out.println("¿Es la segunda capa (anidada) una copia profunda?: " + secondLevelDeep);
    }
}

1. Constructor de ArrayList (Copia Superficial)

El constructor new ArrayList<>(collection) crea una nueva instancia de ArrayList y copia las referencias de los elementos de la colección original. Esto significa que si los objetos son mutables, cualquier cambio en ellos a través de la lista original se reflejará en la copia.

Código de Prueba

public class CollectionCopyTests {
    // ... métodos auxiliares createInitialData, modifyOriginalData, evaluateCopyResult ...

    public static void testConstructorArrayList() {
        System.out.println("\n--- Prueba: Constructor de ArrayList ---");
        List<ItemDetails> originalList = CollectionCopyUtils.createInitialData();
        List<ItemDetails> copiedList = new ArrayList<>(originalList); // Copia superficial
        CollectionCopyUtils.modifyOriginalData(originalList);
        CollectionCopyUtils.evaluateCopyResult(copiedList);
    }

    public static void main(String[] args) {
        testConstructorArrayList();
        // ... otras pruebas ...
    }
}

Resultado Esperado/Obtenido

--- Prueba: Constructor de ArrayList ---
¿Es la primera capa una copia profunda?: false
¿Es la segunda capa (anidada) una copia profunda?: false

Ambas capas se ven afectadas, confirmando una copia superficial. Los objetos en la lista copiada son las mismas instancias que en la original.

2. Método List.addAll() (Copia Superficial)

El método addAll(), similar al constructor de ArrayList, también realiza una copia superficial. Copia las referencias de los elementos de la colección de origen a la colección de destino.

Código de Prueba

public class CollectionCopyTests {
    // ... métodos auxiliares ...

    public static void testAddAllMethod() {
        System.out.println("\n--- Prueba: Método addAll() ---");
        List<ItemDetails> originalList = CollectionCopyUtils.createInitialData();
        List<ItemDetails> copiedList = new ArrayList<>();
        copiedList.addAll(originalList); // Copia superficial
        CollectionCopyUtils.modifyOriginalData(originalList);
        CollectionCopyUtils.evaluateCopyResult(copiedList);
    }

    public static void main(String[] args) {
        // ... testConstructorArrayList();
        testAddAllMethod();
        // ... otras pruebas ...
    }
}

Resultado Esperado/Obtenido

--- Prueba: Método addAll() ---
¿Es la primera capa una copia profunda?: false
¿Es la segunda capa (anidada) una copia profunda?: false

Al igual que con el constructor, ambas capas se modifican, lo que indica una copia superficial.

3. Stream API con collect(Collectors.toList()) (Copia Superficial)

Cuando se utiliza stream().collect(Collectors.toList()) sin una operación de mapeo que cree nuevas instancias, el flujo simplemente recolecta las referencias de los objetos existentes en una nueva lista.

Código de Prueba

public class CollectionCopyTests {
    // ... métodos auxiliares ...

    public static void testStreamCollectToList() {
        System.out.println("\n--- Prueba: Stream API (collect(Collectors.toList())) ---");
        List<ItemDetails> originalList = CollectionCopyUtils.createInitialData();
        List<ItemDetails> copiedList = originalList.stream().collect(Collectors.toList()); // Copia superficial
        CollectionCopyUtils.modifyOriginalData(originalList);
        CollectionCopyUtils.evaluateCopyResult(copiedList);
    }

    public static void main(String[] args) {
        // ... pruebas anteriores ...
        testStreamCollectToList();
        // ... otras pruebas ...
    }
}

Resultado Esperado/Obtenido

--- Prueba: Stream API (collect(Collectors.toList())) ---
¿Es la primera capa una copia profunda?: false
¿Es la segunda capa (anidada) una copia profunda?: false

Este método también resulta en una copia superficial, ya que solo se copian las referencias de los objetos.

4. Spring BeanUtils.copyProperties() con Stream (Copia Profunda Parcial)

La utilidad BeanUtils.copyProperties() de Spring Framwork es útil para copiar propiedades entre objetos. Si se utiliza dentro de un stream para mapear cada objeto original a una nueva instancia del objeto de destino, se crea una copia "profunda" para la primera capa (la colección misma y los objetos de primer nivel). Sin embargo, para los objetos anidados dentro de estos objetos de primer nivel, solo se copian las referencias, resultando en una copia superficial en las capas más profundas.

Código de Prueba

public class CollectionCopyTests {
    // ... métodos auxiliares ...

    public static void testSpringBeanUtilsWithStream() {
        System.out.println("\n--- Prueba: Spring BeanUtils.copyProperties() con Stream ---");
        List<ItemDetails> originalList = CollectionCopyUtils.createInitialData();
        List<ItemDetails> copiedList = originalList.stream().map(original -> {
            ItemDetails newItem = new ItemDetails();
            BeanUtils.copyProperties(original, newItem); // Copia propiedades, pero no recursivamente objetos anidados
            return newItem;
        }).collect(Collectors.toList());
        CollectionCopyUtils.modifyOriginalData(originalList);
        CollectionCopyUtils.evaluateCopyResult(copiedList);
    }

    public static void main(String[] args) {
        // ... pruebas anteriores ...
        testSpringBeanUtilsWithStream();
        // ... otras pruebas ...
    }
}

Resultado Esperado/Obtenido

--- Prueba: Spring BeanUtils.copyProperties() con Stream ---
¿Es la primera capa una copia profunda?: true
¿Es la segunda capa (anidada) una copia profunda?: false

Se observa que la primera capa es una copia profunda (los objetos ItemDetails son nuevos), pero la segunda capa (ItemProperty) es superficial (las referencias a la lista de propiedades anidadas son las mismas).

5. Hutool BeanUtil.copyToList() (Copia Profunda Parcial)

La biblioteca Hutool ofrece BeanUtil.copyToList() para copiar coleccionse de beans. Al igual que Spring BeanUtils.copyProperties(), este método realiza una copia superficial para los objetos anidados por defecto, aunque crea nuevas instancias para los objetos de la primera capa.

Código de Prueba

public class CollectionCopyTests {
    // ... métodos auxiliares ...

    public static void testHutoolBeanUtilCopyToList() {
        System.out.println("\n--- Prueba: Hutool BeanUtil.copyToList() ---");
        List<ItemDetails> originalList = CollectionCopyUtils.createInitialData();
        List<ItemDetails> copiedList = BeanUtil.copyToList(originalList, ItemDetails.class); // Copia propiedades, no objetos anidados recursivamente
        CollectionCopyUtils.modifyOriginalData(originalList);
        CollectionCopyUtils.evaluateCopyResult(copiedList);
    }

    public static void main(String[] args) {
        // ... pruebas anteriores ...
        testHutoolBeanUtilCopyToList();
        // ... otras pruebas ...
    }
}

Resultado Esperado/Obtenido

--- Prueba: Hutool BeanUtil.copyToList() ---
¿Es la primera capa una copia profunda?: true
¿Es la segunda capa (anidada) una copia profunda?: false

El comportamiento es idéntico al de Spring BeanUtils: copia profunda en la primera capa y superficial en las anidadas.

6. Serialización/Deserialización con Fastjson (Copia Profunda Completa)

Un método común y robusto para lograr una copia profunda completa es la serialización y deserialización de objetos. Al convertir un objeto a una cadena JSON y luego volver a convertir esa cadena a un objeto, se crean nuevas instancias para todos los objetos, incluidas las anidadas, rompiendo todas las referencias con los objetos originales. Requiere que las clases implementen Serializable.

Código de Prueba

public class CollectionCopyTests {
    // ... métodos auxiliares ...

    public static void testFastjsonSerializationDeserialization() {
        System.out.println("\n--- Prueba: Fastjson (Serialización/Deserialización) ---");
        List<ItemDetails> originalList = CollectionCopyUtils.createInitialData();
        // Los objetos ItemDetails y ItemProperty deben implementar Serializable para esto
        String jsonString = JSON.toJSONString(originalList);
        List<ItemDetails> copiedList = JSONArray.parseArray(jsonString, ItemDetails.class); // Copia profunda
        CollectionCopyUtils.modifyOriginalData(originalList);
        CollectionCopyUtils.evaluateCopyResult(copiedList);
    }

    public static void main(String[] args) {
        // ... pruebas anteriores ...
        testFastjsonSerializationDeserialization();
    }
}

Resultado Esperado/Obtenido

--- Prueba: Fastjson (Serialización/Deserialización) ---
¿Es la primera capa una copia profunda?: true
¿Es la segunda capa (anidada) una copia profunda?: true

Este método logra una copia profunda completa, ya que los cambios en la lista original no afectan a la copia en ninguna de las capas.

Etiquetas: java Colecciones CopiaProfunda CopiaSuperficial StreamAPI

Publicado el 8-1 06:59