如何在Java中创建不可变对象?


83

如何在Java中创建不可变对象?

哪些对象应称为不可变的?

如果我的所有静态成员都上课,那是不可变的吗?


可能的重复是什么?
Joachim Sauer

1
上面链接的问题并不相同,但是该问题的答案应该可以回答您的所有问题。
Joachim Sauer

如果您的类都是静态成员,则它是无状态的(没有实例具有单独的状态),并且可变或不可变的问题变得毫无意义。
塞巴斯蒂安·雷德尔2014年

除了构造函数外,还有没有其他方法可以初始化字段。我班上有20多个领域。使用构造函数初始化所有字段非常困难,有些字段甚至是可选的。
Nikhil Mishra '18

Answers:


88

以下是不可变对象的严格要求。

  1. 最终上课
  2. 使所有成员成为最终成员,在静态块或构造函数中显式设置它们
  3. 将所有成员设为私人
  4. 否修改状态的方法
  5. 要特别注意限制对可变成员的访问(请记住,该字段可能final是可变的,但对象仍然可以是可变的。即private final Date imStillMutable)。defensive copies在这种情况下,您应该这样做。

上课的final原因非常微妙,常常被忽视。如果不是最终的人,他们可以自由地扩展您的类,覆盖publicprotected行为,添加可变属性,然后提供其子类作为替代。通过声明该类,final您可以确保不会发生这种情况。

要查看实际问题,请考虑以下示例:

public class MyApp{

    /**
     * @param args
     */
    public static void main(String[] args){

        System.out.println("Hello World!");

        OhNoMutable mutable = new OhNoMutable(1, 2);
        ImSoImmutable immutable = mutable;

        /*
         * Ahhhh Prints out 3 just like I always wanted
         * and I can rely on this super immutable class 
         * never changing. So its thread safe and perfect
         */
        System.out.println(immutable.add());

        /* Some sneak programmer changes a mutable field on the subclass */
        mutable.field3=4;

        /*
         * Ahhh let me just print my immutable 
         * reference again because I can trust it 
         * so much.
         * 
         */
        System.out.println(immutable.add());

        /* Why is this buggy piece of crap printing 7 and not 3
           It couldn't have changed its IMMUTABLE!!!! 
         */
    }

}

/* This class adheres to all the principles of 
*  good immutable classes. All the members are private final
*  the add() method doesn't modify any state. This class is 
*  just a thing of beauty. Its only missing one thing
*  I didn't declare the class final. Let the chaos ensue
*/ 
public class ImSoImmutable{
    private final int field1;
    private final int field2;

    public ImSoImmutable(int field1, int field2){
        this.field1 = field1;
        this.field2 = field2;
    }

    public int add(){
        return field1+field2;
    }
}

/*
This class is the problem. The problem is the 
overridden method add(). Because it uses a mutable 
member it means that I can't  guarantee that all instances
of ImSoImmutable are actually immutable.
*/ 
public class OhNoMutable extends ImSoImmutable{   

    public int field3 = 0;

    public OhNoMutable(int field1, int field2){
        super(field1, field2);          
    }

    public int add(){
       return super.add()+field3;  
    }

}

实际上,在依赖注入环境中遇到上述问题是很常见的。您没有显式实例化事物,并且给出的超类引用实际上可能是子类。

要解决的问题是,要坚决保证不变性,您必须将类标记为final。这在Joshua Bloch的Effective Java中进行了深入介绍,并在Java内存模型规范中明确引用。


那么所有静态成员呢?
尼尔·萨尔普

1
该课程不需要为此而定。
Angel O'Sphere 2011年

12
@Nilesh:不变性是instance的属性。静态成员通常不与任何单个实例相关,因此它们不在此处。
Joachim Sauer

4
约书亚·布洛赫(Joshua Bloch)关于不可变性的第15项-没有修改状态的方法,所有字段均为最终字段,所有字段均为私有,确保不能扩展该类,确保对任何可变组件的专有访问。
nsfyn55 2011年

