Costruttori di Copia in Java

Edoardo Midali
Edoardo Midali

I costruttori di copia rappresentano un pattern fondamentale per creare nuove istanze di oggetti basate su istanze esistenti. A differenza di altri linguaggi come C++, Java non fornisce costruttori di copia automatici, richiedendo un’implementazione esplicita che tenga conto delle complessità della copia profonda, della gestione dei riferimenti e dell’immutabilità.

Concetto e Motivazione

Un costruttore di copia è un costruttore speciale che accetta come parametro un’istanza della stessa classe e crea una nuova istanza copiando lo stato dell’oggetto originale. Questo pattern è essenziale per mantenere l’incapsulamento, implementare l’immutabilità e gestire correttamente la condivisione di oggetti.

Necessità dei Costruttori di Copia

Immutabilità: Gli oggetti immutabili spesso necessitano di costruttori di copia per creare versioni modificate senza alterare l’originale.

Prevenzione della Condivisione Non Intenzionale: Quando si passano oggetti mutabili, è spesso necessario creare copie per evitare modifiche accidentali allo stato condiviso.

Builder Pattern e Fluent Interfaces: I costruttori di copia facilitano l’implementazione di pattern che richiedono la creazione di versioni modificate di oggetti esistenti.

Defensive Programming: Proteggere l’integrità interna degli oggetti copiando parametri mutabili ricevuti dall’esterno.

public class Persona {
    private final String nome;
    private final String cognome;
    private final LocalDate dataNascita;
    private final List<String> hobby;
    private final Indirizzo indirizzo;

    // Costruttore principale
    public Persona(String nome, String cognome, LocalDate dataNascita,
                   List<String> hobby, Indirizzo indirizzo) {
        this.nome = Objects.requireNonNull(nome);
        this.cognome = Objects.requireNonNull(cognome);
        this.dataNascita = Objects.requireNonNull(dataNascita);
        this.hobby = new ArrayList<>(Objects.requireNonNull(hobby));
        this.indirizzo = new Indirizzo(indirizzo);
    }

    // Costruttore di copia
    public Persona(Persona altraPersona) {
        this(altraPersona.nome, altraPersona.cognome, altraPersona.dataNascita,
             altraPersona.hobby, altraPersona.indirizzo);
    }

    // Metodi per creare versioni modificate
    public Persona conNome(String nuovoNome) {
        return new Persona(nuovoNome, this.cognome, this.dataNascita,
                          this.hobby, this.indirizzo);
    }
}

Copia Superficiale vs Copia Profonda

La distinzione tra copia superficiale e profonda è cruciale per l’implementazione corretta dei costruttori di copia, specialmente quando si lavora con oggetti che contengono riferimenti ad altri oggetti.

Copia Superficiale (Shallow Copy)

La copia superficiale duplica solo i riferimenti agli oggetti, non gli oggetti stessi. Questo significa che l’oggetto copiato e l’originale condividono gli stessi oggetti referenziati.

public class DocumentoProblematico {
    private String titolo;
    private List<String> autori;
    private Map<String, String> metadati;

    // Costruttore di copia superficiale - PERICOLOSO
    public DocumentoProblematico(DocumentoProblematico altro) {
        this.titolo = altro.titolo; // OK: String è immutabile
        this.autori = altro.autori; // PROBLEMA: stessa lista condivisa
        this.metadati = altro.metadati; // PROBLEMA: stessa mappa condivisa
    }

    public void aggiungiAutore(String autore) {
        autori.add(autore); // Modifica anche l'oggetto originale!
    }
}

Copia Profonda (Deep Copy)

La copia profonda crea nuove istanze di tutti gli oggetti referenziati, garantendo completa indipendenza tra l’oggetto originale e la copia.

public class DocumentoCorretto {
    private final String titolo;
    private final List<String> autori;
    private final Map<String, String> metadati;
    private final List<Sezione> sezioni;

    public DocumentoCorretto(String titolo, List<String> autori,
                            Map<String, String> metadati, List<Sezione> sezioni) {
        this.titolo = Objects.requireNonNull(titolo);
        this.autori = new ArrayList<>(Objects.requireNonNull(autori));
        this.metadati = new HashMap<>(Objects.requireNonNull(metadati));
        this.sezioni = sezioni.stream()
                              .map(Sezione::new)
                              .collect(Collectors.toList());
    }

    // Costruttore di copia profonda
    public DocumentoCorretto(DocumentoCorretto altro) {
        this(altro.titolo, altro.autori, altro.metadati, altro.sezioni);
    }

    // Metodi immutabili per modifiche
    public DocumentoCorretto conTitolo(String nuovoTitolo) {
        return new DocumentoCorretto(nuovoTitolo, this.autori,
                                    this.metadati, this.sezioni);
    }

