我们如何通过可变引用来维护类的不变性

时间:2015-12-05 18:39:10

标签: java collections immutability

我知道使我们的类不可变的所有基本规则但是当有另一个类引用时我有点困惑。我知道如果有集合而不是Address那么我们可以使用Collections.unmodifiableList(new ArrayList<>(modifiable));然后我们可以使我们的类不可变。但在下面的情况下,我仍然无法得到这个概念。

public final class Employee{
    private final int id;
    private Address address;
    public Employee(int id, Address address)
    {
        this.id = id;
        this.address=address;
    }
    public int getId(){
        return id;
    }
    public Address getAddress(){
        return address;
    }
}

public class Address{
    private String street;
    public String getStreet(){
        return street;
    }
    public void setStreet(String street){
        this.street = street;
    }
}

7 个答案:

答案 0 :(得分:10)

嗯,这个概念是阅读JLS并理解它。在这种情况下,JLS说:

  

final字段还允许程序员在没有同步的情况下实现线程安全的不可变对象。线程安全的不可变对象被所有线程视为不可变的,即使使用数据争用传递线程之间的不可变对象的引用也是如此。这可以提供安全保证,防止错误或恶意代码滥用不可变类。必须正确使用最终字段以保证不变性。

     

最终字段的使用模型很简单:在该对象的构造函数中设置对象的最终字段;并且在对象的构造函数完成之前,不要在另一个线程可以看到的地方写入对正在构造的对象的引用。如果遵循这一点,那么当另一个线程看到该对象时,该线程将始终看到该对象的最终字段的正确构造版本。它还将看到那些最终字段引用的任何对象或数组的版本,这些字段至少与最终字段一样是最新的。

所以你需要:

  1. address设为最终版和私密版。
  2. 对于任何可变对象,您必须防止从外部看到对该对象的引用。
  3. 在这种情况下,#2可能意味着您无法像getAddress()那样返回对地址的引用。 你必须在构造函数中制作一个防御性副本。即,制作任何可变参数的副本,并将副本存储在Employee中。如果你不能制作防御性副本,那么就没有办法让员工不变。

    public final class Employee{
        private final int id;
        private final Address address;
        public Employee(int id, Address address)
        {
            this.id = id;
            this.address=new Address();  // defensive copy
            this.address.setStreet( address.getStreet() );
        }
        pulbic int getId(){
            return id;
        }
        public Address getAddress() {
            Address nuAdd = new Address(); // must copy here too
            nuAdd.setStreet( address.getStreet() );
            return nuAdd;
    }
    

    实现clone()或类似的东西(复制ctor)会使复杂的类更容易创建防御对象。但是,我认为最好的建议是使Address不可变。一旦你这样做,你就可以自由地传递它的参考,而不会出现任何线程安全问题。

    在此示例中,请注意我 NOT 必须复制street的值。 Street是一个String,字符串是不可变的。如果street由可变字段(例如整数街道号码)组成,那么我 也必须复制street,等等无限制地。这就是为什么不可变对象是如此有价值的原因,它们打破了无限复制&#34;链

答案 1 :(得分:7)

Java docs

提供了一些步骤
  

定义不可变对象的策略

     

以下规则定义了创建不可变的简单策略   对象。并非所有课程都记录为&#34;不可变的&#34;遵循这些规则。   这并不一定意味着这些类的创建者   马虎 - 他们可能有充分的理由相信这种情况   他们的班级在施工后从未改变过但是,这样的   策略需要复杂的分析,不适合初学者。

     
      
  • 不要提供&#34; setter&#34; methods - 修改字段或对象的方法   字段提到。
  •   
  • 将所有字段设为最终和私有。
  •   
  • 不允许   子类重写方法。最简单的方法是   将该类声明为final。更复杂的方法是制作   构造函数private和在工厂方法中构造实例。
  •   
  • 如果   实例字段包括对可变对象的引用,不允许   那些要改变的对象:      
        
    • 不提供修改方法   可变对象。
    •   
    • 不要共享对可变对象的引用。决不   存储对传递给的外部可变对象的引用   构造函数;如有必要,创建副本,并存储对的引用   副本。同样,在创建内部可变对象的副本时   必须避免在方法中返回原件。
    •   
  •   

地址类是可变的,因为您可以使用setStreet方法对其进行修改。 所以其他类可以修改这个类。

我们可以通过传入Address实例的副本来防止这种情况,而不是信任对我们给出的实例的引用。

使地址对象最终

private final Address address;

其次,

this.address = new Address(address.getStreet());

在Address类中创建构造函数,为Street设置Street.Remove setter方法。

最后代替

public Address getAddress(){
    return address;
} 

使用

public Address getAddress(){
    return new Address(address.getStreet());
}

答案 2 :(得分:1)

如果要将可变对象封装到不可变对象中,则需要:

  1. 创建可变对象的副本(即通过复制构造函数,克隆,序列化/反序列化等); 永远不要存储对原始可变对象的引用。
  2. 永远不要返回可变对象。如果必须,则返回该对象的副本
  3. 避免使用可以更改可变对象的方法。
  4. public Employee(int id,Address address){

            this.id = id;
            this.address=new Address();  
            this.address.setStreet( address.getStreet() );
        }
    
    
    
    public Address getAddress() {
            Address nuAdd = new Address(); // must copy here too
            nuAdd.setStreet( address.getStreet() );
            return nuAdd;
    }
    

答案 3 :(得分:1)

What is difference between the creating the object of employee calling the clonne method.

1)在构造函数和getter方法内部创建雇员对象,并通过在构造函数内部创建雇员的对象并将雇员的必填字段设置为必需的值来设置所需的值。

