繁体   English   中英

如何使用 LinkedList 作为实例字段管理不可变类?

[英]How to manage immutable class with LinkedList as an instance field?

我有一个名为Employees的不可变类,如下所示:

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

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

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

我如何保持这个类不可变?

我将字段privatefinal ,并且没有提供 setter 方法。 这足以实现不变性吗?

答案编辑解释不仅与一个可变版本的情况下Person ,还带着一个不变的版本Person


你的类是可变的,因为你可以这样做:

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

请注意,如果您更改代码以创建不可变类,则不将人员列表传递给构造函数,您将拥有一个无用的类,其中包含一个空的人员列表,因此我认为有必要将List<Person>传递给构造函数。

现在有两种场景,具有不同的实现:

  • Person是不变的
  • Person是可变的

场景 1 - Person是不可变的

您只需要在构造函数中创建一个人参数的不可变副本

您还需要使类或至少方法getPersons成为final以确保没有人提供getPersons方法的可变覆盖版本。

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

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

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

场景 2 - Person是可变的

您需要创建的深层副本personsgetPersons方法

您需要创建的深层副本persons的构造函数

您还需要使类或至少方法getPersons成为final以确保没有人提供getPersons方法的可变覆盖版本。

public final class Employees {
    private final List<Person> 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<Person> 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时会发生这种情况),那么您的类就不再是immutable

您有 2 个选择:

  1. 创建List的防御副本并在调用 get 时返回它。
  2. 将您的 List 包装为一个不可变/不可修改的List并返回它(或用它替换您的原始List ,然后您可以安全地返回它而无需进一步包装它)

注意:您必须确保Person是不可变的,或者为List每个Person创建防御性副本

答案可以在文档 - 定义不可变对象的策略中找到:

  1. 不要提供“setter”方法——修改字段或字段引用的对象的方法。

  2. 将所有字段设为最终和私有。

  3. 不允许子类覆盖方法。

  4. 如果实例字段包含对可变对象的引用,则不允许更改这些对象

    4.1. 不要提供修改可变对象的方法。

    4.2. 不要共享对可变对象的引用。 永远不要存储对传递给构造函数的外部可变对象的引用; 如有必要,创建副本并存储对副本的引用。 同样,必要时创建内部可变对象的副本,以避免在方法中返回原始对象。

你可以做得更好

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

在这种情况下,我宁愿根本不公开 List 或 Map。 如果你传递了集合,你是不是通过假设它必须存储为列表来破坏封装。

如果不公开 List,则可以避免需要对其进行包装或提供防御性副本。

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);

}

通过提供特定方法,您可以优化该用例的代码。

我认为这种方法的问题在于我仍然可以执行 getPersons().add(x)。 如果不允许这样做,您必须创建一个构造函数来接收人员列表,然后将您的内部最终列表实例化为,例如, ImmutableList 其余的代码对我来说似乎没问题。

编辑:

正如 Davide Lorenzo MARINO 所指出的,这还不够。 您还需要在 getter 中返回列表的深层副本,因此用户无法通过 getter 返回的引用修改您的内部列表。

通过这样做,您可以将一个普通的 LinkedList 作为您的内部列表(私有最终列表)。 在您的构造函数中,您创建了 List 的深层副本,并在 getter 上返回了内部 List 的深层副本。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM