Java Language
Niezmienne przedmioty
Szukaj…
Uwagi
Niezmienne obiekty mają ustalony stan (bez ustawiaczy), więc cały stan musi być znany w czasie tworzenia obiektu.
Chociaż nie jest to technicznie wymagane, najlepszą praktyką jest, aby wszystkie pola były final
. Dzięki temu niezmienna klasa będzie bezpieczna dla wątków (por. Java Concurrency in Practice, 3.4.1).
Przykłady pokazują kilka wzorców, które mogą pomóc w osiągnięciu tego.
Tworzenie niezmiennej wersji typu za pomocą defensywnego kopiowania.
Niektóre podstawowe typy i klasy w Javie są zasadniczo zmienne. Na przykład wszystkie typy tablic są zmienne, podobnie jak klasy java.util.Data
. Może to być niezręczne w sytuacjach, w których wymagany jest niezmienny typ.
Jednym ze sposobów na poradzenie sobie z tym jest stworzenie niezmiennego opakowania dla typu zmiennego. Oto proste opakowanie dla tablicy liczb całkowitych
public class ImmutableIntArray {
private final int[] array;
public ImmutableIntArray(int[] array) {
this.array = array.clone();
}
public int[] getValue() {
return this.clone();
}
}
Ta klasa działa przy użyciu defensywnego kopiowania w celu odizolowania stanu zmiennego ( int[]
) od dowolnego kodu, który mógłby go zmutować:
Konstruktor używa
clone()
aby utworzyć wyraźną kopię tablicy parametrów. Jeśli obiekt wywołujący konstruktora następnie zmieni tablicę parametrów, nie wpłynie to na stanImmutableIntArray
.Metoda
getValue()
używa równieżclone()
do utworzenia zwracanej tablicy. Jeśli program wywołujący zmieni tablicę wyników, nie wpłynie to na stanImmutableIntArray
.
Możemy również dodać metody do ImmutableIntArray
aby wykonywać operacje tylko do odczytu na opakowanej tablicy; np. uzyskaj jego długość, uzyskaj wartość przy określonym indeksie i tak dalej.
Zauważ, że niezmienny typ opakowania zaimplementowany w ten sposób nie jest zgodny z typem oryginalnym. Pierwszego nie można zastąpić drugim.
Przepis na niezmienną klasę
Niezmienny obiekt to obiekt, którego stanu nie można zmienić. Niezmienna klasa to klasa, której instancje są niezmienne przez projekt i implementację. Klasą Java, która jest najczęściej prezentowana jako przykład niezmienności, jest java.lang.String .
Oto stereotypowy przykład:
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;
}
}
Odmianą jest zadeklarowanie konstruktora jako private
i podanie public static
metody public static
fabryki zamiast.
Standardowa recepta na niezmienną klasę jest następująca:
- Wszystkie właściwości muszą być ustawione w konstruktorze lub metodzie fabrycznej.
- Nie powinno być seterów.
- Jeśli konieczne jest dołączenie seterów ze względu na kompatybilność interfejsu, nie powinni nic robić ani zgłaszać wyjątku.
- Wszystkie nieruchomości powinny być zadeklarowane jako
private
ifinal
. - Dla wszystkich właściwości, które są odniesieniami do typów zmiennych:
- właściwość powinna zostać zainicjowana głęboką kopią wartości przekazywanej przez konstruktor, oraz
- moduł pobierający właściwość powinien zwrócić głęboką kopię wartości właściwości.
- Klasa powinna zostać zadeklarowana jako
final
aby zapobiec tworzeniu przez użytkownika zmiennej podklasy niezmiennej klasy.
Kilka innych rzeczy do zapamiętania:
- Niezmienność nie uniemożliwia zerowania obiektu; np.
null
może być przypisany do zmiennejString
. - Jeśli niezmienne właściwości klas zostaną zadeklarowane jako
final
, instancje są z natury bezpieczne dla wątków. To czyni niezmienne klasy dobrym budulcem do implementacji aplikacji wielowątkowych.
Typowe wady konstrukcyjne, które uniemożliwiają niezmienność klasy
Korzystanie z niektórych ustawień, bez ustawiania wszystkich potrzebnych właściwości w konstruktorze
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); }
}
Łatwo jest pokazać, że klasa Person
nie jest niezmienna:
Person person = new Person("Joe");
person.setSurname("Average"); // NOT OK, change surname field after creation
Aby to naprawić, po prostu usuń setSurname()
i refaktoryzuj konstruktora w następujący sposób:
public Person(String name, String surname) {
this.name = name;
this.surname = surname;
}
Nie oznaczanie zmiennych instancji jako prywatnych i końcowych
Spójrz na następującą klasę:
public final class Person {
public String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
Poniższy fragment kodu pokazuje, że powyższa klasa nie jest niezmienna:
Person person = new Person("Average Joe");
person.name = "Magic Mike"; // not OK, new name for person after creation
Aby to naprawić, po prostu oznacz własność name jako private
i final
.
Ujawnianie modyfikowalnego obiektu klasy w getterze
Spójrz na następującą klasę:
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();
}
}
Klasa Names
wydaje się niezmienna na pierwszy rzut oka, ale nie jest tak, jak pokazuje następujący kod:
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"
Stało się tak, ponieważ zmiana listy referencyjnej zwrócona przez getNames()
może modyfikować rzeczywistą listę Names
.
Aby rozwiązać ten problem, po prostu uniknąć powrotu modyfikowalnych referencji że klasa obiektów referencyjnych albo przez wykonywanie kopii obronne, co następuje:
public List<String> getNames() {
return new ArrayList<String>(this.names); // copies elements
}
lub projektując metody pobierające w taki sposób, że zwracane są tylko inne niezmienne obiekty i prymitywy :
public String getName(int index) {
return names.get(index);
}
public int size() {
return names.size();
}
Wstrzykiwanie konstruktora obiektom, które można modyfikować poza niezmienną klasą
Jest to odmiana poprzedniej wady. Spójrz na następującą klasę:
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();
}
}
Podobnie jak poprzednia klasa Names
, również klasa NewNames
na pierwszy rzut oka wydaje się niezmienna, ale tak naprawdę nie jest to następujący fragment kodu:
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"
Aby to naprawić, podobnie jak w poprzedniej usterce, po prostu wykonaj kopie obronne obiektu bez przypisywania go bezpośrednio do niezmiennej klasy, tzn. Konstruktor można zmienić w następujący sposób:
public Names(List<String> names) {
this.names = new ArrayList<String>(names);
}
Pozwalanie na przesłonięcie metod klasy
Spójrz na następującą klasę:
public class Person {
private final String name;
public Person(String name) {
this.name = name;
}
public String getName() { return name;}
}
Klasa Person
wydaje się niezmienna na pierwszy rzut oka, ale załóżmy, że zdefiniowano nową podklasę Person
:
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;
}
}
teraz Person
(im) można wykorzystać poprzez polimorfizm przy użyciu nowej podklasy:
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
Aby rozwiązać ten problem, albo oznaczyć klasę jako final
, więc nie może być przedłużony lub zadeklarować wszystkie jego konstruktora (S) jako private
.