如何使用 LinkedList 作为实例字段来管理不可变 class?
How to manage immutable class with LinkedList as an instance field?
我有一个名为 Employees
的不可变 class,如下所示:
public final class Employees {
private final List<Person> persons;
public Employees() {
persons = new LinkedList<Person>();
}
public List<Person> getPersons() {
return persons;
}
}
如何让这个 class 不可变?
我做了字段private
和final
,我没有提供setter方法。这足以实现不变性吗?
不,这还不够,因为在 java 中甚至 引用也是按值传递的 。所以,如果你的 List's
引用逃逸(当他们调用 get
时会发生这种情况),那么你的 class 不再是 immutable.
您有 2 个选择:
- 创建
List
的防御副本,并在调用 get 时 return 它。
- 将您的 List 包装为不可变/不可修改的
List
和 return 它(或用它替换您原来的 List
,然后您可以 return 安全地使用它而无需进一步包装它)
注意:您必须确保 Person
是不可变的,或者为 List
中的每个 Person
创建防御副本
你可以做得更好
import java.util.Collections;
...
public List<Person> getPersons() {
return Collections.unmodifiableList( persons);
}
编辑的答案不仅解释了 Person
的可变版本的情况,还解释了 Person
.
的不可变版本的情况
您的 class 是可变的,因为您可以这样做:
Employees employees = new Employees();
employees.getPersons().add(new Person());
请注意,如果您更改代码以创建不可变的 class,请不要将人员列表传递给构造函数,您将没有用 class 保存一个空的人员列表,所以我认为有必要将 List<Person>
传递给构造函数。
现在有两种场景,实现方式不同:
Person
是不可变的
Person
是可变的
场景 1 - Person
是不可变的
您只需在构造函数中创建 persons 参数的不可变副本。
您还需要制作final
class或至少getPersons
方法以确保没有人提供可变覆盖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
是可变的
您需要在 getPersons
方法中创建 persons
的深层副本。
您需要在构造函数上创建 persons
的深层副本。
您还需要制作final
class或至少getPersons
方法以确保没有人提供可变覆盖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());
答案在文档中找到 - A Strategy for Defining Immutable Objects:
不提供 "setter" 方法 — 修改字段或字段引用的对象的方法。
将所有字段设为最终字段和私有字段。
不允许子类覆盖方法。
如果实例字段包含对可变对象的引用,则不允许更改这些对象:
4.1。不要提供修改可变对象的方法。
4.2。不要共享对可变对象的引用。永远不要存储对传递给构造函数的外部可变对象的引用;如有必要,创建副本并存储对副本的引用。同样,必要时创建内部可变对象的副本,以避免在方法中返回原始对象。
我认为这种方法的问题在于我仍然可以执行 getPersons().add(x)。如果不允许这样做,您必须创建一个构造函数来接收人员列表,然后实例化您的内部最终列表,例如 ImmutableList。
其余代码对我来说似乎没问题。
编辑:
正如 Davide Lorenzo MARINO 指出的那样,这还不够。您还需要 return getter 中列表的深层副本,因此用户无法通过 getter returned 引用修改您的内部列表。
通过这样做,您可以将普通的 LinkedList 作为您的内部列表(私人最终)。在您的构造函数中,您创建了 List 的深层副本,在 getter 中,您 return 了内部 List 的深层副本。
在这种情况下,我赞成根本不公开 List 或 Map。如果你传出集合,你是否通过假设它必须存储为列表来破坏封装。
如果您不公开列表,则可以避免需要包装它或提供防御性副本。
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);
}
通过提供特定方法,您可以针对该用例优化代码。
我有一个名为 Employees
的不可变 class,如下所示:
public final class Employees {
private final List<Person> persons;
public Employees() {
persons = new LinkedList<Person>();
}
public List<Person> getPersons() {
return persons;
}
}
如何让这个 class 不可变?
我做了字段private
和final
,我没有提供setter方法。这足以实现不变性吗?
不,这还不够,因为在 java 中甚至 引用也是按值传递的 。所以,如果你的 List's
引用逃逸(当他们调用 get
时会发生这种情况),那么你的 class 不再是 immutable.
您有 2 个选择:
- 创建
List
的防御副本,并在调用 get 时 return 它。 - 将您的 List 包装为不可变/不可修改的
List
和 return 它(或用它替换您原来的List
,然后您可以 return 安全地使用它而无需进一步包装它)
注意:您必须确保 Person
是不可变的,或者为 List
Person
创建防御副本
你可以做得更好
import java.util.Collections;
...
public List<Person> getPersons() {
return Collections.unmodifiableList( persons);
}
编辑的答案不仅解释了 Person
的可变版本的情况,还解释了 Person
.
您的 class 是可变的,因为您可以这样做:
Employees employees = new Employees();
employees.getPersons().add(new Person());
请注意,如果您更改代码以创建不可变的 class,请不要将人员列表传递给构造函数,您将没有用 class 保存一个空的人员列表,所以我认为有必要将 List<Person>
传递给构造函数。
现在有两种场景,实现方式不同:
Person
是不可变的Person
是可变的
场景 1 - Person
是不可变的
您只需在构造函数中创建 persons 参数的不可变副本。
您还需要制作final
class或至少getPersons
方法以确保没有人提供可变覆盖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
是可变的
您需要在 getPersons
方法中创建 persons
的深层副本。
您需要在构造函数上创建 persons
的深层副本。
您还需要制作final
class或至少getPersons
方法以确保没有人提供可变覆盖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());
答案在文档中找到 - A Strategy for Defining Immutable Objects:
不提供 "setter" 方法 — 修改字段或字段引用的对象的方法。
将所有字段设为最终字段和私有字段。
不允许子类覆盖方法。
如果实例字段包含对可变对象的引用,则不允许更改这些对象:
4.1。不要提供修改可变对象的方法。
4.2。不要共享对可变对象的引用。永远不要存储对传递给构造函数的外部可变对象的引用;如有必要,创建副本并存储对副本的引用。同样,必要时创建内部可变对象的副本,以避免在方法中返回原始对象。
我认为这种方法的问题在于我仍然可以执行 getPersons().add(x)。如果不允许这样做,您必须创建一个构造函数来接收人员列表,然后实例化您的内部最终列表,例如 ImmutableList。 其余代码对我来说似乎没问题。
编辑:
正如 Davide Lorenzo MARINO 指出的那样,这还不够。您还需要 return getter 中列表的深层副本,因此用户无法通过 getter returned 引用修改您的内部列表。
通过这样做,您可以将普通的 LinkedList 作为您的内部列表(私人最终)。在您的构造函数中,您创建了 List 的深层副本,在 getter 中,您 return 了内部 List 的深层副本。
在这种情况下,我赞成根本不公开 List 或 Map。如果你传出集合,你是否通过假设它必须存储为列表来破坏封装。
如果您不公开列表,则可以避免需要包装它或提供防御性副本。
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);
}
通过提供特定方法,您可以针对该用例优化代码。