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 stan ImmutableIntArray .

  • 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 stan ImmutableIntArray .

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 i final .
  • 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 zmiennej String .
  • 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 .



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow