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

86

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

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

如果我有一个所有成员都是静态的类,它是不可变的吗?


可能是什么是不可变的?的重复问题。 - Joachim Sauer
1
上面链接的问题不同,但那个问题的答案应该能回答你所有的问题。 - Joachim Sauer
如果你的类都是静态成员,那它就是无状态的(没有实例有独立状态),这时可变或不可变的问题变得无关紧要。 - Sebastian Redl
除了构造函数之外,还有其他初始化字段的方法吗?我的类中有超过20个字段。使用构造函数初始化所有字段非常困难,有些字段甚至是可选的。 - Nikhil Mishra
14个回答

91
以下是不可变对象的硬性要求。
  1. 将类设置为final
  2. 将所有成员设置为final,可以在静态块、构造函数中明确设置,或者直接设置
  3. 将所有成员设置为私有
  4. 没有修改状态的方法
  5. 非常小心地限制对可变成员的访问权限(记住字段可能是final,但对象仍然可能是可变的。例如:private final Date imStillMutable)。在这些情况下,应该进行防御性拷贝

将类声明为final的原因非常微妙,经常被忽视。如果它不是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
该类不需要被声明为final。 - Angel O'Sphere
静态成员由第二点覆盖。您无法在构造函数中设置它们,因此必须明确地设置它们或在static{}块中设置它们。 - nsfyn55
12
@Nilesh:不变性是实例的特性。静态成员通常与任何单个实例都没有关系,因此它们在这里不起作用。 - Joachim Sauer
4
Joshua Bloch在他的《Effective Java》一书中的第15条建议是使用不可变性(immutability)- 不要提供修改状态的方法,将所有字段声明为final和private,确保类不能被继承,确保对任何可变组件的独占访问。 - nsfyn55
2
@Jaochim - 他们绝对是方程的一部分 - 如果我添加一个可变的静态成员并在ImSoImmutable的add函数中使用它,你就会遇到同样的问题。如果一个类是不可变的,那么所有方面都必须是不可变的。 - nsfyn55
显示剩余5条评论

14

类不是不可变的,对象才是。

不可变意味着:我的公共可见状态在初始化之后无法更改。

字段不一定要声明为final,尽管这可以极大地帮助确保线程安全性。

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


3
将所有的字段都设为静态会限制所有实例共享相同的状态,这并不是非常有用。 - aviad

13

不要向类中添加公共 mutator(setter)方法。


所有静态成员怎么办?这种类型的对象的引用或状态会改变吗? - Neel Salpe
7
没关系。如果你无法通过某种方式从外部改变它们,那么它就是不可变的。 - BalusC
无法回答这个问题,因为我们不知道静态成员的作用...当然,它们可能会修改私有字段。如果这样做,该类就不是不可变的。 - Angel O'Sphere
而且类的默认构造函数应该是“私有的”或者这个类应该是“final”的。只是为了避免继承。因为继承会破坏封装性。 - Talha Ahmed Khan
将一个可变对象(例如List)传递给不可变对象,然后从外部更改它怎么样?这是可能的,应该在对象创建期间使用防御性副本来处理。 - Yassin Hajaj
显示剩余3条评论

6
为了使Java中的类变为不可变,请注意以下几点:

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

2. 声明该类为 'final' 。这将防止其他类继承它,因此无法覆盖任何可能修改实例变量值的方法。

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

4. 也可以将类的构造函数声明为 private 并添加工厂方法以在需要时创建类的实例。

这些要点应该会有所帮助!

3
构造函数的可见性如何影响可变性?String是不可变的,但有几个公共构造函数。 - Ryan
正如@Ryan所说,实例变量也是一样的:为什么这些应该声明为“私有的”? - MC Emperor
不确定为什么这个答案有任何赞。这个答案是不完整的。它没有讨论可变对象,这是需要解决的一个重要问题。请阅读@nsfyn55的解释以获得更好的理解。 - Ketan R

5
Oracle网站,学习如何在Java中创建不可变对象。
  1. 不要提供“setter”方法——修改字段或字段所引用的对象的方法。
  2. 使所有字段都为final和private。
  3. 不允许子类重写方法。最简单的方法是将类声明为final。更复杂的方法是将构造函数设置为私有,并在工厂方法中构建实例。
  4. 如果实例字段包括对可变对象的引用,请勿允许更改这些对象:
    I. 不提供修改可变对象的方法。
    II. 不共享对可变对象的引用。永远不要存储传递给构造函数的外部可变对象的引用;如有必要,请创建副本,并存储对副本的引用。同样,在必要时创建内部可变对象的副本,以避免在方法中返回原始对象。

