没有使用final字段如何创建一个不可变对象?

9

我们是否可以在不将所有字段设置为final的情况下创建不可变对象?

如果可能,举几个例子会很有帮助。


仅使用final只能帮助编译器捕获一些错误。一个类可以具有final字段而可变,也可以没有它们而不可变。然而,如果您有一个不可具有final字段的不可变类,那么您必须问为什么会这样... ;) - Peter Lawrey
1
为什么你不想使用final字段呢? - David Snabel-Caunt
1
@DavidCaunt 字段的惰性求值,例如 java.lang.String 的 hashCode。虽然非常棘手,但可以很有用地避免将字段设置为 final。 - Grundlefleck
可能是不可变对象的所有属性都必须是final吗?的重复问题。 - Raedwald
它使其不可变,但不是线程安全的!!!http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.5-110 - Murat Mustafin
7个回答

7

将所有字段声明为私有,并仅定义getter:

public final class Private{
    private int a;
    private int b;

    public int getA(){return this.a;}
    public int getB(){return this.b;}
}

引用@Jon Skeet的评论,final类修饰符对以下情况非常有用:

虽然仅具有Private修饰符的实例是不可变的,但子类的实例可能是可变的。因此,接收类型为Private的引用的代码不能依赖于它是不可变的,除非检查它是否是仅具有Private修饰符的实例。

因此,如果您希望确保所引用的实例是不可变的,则还应使用final类修饰符。


请注意,这个类不是最终的 - 因此,虽然 只有 Private 的实例是不可变的,但子类的实例可能是可变的。因此,接收类型为 Private 的引用的代码不能依赖于它是不可变的,而必须检查它是否只是 Private 的实例。 - Jon Skeet
@Jon Skeet:是的,谢谢你指出来。我的回答只是简单的...无论如何,final 是处理不应该改变的字段更准确的方式。 - Heisenbug
@Heisenbug "所以如果你需要子类也是不可变的,请使用final类修饰符。" 如果使用final修饰符,就无法创建子类,请更正最后一行或让我理解。 - lowLatency
@Naroji:抱歉,我的答案是错误的。我尝试进行了更正。Jon 指出的是,如果你有一个基类引用指向一个子类实例的对象,那么你不能假设子类实例也是不可变的(因此你可能会引用你认为不可变但实际上并非如此的东西)。如果你想确保一个类是不可变的,就要防止它被继承。 - Heisenbug

6

是的,它可以 - 只需确保您的状态是私有的,并且类中的任何内容都不会改变它:

public final class Foo
{
    private int x;

    public Foo(int x)
    {
        this.x = x;
    }

    public int getX()
    {
        return x;
    }
}

在这个类中,没有任何一种方式可以改变状态,并且因为它是final的,所以你知道没有子类会添加可变状态。

然而:

  • 非final字段的赋值并没有像final字段那样具有相同的内存可见性规则,因此可能会观察到对象从不同线程"改变"。更多关于final字段保证的细节请参见 JLS第17.5节
  • 如果您不打算更改字段值,我个人会将其设置为final以记录该决定并避免意外添加可变方法。
  • 我无法立即记起JVM是否通过反射防止更改final字段;显然,任何具有足够权限的调用者都可以使上述代码中的x字段可访问,并使用反射进行更改。(根据评论,可以使用final字段进行更改,但结果可能是不可预测的。)

反射机制呢? - Damian Leszczyński - Vash
对我来说,有时很难理解你回答问题的速度。可能有两种可能性,一是你生活在某个时间运动更慢的平行世界,或者你的手指拥有如神一般的速度。;-) - Damian Leszczyński - Vash
@jon Skeet: “非 final 字段的赋值操作与 final 字段不具备完全相同的内存可见性规则,因此在不同线程中可能会观察到对象的“变化”。你能否稍微详细解释一下这个问题?” - Heisenbug
1
@Heisenbug:基本上,在构造函数中对final字段的任何赋值都保证在构造函数完成时对所有线程可见;非final字段没有同样的保证。有关更多详细信息,请参见JLS的第17.5节。 - Jon Skeet
@JonSkeet:感谢您的解释:我能否编辑我的答案,将您的评论添加到最终类声明中?我认为这可能会有所帮助。 - Heisenbug
@JonSkeet:具有足够特权的情况下,可以通过反射更改final字段,但文档指出这可能会对线程产生不可预测的影响。 - ILMTitan

3
当描述Java对象时,术语“immutable”应该表示线程安全的不可改变性。如果一个对象是不可变的,通常理解为任何线程都必须观察到相同的状态。
单线程的不可变性并不是很有趣。如果真正指的是这个,那么应该用“单线程”来完全限定;更好的术语应该是“不可修改”。
问题在于如何给出对术语“immutable”的严格用法的官方参考。我不能,因为它基于Java大佬们使用该术语的方式。无论何时他们说“不可变对象”,他们总是在谈论线程安全的不可变对象。
实现不可变对象的惯用方法是使用final字段;final语义特别升级以支持不可变对象。这是一个非常强的保证;事实上,final字段是唯一的方法;volatile字段甚至synchronized块都不能防止构造函数完成之前发布对象引用。

虽然我不认为一个对象只有在它是线程安全的并且保证在引用被暴露给外部世界后没有可见状态会改变时才被视为“不可变”,但这并不意味着具有内部可变状态的对象不应该被视为“不可变”。通常情况下,对于一个不可变对象来说,在不可见于外部世界的方式下改变其内部状态是可以接受的,只要两个线程尝试更改相同的内部状态方面的唯一后果是执行冗余工作(如字符串的哈希码)即可。 - supercat

1

我相信答案是肯定的。

考虑以下对象:

public class point{
   private int x; 
   private int y;
   public point(int x, int y)
   {
      this.x =x; 
      this.y =y;
    }

   public int getX()
    {
       return x;
    }

    public int getY()
    { 
        return y;
    }

}

此对象为不可变对象。


1
如果一个类没有提供任何可以从外部访问并修改对象状态的方法,那么它就是不可变的。因此,您可以创建一个不可变的类而不必使字段为final。例如:
public final class Example {
    private int value;

    public Example(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

然而,在实际编程中并不需要这样做,如果你的类应该是不可变的,建议始终将字段设置为final。


1

是的,如果您创建了一个仅包含私有成员且没有提供设置器的对象,则它将是不可变的。


0

是的,把字段设置为私有的。除了构造函数之外,不要在任何其他方法中更改它们。当然,既然如此,为什么不将它们标记为final呢?


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