【问题标题】:Good way to create a immutable class with modifiers (thread-safe)使用修饰符创建不可变类的好方法(线程安全)
【发布时间】:2014-06-25 15:43:50
【问题描述】:

我有一个案例,我想避免防御性副本,因为数据可能会被修改,但通常只是读取,而不是写入。所以,我想使用不可变对象和函数式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);
  }
}

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

【问题讨论】:

  • 如果你的类是不可变的,那么根据定义它不能有修饰符。但是一个相当有趣的问题......
  • 我所做的是只有一个线程本地的可变数据。这避免了复制大型对象的需要,并且可能会使用具有大型不可变对象的多个线程来执行。
  • 你想如何管理你的“而不是带有XX函数的setter”,每个属性一个只针对某些属性?
  • 实际上对于大多数属性。

标签: java multithreading immutability final volatile


【解决方案1】:

我建议你看看 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 成为最终类(如果类可以扩展);
  • 将所有字段设为最终字段和私有字段;
  • 初始化构造函数中的所有字段(如果您提供构建器和/或工厂方法,则可以是私有的);
  • 如果返回可变对象(可变集合、日期、第三方类等),则从 getter 中制作防御性副本。

【讨论】:

  • 谢谢,我喜欢你的系统避免重复方法的方式(不需要在构建器中有 setXXX 加上类中的 withXXX 方法......另外它还避免了对象数据的不必要重复修改了多个字段。
【解决方案2】:

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

public interface Person {
   String getName();
}

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

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

它不能解决根本问题,在我确实需要一个可变版本之前,我更喜欢不可变对象。 builder 方法效果很好,您可以将其集成到对象中以提供修饰符:

Person newPerson = existingPerson.withAge(30);

【讨论】:

    【解决方案3】:

    为什么不让你的字段为 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 之类的更大/更复杂的对象是否可行...
    • OP 表示实际的对象要大得多,并且可能会有很多修饰方法,这些方法很快就会变得非常复杂。
    • @GriffeyDog 我的解决方案不如 OP Builder 版本复杂,后者仍然使用带有所有参数的 Builder 构造函数 new PersonBuilder(name, surname); 所以我看不出抱怨是什么。无论在某些时候如何编写这些解决方案中的任何一个,都必须复制所有字段。
    • 构建器构造函数将原始的 Person 对象作为参数。诚然,它比您的解决方案更冗长,但它避免了具有长参数列表的构造函数,这可能难以阅读/维护。
    【解决方案4】:

    您的问题归结为:您需要一个安全地发布有效不可变对象的方法,该方法几乎但不完全忠实地复制有效不可变对象。

    我会选择构建器解决方案:它很冗长,因为所有内容都已退出,但 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);
            }
        }
    }
    

    【讨论】:

      【解决方案5】:

      取决于您打算更改多少字段。您可以制作特殊的 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 - 如果您想自动化传递过程,请不要忘记 Proxy 类。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-09-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多