4
一个不可变对象是指在创建后不会改变其内部状态的对象。它们在多线程应用程序中非常有用,因为它们可以在没有同步的情况下在线程之间共享。
要创建一个不可变对象,您需要遵循一些简单的规则:
1. 不要添加任何setter方法
如果您正在构建不可变对象,则其内部状态永远不会更改。setter方法的任务是更改字段的内部值,因此您不能添加它。
2. 声明所有字段为final和private 私有字段从类外部不可见,因此无法对其进行手动更改。声明字段为final将保证如果它引用了一个基本值,则该值永远不会更改;如果它引用了一个对象,则该引用不能更改。这还不足以确保只有私有final字段的对象不可变。
3. 如果字段是可变对象,请为getter方法创建防御性副本
我们之前已经看到,定义final和private字段是不够的,因为可以更改其内部状态。为了解决这个问题,我们需要创建该字段的防御性副本,并在每次请求时返回该字段。
4. 如果传递给构造函数的可变对象必须分配给字段,请创建其防御性副本
如果持有传递给构造函数的对象的引用,则会出现相同的问题,因为可以更改它。因此,持有传递给构造函数的对象的引用可能会创建可变对象。要解决这个问题,如果它们是可变对象,则需要创建参数的防御性副本。
请注意,如果字段是对不可变对象的引用,则在构造函数和getter方法中不需要创建其防御性副本,将字段定义为final和private即可。
5. 不允许子类覆盖方法
如果子类覆盖方法,则可以返回可变字段的原始值,而不是其防御性副本。
为了解决这个问题,可以执行以下操作之一:
1. 声明不可变类为final,以便无法扩展它 2. 将不可变类的所有方法声明为final,以便它们无法被覆盖 3. 创建一个私有构造函数和一个工厂来创建不可变类的实例,因为具有私有构造函数的类无法扩展
如果遵循这些简单的规则,您可以自由地在线程之间共享不可变对象,因为它们是线程安全的!
以下是一些值得注意的要点:
不可变对象确实在许多情况下使生活更加简单。它们特别适用于值类型,其中对象没有身份,因此可以轻松替换,并且它们可以使并发编程更加安全和清洁(大多数臭名昭著的难以找到的并发错误最终都是由线程之间共享的可变状态引起的)。但是,对于大型和/或复杂的对象,为每个更改创建一个新副本可能非常昂贵和/或乏味。对于具有明显身份的对象,更改现有对象比创建新的修改副本要简单得多且更直观。
有些事情你只能使用不可变对象无法完成,例如拥有双向关系。一旦在一个对象上设置了关联值,其身份就会更改。所以,您在另一个对象上设置新值时,它也会更改。问题是第一个对象的引用不再有效,因为已经创建了一个新实例来表示具有该引用的对象。继续这样做只会导致无限递归。
要实现二叉搜索树,您必须每次返回一个新树:您的新树将不得不复制每个已修改的节点(未修改的分支是共享的)。对于插入函数而言,这还不算太糟糕,但是对于删除和重新平衡,事情很快变得相当低效。
Hibernate和JPA本质上要求您的系统使用可变对象,因为它们的整个前提是检测并保存对数据对象的更改。
根据语言,编译器可以在处理不可变数据时进行一堆优化,因为它知道数据永远不会改变。各种东西都被跳过,从而给您带来巨大的性能优势。
如果您查看其他已知的JVM语言(Scala、Clojure),可变对象在代码中很少出现,这就是为什么人们开始在单线程不足的情况下使用它们的原因。
没有对错之分,这只取决于您的偏好和您想要实现的目标(能够轻松使用两种方法,而不会使其中任何一方的铁杆支持者感到疏远,这是某些语言正在寻求的圣杯)。

2
  • 不要提供“setter”方法——修改字段或被字段引用的对象的方法
  • 使所有字段都是final和private
  • 不允许子类重写方法。最简单的方法是将其声明为final类。更复杂的方法是将构造函数设置为private,并在工厂方法中构造实例。
  • 如果实例字段包括对可变对象的引用,请勿允许更改这些对象:
    • 不提供修改可变对象的方法
    • 不共享对可变对象的引用。不要存储传递到构造函数的外部可变对象的引用;必要时创建副本,并存储副本的引用。同样,在必要时创建内部可变对象的副本,以避免在方法中返回原始对象。

2

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

不可变对象的优点

并发和多线程 它自动支持线程安全,因此无需同步问题....等等

不需要复制构造函数 不需要实现克隆。 类不能被覆盖 将字段设置为private和final 强制调用者在单个步骤中完全构造对象,而不是使用无参数构造函数

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

请参见下面的代码。

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中的不可变类有:String、装箱原始类型(wrapper classes)、BigInteger和BigDecimal等。

如何使一个类成为不可变类?

  1. 不要提供任何修改对象状态的方法(称为mutators)。
  2. 确保该类不能被扩展。
  3. 将所有字段设为final。
  4. 将所有字段设为private。 这可以防止客户端访问由字段引用的可变对象并直接修改这些对象。
  5. 进行防御性复制。 确保对任何可变组件的独占访问权。

    public List getList() { return 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
1
@NikhilMishra ,你可以使用Builder设计模式在对象构建期间初始化变量。 你可以将必须设置的变量放在构造函数中,并使用setter方法设置其余可选变量。 但严格来说,这样做并不会创建一个真正的不可变类。 - sunny_dev

1
Immutable Objects是那些一旦被创建就不可更改状态的对象,例如String类就是一个不可变类。不可变对象无法被修改,因此在并发执行时也是线程安全的。
不可变类的特点:
- 构造简单 - 自动线程安全 - 作为Map键和Set的良好选择,因为它们的内部状态在处理过程中不会改变 - 不需要实现clone,因为它们始终表示相同的状态
编写不可变类的关键:
- 确保类不能被覆盖 - 将所有成员变量设置为私有和final - 不提供setter方法 - 在构建阶段不应泄漏对象引用

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接