2
@Jaochim-它们绝对是等式的一部分-如果我添加一个可变的静态成员并在ImSoImmutable的add函数中使用它,请使用上面的示例,您也会遇到同样的问题。如果一个类是不可变的,那么所有方面都必须是不可变的。
nsfyn55 2011年

14

只是不要在类中添加公共mutator(setter)方法。


那么所有静态成员呢?此类对象的引用或对象状态是否会发生变化?
Neel Salpe 2011年

7
没关系 如果您无法通过某种方法在外部进行更改,那么它是不可变的。
BalusC

无法回答,因为我们不知道静态成员会做什么...他们可能会修改私有字段。如果他们这样做,那该阶级就不会是不可替代的。
Angel O'Sphere 2011年

并且类的默认构造函数应为private或类应为final。只是为了避免继承。因为继承违反了封装。
塔拉·艾哈迈德·汗

将可变对象(例如List)传递给不可变对象然后从外部更改它,该怎么办?这是可能的,应该在对象创建过程中使用防御性副本来应对
Yassin Hajaj

14

类不是一成不变的,对象是一成不变的。

不变是指:初始化后,我的公开可见状态无法更改。

字段不必声明为final,尽管它可以极大地确保线程安全

如果您的类只有静态成员,则该类的对象是不可变的,因为您无法更改该对象的状态(也可能无法创建它:)。


3
将所有字段设置为静态会限制所有实例共享相同的状态,这实际上没有用。
2011年

6

要使类在Java中不可变,可以注意以下几点:

1.不要提供setter方法来修改该类的任何实例变量的值。

2.将课程声明为“最终”。这将防止任何其他类对其进行扩展,从而阻止其覆盖可能修改实例变量值的任何方法。

3.将实例变量声明为private和final

4.您还可以将类的构造函数声明为私有,并在需要时添加工厂方法来创建类的实例。

这些要点应该有帮助!


3
WRT#4构造函数可见性如何影响可变性?字符串是不可变的,但具有多个公共构造函数。
瑞安

正如@Ryan所说的,实例变量也是如此:为什么要声明这些变量private
MC Emperor'1

不知道为什么这个答案有任何不赞成。这个答案是不完整的。它没有谈论可变对象,这是需要解决的重要问题。请阅读@ nsfyn55的说明以获得更好的理解。
Ketan R

4

oracle站点开始,如何在Java中创建不可变对象。

  1. 不要提供“ setter”方法-修改字段或字段引用的对象的方法。
  2. 将所有字段定为私有字段。
  3. 不允许子类覆盖方法。最简单的方法是将类声明为final。一种更复杂的方法是使构造函数私有,并在工厂方法中构造实例。
  4. 如果实例字段包含对可变对象的引用,则不允许更改这些对象:
    I.不提供修改可变对象的方法。
    二。不要共享对可变对象的引用。永远不要存储对传递给构造函数的外部可变对象的引用;如有必要,请创建副本,并存储对副本的引用。同样,在必要时创建内部可变对象的副本,以避免在方法中返回原始对象。

3

不变对象是创建后不会更改其内部状态的对象。它们在多线程应用程序中非常有用,因为它们可以在线程之间共享而无需同步。

要创建一个不变的对象,您需要遵循一些简单的规则:

1.不要添加任何设置方法

如果您要构建一个不可变的对象,其内部状态将永远不会改变。setter方法的任务是更改字段的内部值,因此您无法添加它。

2.声明所有字段为最终字段和私有字段

从班级外部看不到私有字段,因此无法对其进行任何手动更改。

声明字段final将保证,如果它引用原始值,则该值在引用对象时永远不会改变,引用也不会更改。这还不足以确保只有私有final字段的对象是不可变的。

3.如果字段是可变对象,则为获取方法创建其防御性副本

我们之前已经看到,仅定义字段final和private是不够的,因为可以更改其内部状态。为了解决这个问题,我们需要创建该字段的防御性副本,并在每次请求时返回该字段。

4.如果必须将传递给构造函数的可变对象分配给字段,请为其创建防御性副本

