【问题标题】:Can we treat an object as immutable if we document its immutability如果我们记录一个对象的不可变性,我们可以将其视为不可变的吗
【发布时间】:2016-01-22 12:47:26
【问题描述】:

例如,我有这个:

public class Container{

    private final List<String> strs;

    /**
    * Contructs {@code Container} by a given {@code List}
    * The content of the list referenced should not be modified 
    * after the constructor invocation.
    */
    public Container(List<String> strs){
        this.strs = strs;
    }
    //Other staff
}

Container 的状态可以在构造后修改,但我们在文档中禁止。对象可以被视为不可变的吗?在构造函数中进行复制并不是完全可取的。

不变性是为了线程安全。

【问题讨论】:

  • 依靠 cmets 来指导程序员的选择是不好的,因为我们并不总是阅读 cmets。更多时候,我们依靠示例代码和直觉来决定可以用对象做什么。
  • “在构造函数中复制并不完全可取。”。好吧,但如果你不这样做,你就不能声称是不可变的。
  • 就个人而言,我会在构造函数中进行复制。这给了我一个我可以保证不会添加、重新排序或丢失元素的列表。但它是否应该是深拷贝(即元素本身是否应该是不可变的)取决于具体情况。
  • “调用”(“c”不是“k”)
  • 在我看来,问题不在于我们是否可以——当然我们可以——而是我们是否应该 .

标签: java multithreading immutability


【解决方案1】:

如果您不想复制列表,但仍希望它真正不可变,则可以使用 persistent data structure,例如 Clojure 中的那些。

在 Clojure 中,所有值都是不可变的,因此您始终可以安全地传递它们。当有人在列表的前面添加一个项目时,这在逻辑上会创建一个新列表,但实际上它只添加了一个指向旧列表头部的元素,而不必复制之前的所有列表。 Clojure 中的所有数据结构都是这样的,您也可以在 Java 中使用它们(另见what's a good persistent collections framework for use in java?)。

或者,您可以使用pcollections

【讨论】:

【解决方案2】:

您可以只记录它以提高性能,但我只建议将这种方法用于非公共 API。在我的公司,我们构建了一个框架并设法取消其中一些保护以提高性能/减少内存使用,但仅限于内部代码。我们决定假设框架开发人员不会乱来,而且效果很好。

如果您的 API 将暴露给外部的其他开发人员,您应该始终保持安全,特别是出于安全原因。如果您确实需要提高性能/内存使用率,这是一种选择,但请谨慎应用。

【讨论】:

    【解决方案3】:

    您通常不应该将类的内部结构暴露给外部。这对于不可变对象尤其重要。

    因此我建议在构造函数中使用new ArrayList(x) 进行复制。

    此外,您可以使用Collections.unmodifiableList() 来防止从您的类内部进行修改。

    我建议将两者结合起来,如下所示:

    import java.util.Collections;
    ...
    
    public Container(List<String> strs){
        this.strs = Collections.unmodifiableList(new ArrayList<>(strs));
    }
    

    您的容器将在调用构造函数时记住列表的成员。如果列表在其他地方发生更改,它不会对您的不可变对象产生任何影响。

    即使容器中的代码也无法修改列表 - 列表会抛出 UnsupportedOperationException

    完整的工作示例代码:

    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    
    public class X {
    
        public static void main(String[] args) {
    
            // create a list
            List<String> myList = new ArrayList<>();
            myList.add("a");
            myList.add("b");
    
            // hand it over to the container
            Container container = new Container(myList);
    
            // modify it afterwards
            myList.add("BUH!");
    
            // check contents of container
            for (String item : container.strs) {
                System.out.println(item);
            }
        }
    
    }
    
    class Container{
    
        final List<String> strs;
    
        /**
        * Contructs {@code Container} by a given {@code List}
        * The content of the list referenced should not be modified 
        * after the constructor invokation.
        */
        public Container(List<String> strs){
            this.strs = Collections.unmodifiableList(new ArrayList<>(strs));
        }
        //Other staff
    }
    

    它输出:

    a
    b
    

    (不输出BUH!

    【讨论】:

    • 好吧,如果你复制了,你就不需要再做unmodifiableList了(除非你想保护自己免受自己的伤害)。
    【解决方案4】:

    您决定允许这样的构造是在保证正确性和性能之间进行权衡。

    构建器模式可能为您提供第三种处理方式(取决于它是否适用于您的列表构建):

    要构造Container,请创建一个Builder,它收集列表元素,然后最终调用(私有)Container 构造函数。这样可以方便地收集列表元素并避免在构造最终对象时复制列表:

    public class Container {
         public static class Builder {
             public Builder add(String s) { strs.add(s); return this; }
             public Container create() { 
                   Container c = new Container(Collections.unmodifiableList(strs)); 
                   strs = null; 
                   return c; 
             }
             private List<String> strs = new ArrayList<>();
         }
    
         private final List<String> strs;
    
         private Container(List<String> strs){
              this.strs = strs;
         }
    }
    

    【讨论】:

    • 请注意它在构造实例后如何设置strs = null,这样调用者就不能在列表被用于构造之后继续构建。这样就保证不会再改变了。 (对 unmodifiableList 的调用并不是真正需要的,只是保护您的 Container 不受自身影响)。
    【解决方案5】:

    一般来说,不会。不可变对象可以在线程之间安全地共享。在线程之间安全共享的对象不仅仅是文档问题;它的类必须以特定的方式实现。说“这个类的对象是不可变的”而没有使其不可变的实现会积极误导,不真实,应该被视为错误。

    【讨论】:

    • 实际上,上面的类即使使用单个线程也会有问题(因为列表的内容可以改变,这很奇怪,即使它不会导致“内部不一致”的列表本身-- 这可能只发生在多个线程上)。
    最近更新 更多