Example : 

public final class Employee{
    private final int id;
    private final Address address;
    public Employee(int id, Address address)
    {
        this.id = id;
        this.address=new Address();  // defensive copy
        this.address.setStreet( address.getStreet() );
    }
    public int getId(){
        return id;
    }
    public Address getAddress() {
        Address nuAdd = new Address(); // must copy here too
        nuAdd.setStreet( address.getStreet() );
        return nuAdd;
}

2) cloning the employee object inside constructor and getter method as below.
Example : 

public final class Employee{
    private final int id;
    private Address address;
    public Employee(int id, Address address)
    {
        this.id = id;
        this.address=address.clone();
    }
    public int getId(){
        return id;
    }
    public Address getAddress(){
        return address.clone();
    }
}

答案 4 :(得分:0)

因此,在您的示例Employee中,类是不可变的,因为一旦创建它,​​您就无法更改其状态,因为它只有getter方法。

Address类为mutable,因为您可以使用setStreet方法对其进行修改。

因此,如果您有其他使用Address对象的类,则可以确定该类无法修改对象状态。

答案 5 :(得分:0)

您还可以使用克隆

来使用浅拷贝
public final class Employee{
    private final int id;
    private Address address;
    public Employee(int id, Address address)
    {
        this.id = id;
        this.address=address.clone();
    }
    public int getId(){
        return id;
    }
    public Address getAddress(){
        return address.clone();
    }
}

使用它将在Employee类中创建一个单独的Address对象,因此在这种情况下,对在Employee构造函数中作为参数传递的Address对象所做的任何更改都不会更改Employee类的成员变量Address对象。

getAddress()方法也返回一个克隆对象,因此对此方法提取的对象所做的任何更改都不会影响Employee类的地址对象。

注意: 要使用它,请使地址类Cloneable。

答案 6 :(得分:0)

在Employee类中,在getAddress()方法中返回地址实例的深层克隆副本,而不是返回原始实例。这样,如果有人更改了地址实例,它就不能反映原始实例。但是一个条件是,Address类必须实现Cloneable接口。

public final class Employee{
     private final int id;
     private Address address;
     public Employee(int id, Address address) {
          this.id = id;
          this.address=address;
     }
     public int getId(){
          return id;
     }
     public Address getAddress() throws CloneNotSupportedException{
          return (Address) address.clone();
     }

}

参考:-Immutable Class in java