Java 创建带有修饰符的不可变类的好方法(线程安全)
我有一种情况,我想避免防御性拷贝,因为数据可能会被修改,但通常只是读取,而不是写入。所以,我想使用不可变对象和函数mutator方法,这是一种常见的方法(JavaLombok或多或少能够自动完成)。我的做法如下:Java 创建带有修饰符的不可变类的好方法(线程安全),java,multithreading,immutability,final,volatile,Java,Multithreading,Immutability,Final,Volatile,我有一种情况,我想避免防御性拷贝,因为数据可能会被修改,但通常只是读取,而不是写入。所以,我想使用不可变对象和函数mutator方法,这是一种常见的方法(JavaLombok或多或少能够自动完成)。我的做法如下: public class Person { private String name, surname; public Person(String name, String surname) {....} // getters... // and ins
public class Person {
private String name, surname;
public Person(String name, String surname) {....}
// getters...
// and instead of setters
public Person withName(String name) {
Person p= copy(); // create a copy of this...
p.name= name;
return p;
}
public Person copy() {....}
}
List<String> list1 = ImmutableList.of("a","b","c"); // factory method
List<String> list2 = ImmutableList.builder() // builder pattern
.add("a")
.add("b")
.add("c")
.build();
List<String> list3 = ... // created by other means
List<String> immutableList3 = ImmutableList.copyOf(list3); // immutable copy, lazy if already immutable
所以,为了得到一个不同名字的人的副本,我会打电话给
p= new Person("Bar", "Alfred");
...
p= p.withName("Foo");
实际上,对象相当大(我最终使用序列化来避免编写复制代码的负担)
现在,在浏览web时,我发现此实现存在一个潜在的并发问题,因为我的字段不是最终字段,因此,并发访问可能会看到返回的副本,例如,在没有更改新名称的情况下(因为在此上下文中,操作顺序没有保证)
当然,在当前的实现中,我不能将字段设置为最终字段,因为我首先复制,然后更改副本中的数据
所以,我正在寻找解决这个问题的好方法
我可能会使用volatile,但我觉得这不是一个好的解决方案
另一个解决方案是使用生成器模式:
class PersonBuilder {
String name, surname; ....
}
public class Person {
private final String name, surname;
public Person(PersonBuilder builder) {...}
private PersonBuilder getBuilder() {
return new PersonBuilder(name, surname);
}
public Person withName(String name) {
PersonBuilder b= getBuilder();
b.setName(name);
return new Person(b);
}
}
这里有什么问题吗?最重要的是,有没有更优雅的方法来做同样的事情?一种可能性是将围绕这些对象的接口分为不可变变量(提供getter)和可变变量(提供getter和setter) 它并没有解决对象本身的易变性,但它确实提供了一些保证,当您使用不可变接口引用传递对象时,您知道您要传递给的代码不会更改您的对象。显然,您需要控制对底层对象的引用,并确定通过可变接口控制引用的功能子集 它不能解决根本的问题,在我确实需要一个可变的版本之前,我倾向于使用不可变的对象。生成器方法工作得很好,您可以将其集成到对象中以提供修改器,因此:
Person newPerson = existingPerson.withAge(30);
为什么不将字段设置为最终字段,并直接使用修改器方法创建新对象
public class Person {
private final String name, surname;
public Person(String name, String surname) {....}
// getters...
// and instead of setters
public Person withName(String newName) {
return new Person(newName, surname);
}
}
您的问题可以归结为:您需要一个安全地发布有效不可变对象的几乎但不完全可靠的副本的方法 我将使用构建器解决方案:它在所有输出时都是冗长的,但Eclipse有助于实现这一点,并且它允许所有已发布的对象实际上都是不可变的。实际的不变性使得安全发布成为一件不需要动脑筋的事情 如果是我写的,它会是这样的:
class Person {
public static final FooType DEFAULT_FOO = ...;
public static final BarType DEFAULT_BAR = ...;
public static final BazType DEFAULT_BAZ = ...;
...
private final FooType foo;
private final BarType bar;
private final BazType baz;
...
private Person(Builder builder) {
this.foo = builder.foo;
this.bar = builder.bar;
this.baz = builder.baz;
...
}
public FooType getFoo() { return foo; }
public BarType getBar() { return bar; }
public BazType getBaz() { return baz; }
...
public Person cloneWith(FooType foo) {
return new Builder(this).setFoo(foo).build();
}
public Person cloneWith(BarType bar) {
return new Builder(this).setBar(bar).build();
}
public Person cloneWith(FooType foo, BarType bar) {
return new Builder(this).setFoo(foo).setBar(bar).build();
}
...
public class Builder{
private FooType foo;
private BarType bar;
private BazType baz;
...
public Builder() {
foo = DEFAULT_FOO;
bar = DEFAULT_BAR;
baz = DEFAULT_BAZ;
...
}
public Builder(Person person) {
foo = person.foo;
bar = person.bar;
baz = person.baz;
...
}
public Builder setFoo(FooType foo) {
this.foo = foo;
return this;
}
public Builder setBar(BarType bar) {
this.bar = bar;
return this;
}
public Builder setBaz(BazType baz) {
this.baz = baz;
return this;
}
...
public Person build() {
return new Person(this);
}
}
}
取决于要更改的字段数。您可以创建特殊的更改对象,如:
interface Person {
public String getForeName();
public String getSurName();
}
class RealPerson implements Person {
private final String foreName;
private final String surName;
public RealPerson (String foreName, String surName) {
this.foreName = foreName;
this.surName = surName;
}
@Override
public String getForeName() {
return foreName;
}
@Override
public String getSurName() {
return surName;
}
public Person setSurName (String surName) {
return new PersonWithSurnameChanged(this, surName);
}
}
class PersonWithSurnameChanged implements Person {
final Person original;
final String surName;
public PersonWithSurnameChanged (Person original, String surName) {
this.original = original;
this.surName = surName;
}
@Override
public String getForeName() {
return original.getForeName();
}
@Override
public String getSurName() {
return surName;
}
}
这还可以缓解克隆重对象时遇到的问题 我建议你看看番石榴,比如它们如何从建设者那里创建列表等等 成语如下:
public class Person {
private String name, surname;
public Person(String name, String surname) {....}
// getters...
// and instead of setters
public Person withName(String name) {
Person p= copy(); // create a copy of this...
p.name= name;
return p;
}
public Person copy() {....}
}
List<String> list1 = ImmutableList.of("a","b","c"); // factory method
List<String> list2 = ImmutableList.builder() // builder pattern
.add("a")
.add("b")
.add("c")
.build();
List<String> list3 = ... // created by other means
List<String> immutableList3 = ImmutableList.copyOf(list3); // immutable copy, lazy if already immutable
这里的生成器可以通过copy()
方法从现有实例获取,或者通过Person
类(建议使用私有构造函数)上返回Person生成器的静态方法获取
请注意,上面模拟了一点Scala,您可以从现有实例创建副本
最后,别忘了遵循以下原则:
- 使课程成为最终课程或使所有获得者成为最终课程(如果课程可以扩展)李>
- 使所有字段成为最终字段和私有字段李>
- 初始化构造函数中的所有字段(如果提供生成器和/或工厂方法,则可以是私有的)李>
- 如果返回可变对象(可变集合、日期、第三方类等),则从getter创建防御副本
new PersonBuilder(姓名)代码>所以我不知道投诉是什么。无论这些解决方案中的任何一个在某一点上是如何编写的,所有字段都必须复制过来。构建器构造函数将以原始Person对象作为参数。无可否认,它比您的解决方案更为冗长,但它避免了具有长参数列表的构造函数,这可能很难读取/维护。非常优雅:-)(但是,唉,我可能有太多的字段)。当然,它还将对象的所有连续状态保存在内存中,但如果我想保存历史记录(事实确实如此),这实际上是一项功能。@khaemuaset-如果您想自动化传递过程,请不要忘记该类。谢谢,我喜欢您的系统避免重复方法的方式(无需在生成器中使用setXXX,在类中使用withXXX方法……此外,当修改多个字段时,还可以避免不必要的对象数据复制。