Java Language
Oggetti immutabili
Ricerca…
Osservazioni
Gli oggetti immutabili hanno uno stato fisso (nessun setter), quindi tutti gli stati devono essere noti al momento della creazione dell'oggetto.
Sebbene non sia tecnicamente necessario, è buona pratica rendere final
tutti i campi. Questo renderà la classe immutable thread-safe (vedi Java Concurrency in Practice, 3.4.1).
Gli esempi mostrano diversi modelli che possono aiutare a raggiungere questo obiettivo.
Creare una versione immutabile di un tipo usando la copia difensiva.
Alcuni tipi e classi di base in Java sono fondamentalmente mutevoli. Ad esempio, tutti i tipi di array sono modificabili, così come le classi come java.util.Data
. Questo può essere imbarazzante in situazioni in cui è imposto un tipo immutabile.
Un modo per affrontarlo consiste nel creare un wrapper immutabile per il tipo mutable. Ecco un semplice wrapper per una matrice di numeri interi
public class ImmutableIntArray {
private final int[] array;
public ImmutableIntArray(int[] array) {
this.array = array.clone();
}
public int[] getValue() {
return this.clone();
}
}
Questa classe funziona usando la copia difensiva per isolare lo stato mutabile (l' int[]
) da qualsiasi codice che possa mutarlo:
Il costruttore usa
clone()
per creare una copia distinta dell'array dei parametri. Se il chiamante del costruttore ha successivamente modificato l'array di parametri, non avrebbe influenzato lo stato diImmutableIntArray
.Il metodo
getValue()
usa ancheclone()
per creare l'array che viene restituito. Se il chiamante dovesse cambiare l'array dei risultati, non avrebbe influenzato lo stato diImmutableIntArray
.
Potremmo anche aggiungere metodi a ImmutableIntArray
per eseguire operazioni di sola lettura sull'array avvolto; ad esempio ottenere la sua lunghezza, ottenere il valore in un indice particolare, e così via.
Si noti che un tipo di wrapper immutabile implementato in questo modo non è compatibile con il tipo originale. Non puoi semplicemente sostituire il primo per quest'ultimo.
La ricetta per una lezione immutabile
Un oggetto immutabile è un oggetto il cui stato non può essere modificato. Una classe immutabile è una classe le cui istanze sono immutabili per progettazione e implementazione. La classe Java che viene presentata più comunemente come esempio di immutabilità è java.lang.String .
Quello che segue è un esempio stereotipato:
public final class Person {
private final String name;
private final String ssn; // (SSN == social security number)
public Person(String name, String ssn) {
this.name = name;
this.ssn = ssn;
}
public String getName() {
return name;
}
public String getSSN() {
return ssn;
}
}
Una variazione su questo è dichiarare il costruttore come private
e fornire invece un metodo public static
.
La ricetta standard per una classe immutabile è la seguente:
- Tutte le proprietà devono essere impostate nel costruttore o nei metodi di fabbrica.
- Non dovrebbero esserci setter.
- Se è necessario includere setter per ragioni di compatibilità dell'interfaccia, non devono fare nulla o lanciare un'eccezione.
- Tutte le proprietà dovrebbero essere dichiarate come
private
efinal
. - Per tutte le proprietà che fanno riferimento a tipi mutabili:
- la proprietà deve essere inizializzata con una copia profonda del valore passato tramite il costruttore e
- il getter della proprietà dovrebbe restituire una copia profonda del valore della proprietà.
- La classe dovrebbe essere dichiarata come
final
per impedire a qualcuno di creare una sottoclasse mutevole di una classe immutabile.
Un paio di altre cose da notare:
- L'immutabilità non impedisce all'oggetto di essere annullabile; ad es.
null
può essere assegnato a una variabileString
. - Se le proprietà di una classe immutable sono dichiarate
final
, le istanze sono intrinsecamente thread-safe. Ciò rende le classi immutabili un buon esempio per l'implementazione di applicazioni multi-thread.
Difetti di progettazione tipici che impediscono a una classe di essere immutabile
Utilizzo di alcuni setter, senza impostare tutte le proprietà necessarie nel / i costruttore / i
public final class Person { // example of a bad immutability
private final String name;
private final String surname;
public Person(String name) {
this.name = name;
}
public String getName() { return name;}
public String getSurname() { return surname;}
public void setSurname(String surname) { this.surname = surname); }
}
È facile dimostrare che la classe Person
non è immutabile:
Person person = new Person("Joe");
person.setSurname("Average"); // NOT OK, change surname field after creation
Per risolvere il problema, elimina setSurname()
e setSurname()
il costruttore come segue:
public Person(String name, String surname) {
this.name = name;
this.surname = surname;
}
Non contrassegnare le variabili di istanza come private e final
Dai un'occhiata alla seguente classe:
public final class Person {
public String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
Il seguente frammento mostra che la suddetta classe non è immutabile:
Person person = new Person("Average Joe");
person.name = "Magic Mike"; // not OK, new name for person after creation
Per risolvere il problema, contrassegna semplicemente la proprietà del nome come private
e final
.
Esporre un oggetto mutevole della classe in un getter
Dai un'occhiata alla seguente classe:
import java.util.List;
import java.util.ArrayList;
public final class Names {
private final List<String> names;
public Names(List<String> names) {
this.names = new ArrayList<String>(names);
}
public List<String> getNames() {
return names;
}
public int size() {
return names.size();
}
}
Names
classe dei Names
sembra immutabile a prima vista, ma non è come mostra il seguente codice:
List<String> namesList = new ArrayList<String>();
namesList.add("Average Joe");
Names names = new Names(namesList);
System.out.println(names.size()); // 1, only containing "Average Joe"
namesList = names.getNames();
namesList.add("Magic Mike");
System.out.println(names.size()); // 2, NOT OK, now names also contains "Magic Mike"
Ciò è accaduto perché una modifica all'Elenco di riferimento restituito da getNames()
può modificare l'elenco effettivo di Names
.
Per risolvere questo problema, è sufficiente evitare i riferimenti che fanno riferimento a oggetti mutabili della classe sia per l'esecuzione di copie difensive ritorno, come segue:
public List<String> getNames() {
return new ArrayList<String>(this.names); // copies elements
}
o progettando getter in modo che vengano restituiti solo altri oggetti e primitive immutabili , come segue:
public String getName(int index) {
return names.get(index);
}
public int size() {
return names.size();
}
Iniezione del costruttore con oggetto (i) che può essere modificato al di fuori della classe immutabile
Questa è una variazione del difetto precedente. Dai un'occhiata alla seguente classe:
import java.util.List;
public final class NewNames {
private final List<String> names;
public Names(List<String> names) {
this.names = names;
}
public String getName(int index) {
return names.get(index);
}
public int size() {
return names.size();
}
}
Come classe Names
prima, anche la classe NewNames
sembra immutabile a prima vista, ma non lo è, infatti il seguente frammento dimostra il contrario:
List<String> namesList = new ArrayList<String>();
namesList.add("Average Joe");
NewNames names = new NewNames(namesList);
System.out.println(names.size()); // 1, only containing "Average Joe"
namesList.add("Magic Mike");
System.out.println(names.size()); // 2, NOT OK, now names also contains "Magic Mike"
Per risolvere questo problema, come nel difetto precedente, è sufficiente creare copie difensive dell'oggetto senza assegnarlo direttamente alla classe immutabile, ovvero il costruttore può essere modificato come segue:
public Names(List<String> names) {
this.names = new ArrayList<String>(names);
}
Lasciando ignorare i metodi della classe
Dai un'occhiata alla seguente classe:
public class Person {
private final String name;
public Person(String name) {
this.name = name;
}
public String getName() { return name;}
}
Person
classe della Person
sembra immutabile a prima vista, ma supponiamo che una nuova sottoclasse di Person
sia definita:
public class MutablePerson extends Person {
private String newName;
public MutablePerson(String name) {
super(name);
}
@Override
public String getName() {
return newName;
}
public void setName(String name) {
newName = name;
}
}
ora la mutabilità di Person
(im) può essere sfruttata attraverso il polimorfismo usando la nuova sottoclasse:
Person person = new MutablePerson("Average Joe");
System.out.println(person.getName()); prints Average Joe
person.setName("Magic Mike"); // NOT OK, person has now a new name!
System.out.println(person.getName()); // prints Magic Mike
Per risolvere questo problema, sia contrassegnare la classe come final
in modo che non possa essere esteso o dichiarare tutto il suo costruttore (s) come private
.