Как управлять неизменяемым классом с LinkedList в качестве поля экземпляра?

у меня есть неизменяемый класс Employees такой:

public final class Employees {
    private final List<Person> persons;

    public Employees() {
        persons = new LinkedList<Person>();
    }

    public List<Person> getPersons() {
        return persons;
    }
}

как сохранить этот класс неизменяемым?

Я сделал поле private и final, и я не предоставил метод сеттера. Достаточно ли этого для достижения неизменности?

6 ответов


ответ отредактирован, чтобы объяснить не только случай с изменяемым версия Person, но также с неизменяемой версией Person.


ваш класс является изменяемым, потому что вы можете сделать это:

Employees employees = new Employees();
employees.getPersons().add(new Person());

обратите внимание, что не передавая список лиц конструктору, если вы измените свой код для создания неизменяемого класса, у вас будет не полезный класс, содержащий когда-либо пустой список лиц, поэтому я предполагаю, что необходимо передать List<Person> к конструктор.

теперь есть два сценария, с разными реализациями:

  • Person незыблем
  • Person изменчиво

Сценарий 1 - Person незыблем

вам нужно только создайте неизменяемую копию параметра persons в конструкторе.

вам нужно сделать final класса или, по крайней мере, способ getPersons чтобы убедиться, что никто не предоставляет изменяемую перезаписанную версию getPersons метод.

public final class Employees {
    private final List<Persons> persons;

    public Employees(List<Person> persons) {
        persons =  Collections.unmodifiableList(new ArrayList<>(persons));
    }

    public List<Employees> getPersons() {
        return persons;
    }
}

Сценарий 2 - Person изменчиво

вам нужно создать глубокую копию persons на getPersons метод.

вам нужно создать глубокую копию persons на конструктор.

вам нужно сделать final класса или, по крайней мере, способ getPersons чтобы убедиться, что никто не предоставляет изменяемую перезаписанную версию getPersons метод.

public final class Employees {
    private final List<Persons> persons;

    public Employees(List<Person> persons) {
        persons = new ArrayList<>();
        for (Person person : persons) {
            persons.add(deepCopy(person));   // If clone is provided 
                                           // and creates a deep copy of person
        }
    }

    public List<Employees> getPersons() {
        List<Person> temp = new ArrayList<>();
        for (Person person : persons) {
            temp.add(deepCopy(person)); // If clone is provided 
                                         // and creates a deep copy of person
        }  
        return temp;
    }

    public Person deepCopy(Person person) {
        Person copy = new Person();  
        // Provide a deep copy of person
        ...
        return copy;
    }
}

эта часть ответа должна показать, почему не глубокая копия personsParameter переданный конструктору может создавать изменяемые версии Employees:

List<Person> personsParameter = new ArrayList<>();
Person person = new Person();
person.setName("Pippo");
personsParameter.add(person);
Employees employees = new Employees(personsParameter);

// Prints Pippo    
System.out.println(employees.getPersons().get(0).getName()); 


employees.getPersons().get(0).setName("newName");

// Prints again Pippo    
System.out.println(employees.getPersons().get(0).getName()); 

// But modifiyng something reachable from the parameter 
// used in the constructor 
person.setName("Pluto");

// Now it prints Pluto, so employees has changed    
System.out.println(employees.getPersons().get(0).getName()); 

нет, этого недостаточно, потому что в Java даже ссылки передаются по значению. Итак, если ваш List's ссылка убегает ( что произойдет, когда они называют get), то ваш класс больше не неизменяемые.

у вас есть 2 варианта :

  1. создайте защитную копию вашего List и верните его, когда get вызывается.
  2. оберните свой список как неизменяемый / неизменяемый List и вернуть его (или заменить оригинал List С этим, то вы можете вернуть это безопасно без дальнейшей упаковки его)

Примечание : Вы должны убедиться, что Person является неизменяемым или создает защитные копии для каждого Person на List


ответ находится в документах - стратегия определения неизменяемых объектов:

  1. не предоставляйте методы "сеттера" - методы, которые изменяют поля или объекты, на которые ссылаются поля.

  2. сделать все поля окончательными и частными.

  3. Не позволяйте подклассам переопределять методы.

  4. если поля экземпляра содержат ссылки на изменяемые объекты, не разрешайте объекты для изменения:

    4.1. Не предоставляйте методы, которые изменяют изменяемые объекты.

    4.2. Не делитесь ссылками на изменяемые объекты. Никогда не храните ссылки на внешние изменяемые объекты, переданные конструктору; при необходимости создайте копии и сохраните ссылки на копии. Аналогичным образом, при необходимости создавайте копии внутренних изменяемых объектов, чтобы избежать возврата оригиналов в методах.


вы могли бы сделать еще лучше с

import java.util.Collections;
...
public List<Person> getPersons() {
    return Collections.unmodifiableList( persons);
}

в этом случае я бы предпочел не раскрывать список или карту вообще. Если вы передаете коллекцию, вы нарушаете инкапсуляцию, предполагая, что она должна храниться как список.

Если вы не выставляете список, вы можете избежать необходимости обернуть его или предоставить защитную копию.

public final class Employees {
    private final List<Person> persons;

    public Employees() {
        persons = new LinkedList<Person>();
    }

    public Person getPerson(int n) {
        return persons.get(n);
    }  

    public int getPersonCount() {
        return persons.size();
    }
}

причина, по которой вы хотите избежать защитных копий, заключается в том, что их можно назвать много. Кто-то мог легко написать (Вы были бы удивлены, как часто это пишется)

for (int i = 0; i < employees.getPersons().size(); i++) {
    Person p = employees.getPersons().get(i);

}

By предоставление конкретных методов вы можете оптимизировать код для этого варианта использования.


Я думаю, что проблема с этим подходом заключается в том, что я все еще могу сделать getPersons().добавить(х). Чтобы этого не допустить, вы должны создать конструктор, который получает список лиц, а затем создать экземпляр внутреннего окончательного списка, например,ImmutableList. Остальная часть кода кажется мне подходящей.

EDIT:

Как указывает Давиде Лоренцо Марино, этого недостаточно. Вам также нужно будет вернуть глубокую копию списка в геттере, чтобы пользователи не могли измените свой внутренний список с помощью возвращаемой ссылки getter.

делая это, вы можете просто иметь обычный LinkedList в качестве списка itnernal (private final). В конструкторе создается глубокая копия списка, а в геттере возвращается глубокая копия внутреннего списка.