如果您保留传递给构造函数的引用,则会发生相同的问题,因为可以更改它。因此,持有对传递给构造函数的对象的引用可以创建可变的对象。为了解决此问题,如果参数是可变对象,则必须创建该参数的防御性副本。

请注意,如果字段是对不可变对象的引用,则不必在构造函数中以及在getter方法中创建其防卫副本,将字段定义为final和private即可。

5.不允许子类覆盖方法

如果子类覆盖方法,则它可以返回可变字段的原始值,而不是其防御性副本。

要解决此问题,可以执行以下任一操作:

  1. 将不可变的类声明为final,这样就不能扩展它
  2. 声明不可变类final的所有方法,以使它们无法被覆盖
  3. 创建私有构造函数和工厂以创建不可变类的实例,因为带有私有构造函数的类无法扩展

如果遵循这些简单规则,则可以在线程之间自由共享不可变对象,因为它们是线程安全的!

以下是一些值得注意的地方:

  • 在许多情况下,不可变的对象确实确实使生活变得更简单。它们特别适用于值类型,在这些类型中,对象没有身份,因此可以轻松替换它们,并且可以使并发编程的方式更安全,更干净(众所周知,大多数并发错误最终都是由相互之间共享的可变状态导致的)线程)。 但是,对于大型和/或复杂的对象,为每个单个更改创建对象的新副本可能会非常昂贵和/或乏味。对于具有独特标识的对象,更改现有对象比创建新的,修改后的副本要简单和直观得多。
  • 对于不可变对象,有些事情是您做不到的,例如具有双向关系。一旦在一个对象上设置了关联值,它的身份就会改变。因此,您在另一个对象上设置了新值,并且它也发生了变化。问题在于第一个对象的引用不再有效,因为已经创建了一个新实例来用引用表示该对象。继续这样做只会导致无限回归。
  • 为了实现二叉搜索树,您每次都必须返回一棵新树:您的新树将必须为每个已修改的节点制作一个副本(共享未修改的分支)。对于您的插入功能来说还算不错,但是对我来说,当我开始进行删除和重新平衡时,事情很快就变得效率低下。
  • Hibernate和JPA本质上要求您的系统使用可变对象,因为它们的全部前提是它们检测并保存对数据对象的更改。
  • 根据语言的不同,编译器可以在处理不可变数据时进行一系列优化,因为它知道数据永远不会改变。各种东西都被跳过了,这为您带来了巨大的性能优势。
  • 如果您查看其他已知的JVM语言(Scala和Clojure),则在代码中很少看到可变对象,这就是为什么人们在单线程还不够的情况下开始使用它们的原因。

没有对与错,这取决于您的喜好。这仅取决于您的喜好以及要实现的目标(并且能够轻松使用这两种方法而不会疏远一方或另一方的顽固支持者是某些语言所追求的圣杯)。


2
  • 不要提供“ setter”方法-修改字段或字段引用的对象的方法。
  • 将所有字段定为私有字段。
  • 不允许子类覆盖方法。最简单的方法是将类声明为final。一种更复杂的方法是使构造函数私有,并在工厂方法中构造实例。
  • 如果实例字段包括对可变对象的引用,则不允许更改这些对象:
    • 不要提供修改可变对象的方法。
    • 不要共享对可变对象的引用。永远不要存储对传递给构造函数的外部可变对象的引用;如有必要,请创建副本,并存储对副本的引用。同样,在必要时创建内部可变对象的副本,以避免在方法中返回原始对象。

2

首先,您知道为什么需要创建不可变对象,以及不可变对象的优点是什么。

不可变对象的优点

并发和多线程自动线程安全,因此同步问题...等

不需要复制构造函数 不需要执行克隆。 不能覆盖类 使字段成为私有和最终的 Force调用者,以一步完成一个对象,而不是使用无参数构造函数

不可变的对象只是对象,其状态意味着在构造了不可变的对象之后,对象的数据无法更改。

请参见下面的代码。

public final class ImmutableReminder{
    private final Date remindingDate;

