创建一个带修饰符的不可变类的好方法(线程安全)

6
我有一个情况需要避免进行防御性拷贝,这种情况下数据可能会被修改,但通常只是读取而不写入。因此,我希望使用不可变对象,并使用函数式的mutator方法来修改它们,这是比较常见的做法(Java Lombok可以在很大程度上自动完成)。我的处理方式如下:
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() {....}         
}

所以,为了获取名称不同的人的副本,我需要调用
p= new Person("Bar", "Alfred");
...
p= p.withName("Foo");

实际上,这些对象相当大(我最终使用序列化来避免编写复制代码的负担)。

现在,在浏览网页时,我发现了这个实现中潜在的并发问题,因为我的字段不是final的,因此,并发访问可能会看到返回的副本,例如,没有新名称更改(因为在此上下文中不存在操作顺序的保证)。

当然,我不能使我的字段final,因为我首先要进行复制,然后再更改复制品中的数据。

所以,我正在寻找一个好的解决方案来解决这个问题。

我可以使用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);
  }
}

这里有问题吗?最重要的是,有没有更优雅的方法来完成相同的事情?


如果你的类是不可变的,那么根据定义它就不能有修饰符。但这是一个相当有趣的问题... - awksp
我的做法是使用仅限于一个线程的可变数据,这避免了需要复制大型对象并可以优化使用具有大型不可变对象的多个线程的性能。 - Peter Lawrey
1
你想如何管理你的“代替设置器与XX函数”,每个属性只针对特定属性? - Serge Ballesta
对于大多数属性来说,实际上是这样的。 - khaemuaset
5个回答

3
我建议您查看Guava的不可变集合,例如不可变列表以及它们如何从构建器创建列表等。
该习语的格式如下:
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

我很喜欢上面的成语。对于实体构建器,我会采取以下方法:
Person johnWayne = Person.builder()
  .firstName("John")
  .lastName("Wayne")
  .dob("05-26-1907")
  .build();

Person johnWayneClone = johnWayne.copy() // returns a builder!
  .dob("06-25-2014")
  .build();

通过copy()方法或者Person类上的静态方法(建议使用私有构造函数)可以从现有实例获取生成器。

请注意,上述方法类似于Scala的case classes,因为您可以从现有实例创建副本。

最后,请不要忘记遵循不可变类的指导方针

  • 将类设为final使所有getter方法都是final(如果该类可以被扩展);
  • 将所有字段设为final和private;
  • 在构造函数中初始化所有字段(如果提供了生成器和/或工厂方法,则可以将其设置为私有);
  • 如果返回可变对象(可变集合、日期、第三方类等),则从getter方法中进行防御性拷贝。

谢谢,我喜欢你的系统避免了方法的重复(在构建器中不需要setXXX和类中的withXXX方法...此外,当多个字段被修改时,它还避免了对象数据的不必要重复)。 - khaemuaset

1

其中一种可能性是将围绕这些对象的接口分为不可变版本(提供 getter)和可变版本(提供 getter 和 setter)。

public interface Person {
   String getName();
}

public interface MutablePerson extends Person {
   void setName(String name);
}

它并不解决对象的可变性本身,但是当使用 不可变接口 引用传递对象时,它提供了一些保证,您知道您将对象传递给的代码不会更改您的对象。显然,您需要控制对底层对象的引用,并通过可变接口确定具有引用控制的功能子集。
它并没有解决根本问题,我更倾向于使用不可变对象,直到我确实需要可变版本。生成器方法很好用,您可以将其集成到对象中以获得修改器,如下所示:
Person newPerson = existingPerson.withAge(30);

1
为什么不将你的字段设为final,并让修改器方法直接创建新对象?
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);         
    }

}

我不确定对于像OP这样更大/更复杂的对象来说,这种方法是否可行... - awksp
OP表示实际对象要大得多,可能会有许多修改器方法,使用你的解决方案很快就会变得非常复杂。 - GriffeyDog
@GriffeyDog 我的解决方案比 OP Builder 版本更简单,仍然使用带有所有参数的 Builder 构造函数 new PersonBuilder(name, surname);,所以我不知道抱怨在哪里。无论这些解决方案如何编写,最终都必须复制所有字段。 - dkatzel
构建器构造函数将原始的Person对象作为参数。诚然,它比您的解决方案更冗长,但它避免了具有长参数列表的构造函数,这可能很难阅读/维护。 - khaemuaset

0

你的问题归结为:你需要一个方法,可以安全地发布一个有效不可变、几乎但不完全忠实的对象副本。

我会选择使用构建器解决方案:虽然它非常冗长,但 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);
        }
    }
}

0

这取决于您打算更改多少个字段。您可以创建特殊的Changed对象,例如:

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

这也可能缓解您在克隆大型对象方面遇到的问题。


相当优雅 :-) (但是,恐怕我可能有太多的字段)。当然,它还会在内存中保留对象的所有连续状态,但如果我想保留历史记录(这确实是情况),这实际上可以成为一个功能。 - khaemuaset
@khaemuaset - 如果你想自动化传递过程,请不要忘记Proxy类。 - OldCurmudgeon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接