使用修饰符创建不可变类的好方法(线程安全)

时间:2014-06-25 15:43:51

标签: java multithreading immutability final volatile

我有一个案例,当我想要避免防御性副本,对于可能仍然被修改的数据,但通常只是阅读,而不是写入。因此,我想使用不可变对象,使用函数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");

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

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

当然,我不能使用当前的实现来完成我的字段,因为我先复制,然后更改副本中的数据。

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

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

这里有什么问题,最重要的是,是否有更优雅的方式做同样的事情?

5 个答案:

答案 0 :(得分:3)

我建议您查看Guava的immutable collections,例如immutable list以及他们如何从构建器等创建列表。

成语如下:

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,因为您可以从现有实例创建副本。

最后,不要忘记关注guidelines for immutable classes

  • 让课程最终使所有的getter成为最终(如果该类可以扩展);
  • 将所有字段设为final和private;
  • 初始化构造函数中的所有字段(如果您提供构建器和/或工厂方法,则可以是私有的);
  • 如果返回可变对象(可变集合,日期,第三方类等),则从getter制作防御性副本。

答案 1 :(得分:1)

一种可能性是将围绕这些对象的接口分成不可变的变体(提供getter)和可变的变体(提供getter和setter)。

public interface Person {
   String getName();
}

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

它没有解决对象本身的可变性,但它提供了一些保证,当你使用不可变接口引用传递对象时,你知道你传递给它的代码不会改变你的对象。显然,您需要控制对底层对象的引用,并确定通过可变接口控制引用的功能子集。

它没有解决潜在的问题,我宁愿使用不可变对象,直到我肯定需要一个可变版本。构建器方法很好地工作,您可以将它集成到对象中以提供修饰符:

Person newPerson = existingPerson.withAge(30);

答案 2 :(得分:1)

为什么不让你的领域成为最终,你的修饰语方法直接创建新对象?

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

}

答案 3 :(得分: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);
        }
    }
}

答案 4 :(得分:0)

取决于您打算更改的字段数。您可以创建特殊的已更改对象,如:

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

这也可以缓解克隆重物的问题。