    public ImmutableReminder (Date remindingDate) {
        if(remindingDate.getTime() < System.currentTimeMillis()){
            throw new IllegalArgumentException("Can not set reminder" +
                    " for past time: " + remindingDate);
        }
        this.remindingDate = new Date(remindingDate.getTime());
    }

    public Date getRemindingDate() {
        return (Date) remindingDate.clone();
    }
}

2

最小化可变性

不可变类只是其实例无法修改的类。每个实例中包含的所有信息在创建时都会提供,并且在对象的生命周期内是固定的。

JDK不可变类:字符串,装箱的原始类(包装类),BigInteger和BigDecimal等。

如何使一类不变?

  1. 不要提供任何修改对象状态的方法(称为增变器)。
  2. 确保该类不能扩展。
  3. 将所有字段定为最终值。
  4. 将所有字段设为私有。这样可以防止客户端访问由字段引用的可变对象并直接修改这些对象。
  5. 制作防御性副本。确保以独占方式访问任何可变组件。

    公共列表getList(){返回Collections.unmodifiableList(list); <===可变字段的防御性副本,然后将其返回给调用方}

如果您的类具有引用可变对象的任何字段,请确保该类的客户端无法获取对这些对象的引用。切勿将此类字段初始化为客户端提供的对象引用,也不要从访问器返回该对象引用。

import java.util.Date;
public final class ImmutableClass {

       public ImmutableClass(int id, String name, Date doj) {
              this.id = id;
              this.name = name;
              this.doj = doj;
       }

       private final int id;
       private final String name;
       private final Date doj;

       public int getId() {
              return id;
       }
       public String getName() {
              return name;
       }

     /**
      * Date class is mutable so we need a little care here.
      * We should not return the reference of original instance variable.
      * Instead a new Date object, with content copied to it, should be returned.
      * */
       public Date getDoj() {
              return new Date(doj.getTime()); // For mutable fields
       }
}
import java.util.Date;
public class TestImmutable {
       public static void main(String[] args) {
              String name = "raj";
              int id = 1;
              Date doj = new Date();

              ImmutableClass class1 = new ImmutableClass(id, name, doj);
              ImmutableClass class2 = new ImmutableClass(id, name, doj);
      // every time will get a new reference for same object. Modification in              reference will not affect the immutability because it is temporary reference.
              Date date = class1.getDoj();
              date.setTime(date.getTime()+122435);
              System.out.println(class1.getDoj()==class2.getDoj());
       }
}

有关更多信息,请参阅我的博客:http :
//javaexplorer03.blogspot.in/2015/07/minimize-mutability.html


@Pang除了构造函数外,还可以使用其他任何方式初始化字段。我班上有20多个领域。使用构造函数初始化所有字段非常困难,有些字段甚至是可选的。
Nikhil Mishra '18

1
@NikhilMishra,可以在对象构建期间使用Builder设计模式来初始化变量。您可以保留在构造函数中设置的强制变量,并使用setter方法设置其余的可选变量。但是严格来说,您将不会创建True不可变类。
sunny_dev

1

如果对象一旦创建便无法更改,则称为不可变。在Java中创建不可变类的最简单方法之一是将其所有字段设置为final。如果您需要编写不可变类,其中包括诸如“ java.util.Date”之类的可变类。为了在这种情况下保持不变,建议其返回原始对象的副本,


建议不要返回副本,这是必要的。但是在构造函数中创建防御性副本也是必要的。否则,可以在后台更改对象。
瑞安

1

不可变对象是那些一旦创建便无法更改状态的对象,例如String类是不可变类。不变的对象不能被修改,因此它们在并发执行中也是线程安全的。

不变类的特征:

  • 构造简单
  • 自动线程安全
  • 映射键和设置为内部状态的良好候选者在处理时不会改变
  • 不需要克隆的实现,因为它们总是代表相同的状态

编写不可变类的键:

  • 确保不能覆盖类
  • 将所有成员变量设为私有和最终
  • 不要给出他们的setter方法
  • 在构建阶段不应泄漏对象引用

1

当您要将任何类作为不可变类时,必须考虑以下几个步骤。

  1. 班级应标记为最终班
  2. 所有字段都必须是私有且最终的
  3. 用构造函数替换设置器(用于将值分配给变量)。