    public DocumentoCorretto aggiungiAutore(String autore) {
        List<String> nuoviAutori = new ArrayList<>(this.autori);
        nuoviAutori.add(autore);
        return new DocumentoCorretto(this.titolo, nuoviAutori,
                                    this.metadati, this.sezioni);
    }
}

class Sezione {
    private final String titolo;
    private final String contenuto;
    private final List<String> note;

    public Sezione(String titolo, String contenuto, List<String> note) {
        this.titolo = Objects.requireNonNull(titolo);
        this.contenuto = Objects.requireNonNull(contenuto);
        this.note = new ArrayList<>(Objects.requireNonNull(note));
    }

    // Costruttore di copia
    public Sezione(Sezione altraSezione) {
        this(altraSezione.titolo, altraSezione.contenuto, altraSezione.note);
    }
}

Pattern di Utilizzo con Builder

I costruttori di copia si integrano perfettamente con il Builder pattern per creare API fluenti e flessibili.

public class ConfigurazioneComplessa {
    private final String nome;
    private final Map<String, Object> parametri;
    private final List<String> moduli;
    private final Set<String> feature;

    private ConfigurazioneComplessa(Builder builder) {
        this.nome = builder.nome;
        this.parametri = new HashMap<>(builder.parametri);
        this.moduli = new ArrayList<>(builder.moduli);
        this.feature = new HashSet<>(builder.feature);
    }

    // Costruttore di copia
    public ConfigurazioneComplessa(ConfigurazioneComplessa altra) {
        this.nome = altra.nome;
        this.parametri = new HashMap<>(altra.parametri);
        this.moduli = new ArrayList<>(altra.moduli);
        this.feature = new HashSet<>(altra.feature);
    }

    // Metodo per creare builder da configurazione esistente
    public Builder toBuilder() {
        return new Builder()
            .nome(this.nome)
            .parametri(this.parametri)
            .moduli(this.moduli)
            .feature(this.feature);
    }

    public static class Builder {
        private String nome;
        private Map<String, Object> parametri = new HashMap<>();
        private List<String> moduli = new ArrayList<>();
        private Set<String> feature = new HashSet<>();

        public Builder nome(String nome) {
            this.nome = nome;
            return this;
        }

        public Builder parametri(Map<String, Object> parametri) {
            this.parametri.putAll(parametri);
            return this;
        }

        public Builder moduli(Collection<String> moduli) {
            this.moduli.addAll(moduli);
            return this;
        }

        public Builder feature(Collection<String> features) {
            this.feature.addAll(features);
            return this;
        }

        public ConfigurazioneComplessa build() {
            if (nome == null) {
                throw new IllegalStateException("Nome è obbligatorio");
            }
            return new ConfigurazioneComplessa(this);
        }
    }
}

Gestione della Serializzazione

I costruttori di copia diventano particolarmente importanti quando si lavora con la serializzazione, garantendo che gli oggetti deserializzati mantengano l’integrità dei dati.

public class SerializableDocument implements Serializable {
    private static final long serialVersionUID = 1L;

    private final String id;
    private final String content;
    private final LocalDateTime created;
    private final List<String> tags;

    public SerializableDocument(String id, String content, LocalDateTime created,
                               List<String> tags) {
        this.id = Objects.requireNonNull(id);
        this.content = Objects.requireNonNull(content);
        this.created = Objects.requireNonNull(created);
        this.tags = new ArrayList<>(Objects.requireNonNull(tags));
        validateState();
    }

    // Costruttore di copia
    public SerializableDocument(SerializableDocument other) {
        this(other.id, other.content, other.created, other.tags);
    }

    // Metodo per garantire integrità dopo deserializzazione
    private Object readResolve() {
        return new SerializableDocument(this);
    }

    private void validateState() {
        if (id == null || id.trim().isEmpty()) {
            throw new IllegalStateException("ID cannot be null or empty");
        }
        if (content == null) {
            throw new IllegalStateException("Content cannot be null");
        }
    }
}

Performance e Ottimizzazioni

Quando si implementano costruttori di copia, è importante considerare le implicazioni di performance, specialmente per oggetti grandi o complessi.

Copy-on-Write Optimization

Per oggetti con dati binari grandi o collezioni massicce, la strategia copy-on-write può migliorare significativamente le performance.

public class OptimizedDocument {
    private final String content;
    private final byte[] binaryData;
    private final boolean isDataShared;

    public OptimizedDocument(String content, byte[] binaryData) {
        this.content = Objects.requireNonNull(content);
        this.binaryData = Objects.requireNonNull(binaryData).clone();
        this.isDataShared = false;
    }

