【问题标题】:strategy for defining immutable objects定义不可变对象的策略
【发布时间】:2016-08-14 15:40:20
【问题描述】:

我是 Java 新手,正在尝试通过 Java tutorial Oracle 学习定义不可变对象的策略。由于我的知识有限以及可能是如何措辞的,我真的很难理解下面的段落,我只是想知道是否有人可以向我解释它的实际含义。提前感谢您的帮助!

不要共享对可变对象的引用。从不存储引用 传递给构造函数的外部可变对象;如有必要, 创建副本,并存储对副本的引用。同样,创建 必要时避免使用内部可变对象的副本 在您的方法中返回原件。

【问题讨论】:

    标签: java object immutability


    【解决方案1】:

    不要共享对可变对象的引用

    public class A {
      private List<Integer> privateRef;
      private int privateVal = 0;
    
      public List<Integer> bad() {
          return this.privateRef;
      }
    
      public int good() {
          return privateVal;
      }
    
    }
    

    bad() 方法很糟糕,因为它公开了对私有成员的引用,从而使调用者能够这样做:

    A a = new A();
    List<Integer> extractedRef = a.bad();
    extractedRef.add(1);
    

    因此改变 a 的私有列表(如果您检查 a.privateList,您会看到它包含 1)。 A 不再控制其内部状态。

    good() 方法很好,因为即使调用者这样做:

    A a = new A();
    int number = a.good();
    number++;
    

    即使 number 为 1,a.privateVal 的值仍为 0。

    永远不要存储对传递给构造函数的外部可变对象的引用

    假设我们将这个构造函数添加到我们的类中:

    public class A {
        ...
        public A(List<Integer> anotherList) {
            this.privateRef = anotherList;
        } 
        ...
    }
    

    我们处于类似情况:私有列表的突变可能发生在外部:

    List<Integer> list = new ArrayList<Integer>()
    A a = new A(list);
    list.add(1);
    

    我们已经改变了 a 的内部状态(如果您检查 a.privateList,您会看到它包含 1)。

    如有必要,创建副本并存储对副本的引用。

    也就是说,如果你希望 A 是不可变的,你应该这样做:

    public A(List<Integer> anotherList) {
        this.privateRef = new ArrayList<>(anotherList);
    } 
    

    这样,A 的新实例在创建时会获得列表的副本,该副本将成为其内部状态的一部分。从这一点来看,对给定列表(示例中的“列表”)的突变不会影响 a 的内部状态:只有 a 可以改变其私有列表。

    同样,必要时创建内部可变对象的副本,以避免在方法中返回原始对象。

    这是您解决第一个示例的方法,如果 A 想要公开其内部可变列表,则不应这样做:

      public List<Integer> bad() {
          return this.privateRef;
      }
    

    但是

      public List<Integer> better() {
          return new ArrayList<>(this.privateRef);
      }
    

    此时

    A a = new A();
    List<Integer> extractedRef = a.better();
    extractedRef.add(1)
    

    此时,a.privateRef 仍然是空的(因此它的状态受到保护,不会受到外部突变的影响)。但是extractedRef 将包含1。

    补充一点。请注意,即使此类应用了上述所有原则:

    public class A {
        private List<Integer> privateRef = new ArrayList<>();
        private int privateVal = 0;
    
        public A(List) {
            this.privateRef.addAll(this.privateRef);
        }
    
        public List<Integer> getList() {
            return new ArrayList<>(this.privateRef);
        }
    
        public int getVal() {
            return privateVal;
        }
    
        public void mutate() {
            this.privateVal++;
            this.privateRef.clear();
        }
    
    }
    

    (它不公开对可变对象的引用,也不保留对外部可变对象的引用),它并不是真正不可变的,因为有一种方法可以改变其内部状态(调用 mutate() )。

    您当然可以删除 mutate(),但更正确的不变性替代方案可能是:

    public class A {
        private final List<Integer> privateRef = new ArrayList<>();
        private final int privateVal = 0;
    
        public A(List) {
            this.privateRef.addAll(this.privateRef);
        }
    
        public List<Integer> getList() {
            return new ArrayList<>(this.privateRef);
        }
    
        public int getVal() {
            return privateVal;
        }
    
    }
    

    (我还没有编译示例,但它们应该几乎没问题)。

    【讨论】:

      【解决方案2】:

      在创建不可变对象时,如果有像 Date 对象这样的可变字段,则适用以下几行:

      不要共享对可变对象的引用。永远不要存储对传递给构造函数的外部可变对象的引用;如有必要,创建副本并存储对副本的引用。同样,在必要时创建内部可变对象的副本,以避免在方法中返回原始对象。

      同样可以实现如下:

      • 在创建不可变对象时创建 Date 对象的防御性副本。
      • 不要为此日期对象提供任何设置方法。
      • 如果您需要提供 getter 方法,那么 getter 方法应该返回日期字段的副本,因为 Date 本身是可变的。在这里,clone() 方法有助于实现这一目标。

      现在,由于类中没有引用 Date 字段,因此该类之外的任何人都无法修改 Immutable 类的状态。

      查看Example for Immutable class

      【讨论】:

        【解决方案3】:

        例子:

        /**
         * A simple mutable.
         */
        class Mutable {
        
            private int n;
        
            public Mutable(int n) {
                this.n = n;
            }
        
            public int getN() {
                return n;
            }
        
            public void setN(int n) {
                this.n = n;
            }
        
            @Override
            public String toString() {
                return "Mutable{" + "n=" + n + '}';
            }
        
        }
        
        // A thing that holds things.
        class Thing<T> {
        
            T m;
        
            public Thing(T m) {
                this.m = m;
            }
        
            public T getM() {
                return m;
            }
        
            @Override
            public String toString() {
                return "Thing{" + "m=" + m + '}';
            }
        
        }
        
        public void test() {
            // Never store references to external, mutable objects passed to the constructor
            Mutable m = new Mutable(10);
            // t10 holds a reference to my mutable `m`, currently containing `10`
            Thing<Mutable> t10 = new Thing<>(m);
            // Now `m` holds `50`, even the one in `t10`.
            m.setN(50);
            // Make the new one holding `m` at value `50` now.
            Thing<Mutable> t50 = new Thing<>(m);
            // Both print the same because they both hold the same `m`
            System.out.println("t10 = " + t10 + " t50 = " + t50);
            // We can even mess with it after the fact - this is why you should return a copy.
            t50.getM().setN(42);
            // Both print the same because they both hold the same `m`
            System.out.println("t10 = " + t10 + " t50 = " + t50);
        }
        

        这证明了所有三个观点。

        • 如果您将 mutable 传递给构造函数并将该可变对象保持在状态中,则对该可变对象的更改将更改对象的状态。

        • 如果您返回 mutable,它也可以在您的对象之外进行修改,从而改变您的状态。

        为了避免这种情况:

        • 如果可能,在所有情况下都使用immutable
        • 如果必须在构造函数中使用可变变量,请尽可能复制它。
        • 如果您在任何地方返回可变对象,请尽可能返回副本。

        【讨论】:

          【解决方案4】:

          实际上,它表示您不想将您的状态暴露给外部世界,因此外部世界可以根据需要修改您的内部状态。您不希望这样做,因为这是您在内部状态下的业务,并且有人可以更改它。这也是为什么它建议创建内部可变对象的副本并将它们作为副本传递给外部的原因。它们会更改这些副本,但不会对您的内部状态产生副作用。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2013-08-05
            • 2018-10-26
            • 2011-04-03
            • 2021-05-18
            • 2023-03-25
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多