让我们看一下上面键入的内容:

//ImmutableClass
package younus.attari;

public final class ImmutableExample {

    private final String name;
    private final String address;

    public ImmutableExample(String name,String address){
        this.name=name;
        this.address=address;
    }


    public String getName() {
        return name;
    }

    public String getAddress() {
        return address;
    }

}

//MainClass from where an ImmutableClass will be called
package younus.attari;

public class MainClass {

    public static void main(String[] args) {
        ImmutableExample example=new ImmutableExample("Muhammed", "Hyderabad");
        System.out.println(example.getName());

    }
}

0

不变对象上通常被忽略但重要的属性

添加到由@ nsfyn55提供的答案,以下几个方面还需要考虑对象不变性,这是首要的重要性

考虑以下类别:

public final class ImmutableClass {

  private final MutableClass mc;

  public ImmutableClass(MutableClass mc) {
    this.mc = mc;
  }

  public MutableClass getMutClass() {
    return this.mc;
  }
}

public class MutableClass {

  private String name;

  public String getName() {
    return this.name;
  }

  public void setName(String name) {
    this.name = name;
  }
}


public class MutabilityCheck {

public static void main(String[] args) {

  MutableClass mc = new MutableClass();

  mc.setName("Foo");

  ImmutableClass iMC = new ImmutableClass(mc);

  System.out.println(iMC.getMutClass().getName());

  mc.setName("Bar");

  System.out.println(iMC.getMutClass().getName());

  }

 }

以下是MutabilityCheck的输出:

 Foo
 Bar

请务必注意,

  1. (通过构造函数)在不可变对象上构造可变对象,方法是“复制”“关闭”到以下更改所描述的不可变对象的实例变量:

    public final class ImmutableClass {
    
       private final MutableClass mc;
    
       public ImmutableClass(MutableClass mc) {
         this.mc = new MutableClass(mc);
       }
    
       public MutableClass getMutClass() {
         return this.mc;
       }
    
     }
    
     public class MutableClass {
    
      private String name;
    
      public MutableClass() {
    
      }
      //copy constructor
      public MutableClass(MutableClass mc) {
        this.name = mc.getName();
      }
    
      public String getName() {
        return this.name;
      }
    
      public void setName(String name) {
       this.name = name;
      } 
     }
    

仍然不能确保完全不变,因为以下内容对MutabilityCheck类仍然有效:

  iMC.getMutClass().setName("Blaa");
  1. 但是,使用在1.中所做的更改运行MutabilityCheck将会导致输出为:

    Foo
    Foo
    
  2. 为了实现对象的完全不变性,其所有从属对象也必须是不变的


0

从具有JEP 359的JDK 14+开始,我们可以使用“ records”。这是创建Immutable类的最简单,最轻松的方式。

记录类是一组固定的字段(称为记录)的浅层不变的透明载体,为记录components提供state描述。每一个都component产生一个final字段,该字段保存提供的值以及accessor检索该值的方法。字段名称和访问者名称与组件名称匹配。

让我们考虑创建不可变矩形的示例

record Rectangle(double length, double width) {}

无需声明任何构造函数,无需实现equalshashCode方法。只是任何记录都需要名称和状态描述。

var rectangle = new Rectangle(7.1, 8.9);
System.out.print(rectangle.length()); // prints 7.1

如果要在对象创建期间验证值,则必须显式声明构造函数。

public Rectangle {

    if (length <= 0.0) {
      throw new IllegalArgumentException();
    }
  }

记录的主体可以声明静态方法,静态字段,静态初始化器,构造函数,实例方法和嵌套类型。

实例方法

record Rectangle(double length, double width) {

  public double area() {
    return this.length * this.width;
  }
}

静态字段,方法

由于状态应该是组件的一部分,因此我们无法将实例字段添加到记录中。但是,我们可以添加静态字段和方法:

record Rectangle(double length, double width) {

  static double aStaticField;

  static void aStaticMethod() {
    System.out.println("Hello Static");
  }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.