    // Costruttore di copia ottimizzato
    private OptimizedDocument(String content, byte[] binaryData, boolean shared) {
        this.content = content;
        this.binaryData = binaryData;
        this.isDataShared = shared;
    }

    public OptimizedDocument(OptimizedDocument other) {
        this.content = other.content;
        this.binaryData = other.binaryData; // Condividiamo inizialmente
        this.isDataShared = true;
    }

    public byte[] getBinaryData() {
        return binaryData.clone(); // Sempre copia difensiva
    }

    public OptimizedDocument withModifiedContent(String newContent) {
        // Se i dati sono condivisi, li copiamo solo ora
        byte[] newBinaryData = isDataShared ? binaryData.clone() : binaryData;
        return new OptimizedDocument(newContent, newBinaryData, false);
    }
}

Best Practices e Linee Guida

Validazione e Null Safety

I costruttori di copia devono mantenere le stesse validazioni del costruttore principale e gestire appropriatamente i valori null.

public final class BestPracticeExample {
    private final String name;
    private final List<String> items;
    private final Map<String, Object> config;

    public BestPracticeExample(String name, List<String> items, Map<String, Object> config) {
        this.name = Objects.requireNonNull(name, "Name cannot be null");
        if (name.trim().isEmpty()) {
            throw new IllegalArgumentException("Name cannot be empty");
        }

        this.items = items != null ? new ArrayList<>(items) : new ArrayList<>();
        this.config = config != null ? new HashMap<>(config) : new HashMap<>();
    }

    // Costruttore di copia che mantiene le stesse validazioni
    public BestPracticeExample(BestPracticeExample other) {
        this(Objects.requireNonNull(other, "Source object cannot be null").name,
             other.items, other.config);
    }

    // Factory methods per copia con modifiche
    public BestPracticeExample withName(String newName) {
        return new BestPracticeExample(newName, this.items, this.config);
    }

    public BestPracticeExample withAddedItem(String item) {
        List<String> newItems = new ArrayList<>(this.items);
        newItems.add(Objects.requireNonNull(item));
        return new BestPracticeExample(this.name, newItems, this.config);
    }

    // Getters con copie difensive
    public List<String> getItems() {
        return new ArrayList<>(items);
    }

    public Map<String, Object> getConfig() {
        return new HashMap<>(config);
    }
}

Pattern per Oggetti Immutabili

I costruttori di copia sono essenziali per implementare correttamente oggetti immutabili che necessitano di varianti.

public final class ImmutableConfiguration {
    private final String environment;
    private final Map<String, String> properties;
    private final List<String> activeProfiles;

    public ImmutableConfiguration(String environment, Map<String, String> properties,
                                 List<String> activeProfiles) {
        this.environment = Objects.requireNonNull(environment);
        this.properties = Map.copyOf(Objects.requireNonNull(properties));
        this.activeProfiles = List.copyOf(Objects.requireNonNull(activeProfiles));
    }

    // Costruttore di copia
    public ImmutableConfiguration(ImmutableConfiguration other) {
        this(other.environment, other.properties, other.activeProfiles);
    }

    // Metodi per creare varianti immutabili
    public ImmutableConfiguration withEnvironment(String newEnvironment) {
        return new ImmutableConfiguration(newEnvironment, this.properties, this.activeProfiles);
    }

    public ImmutableConfiguration withProperty(String key, String value) {
        Map<String, String> newProperties = new HashMap<>(this.properties);
        newProperties.put(key, value);
        return new ImmutableConfiguration(this.environment, newProperties, this.activeProfiles);
    }

    public ImmutableConfiguration withProfile(String profile) {
        List<String> newProfiles = new ArrayList<>(this.activeProfiles);
        if (!newProfiles.contains(profile)) {
            newProfiles.add(profile);
        }
        return new ImmutableConfiguration(this.environment, this.properties, newProfiles);
    }
}

Conclusione

I costruttori di copia sono uno strumento essenziale per implementare correttamente l’incapsulamento e l’immutabilità in Java. La loro implementazione richiede attenzione alle differenze tra copia superficiale e profonda, considerazioni di performance e una gestione accurata dei tipi mutabili e immutabili.

Quando implementati correttamente, i costruttori di copia facilitano la creazione di codice sicuro, prevedibile e manutenibile. La chiave del successo è comprendere quando utilizzare questo pattern e come implementarlo in modo efficiente senza compromettere le performance o la correttezza del codice.

Una buona regola generale è utilizzare costruttori di copia ogni volta che si lavora con oggetti mutabili che devono essere condivisi o quando si implementano pattern immutabili che richiedono la creazione di versioni modificate di oggetti esistenti.