Java不可变对象

6

我正在学习不可变性的概念。

我明白一旦创建了不可变对象,它们就不能改变其值。

但我不理解不可变对象的以下用途。

它们是

  • 自动线程安全且没有同步问题。如何?证明?
  • 不需要复制构造函数。如何?有例子吗?
  • 不需要实现克隆。如何?有例子吗?
  • 当作为字段使用时,不需要进行防御性复制。如何?有例子吗?
  • 始终具有“失败原子性”(Joshua Bloch使用的术语):如果不可变对象抛出异常,则永远不会处于不良或不确定状态。如何?有例子吗?

请有人详细解释每个点,并提供支持其的示例吗?

谢谢。


3
我已投票关闭。这是五个问题合并为一个,每个问题都需要详细广泛的回答。不确定为什么有三个赞同票。 - Duncan Jones
这是某种作业吗?另外请注意,一次有很多问题,而且有些问题是基于个人观点的。请将您的问题更具体化,并展示您已经尝试过什么。 - Uwe Plonus
这有点像作业 - 你可以提出一些具体的问题,而不是泛泛而谈。首先尝试自己解决这些问题 - 如果卡住了,我们可以提供帮助。 - Mirco
这不是作业!我卡在这个网站上了 http://www.javapractices.com/topic/TopicAction.do?Id=29 - tmgr
4个回答

10

..自动具备线程安全性,无需同步问题

当两个不同的线程修改同一对象的状态时,就会出现并发问题。不可变对象无法被修改,因此没有问题。

例如:一个String。两个线程可以传递相同的String,因为它们都不能以任何方式改变它。

不需要复制构造函数

……因为复制是改变其唯一方法。对于不可变对象常见的设计模式是对每个“修改”操作进行复制,然后在新对象上执行操作。

复制构造函数通常用于您想要更改而不影响原始对象的对象。这在不可变对象的情况下始终如此(按定义)。

String的情况下,所有方法和+运算符都返回新的String

不需要实现克隆

见上文。

作为字段使用时不需要进行防御性复制

曾经我做了一些愚蠢的事情。我有一个包含枚举值的列表:

private static final List<Status> validStatuses;

static {
  validStatuses = new ArrayList<Status>();
  validStates.add(Status.OPEN);
  validStates.add(Status.REOPENED);
  validStates.add(Status.CLOSED);
}

这个列表是从一个方法返回的:

public static List<Status> getAllStatuses() {
  return validStates;
}
我已经获取了那个列表,但我只想在界面中显示开放的状态。
List<Status> statuses = Status.getAllStatuses();
statuses.remove(Status.CLOSED);

太好了,它起作用了!等等,现在所有的状态列表都只显示那两个 - 即使在页面刷新后也是如此!发生了什么?我修改了一个静态对象。糟糕。

我本可以对 getAllStatuses 返回对象使用防御性拷贝。或者,我一开始就可以使用像 Guava的ImmutableList 这样的东西:

private static final List<Status> validStatuses =
    ImmutableList.of(Status.OPEN, Status.REOPENED, Status.CLOSED);

然后当我做了些傻事:

List<Status> statuses = Status.getAllStatuses();
statuses.remove(Status.CLOSED);  // Exception!

始终具有“失败原子性”(由Joshua Bloch使用的术语):如果不可变对象抛出异常,则它永远不会处于不良或不确定状态。

因为该类永远无法被修改,所有通过修改发出的状态都是整体的,合格的对象(因为它们不能改变,它们必须始终处于合格状态才能有用)。异常不会发出新对象,因此您永远不会有不良或不确定的状态。


2
它们自动具有线程安全性,并且没有同步问题。是由于Java内存模型为final字段提供的保证,因此可以实现无需同步即可实现线程安全的不可变对象。即使使用数据竞争在线程之间传递对不可变对象的引用,所有线程也将其视为不可变。因为它们是不可变的,所以它们不能被修改,因此与外部代码共享它们是可以的(您知道它们不会影响对象状态)。推论是:您不需要复制/克隆不可变对象。不可变对象始终具有“失败原子性”。一旦正确构建,不可变对象就不会改变。因此,如果构建失败,则会抛出异常;否则,您就知道对象处于一致状态。

但是字符串默认是不可变对象,为什么我们还要在不可变对象的类中将字符串字段设置为“final”?为什么? - tmgr
为了使一个对象成为不可变的,它的所有字段都必须是 final 的(除了有限和复杂的例外情况)。因此,如果您的对象包含一个字符串字段,则必须将其标记为 final。否则,即使该字符串无法被修改,您也会失去线程安全性保证。更多详情请参见:https://dev59.com/pWQo5IYBdhLWcg3wbe7r - assylias
1
@tm99 如果您没有将字符串字段标记为final,那么您不会失去对字符串本身(它是不可变的)的线程安全性。您会在具有字符串字段但不是final的对象上失去线程安全性! - afsantos

1
不是通过它的示例可以有效地解释的概念。不可变对象的优点在于您知道它们的数据不会更改,因此您无需担心。您可以自由地使用不可变对象,而不必担心传递它们的方法会更改它。
当我们执行多线程程序时,这很方便,因为基于线程更改的数据的错误不应该发生。

0

自动线程安全

  • 由于不可变性(不能发生突变),任何访问该对象的线程都会发现对象处于相同的状态。因此,不会出现一个线程改变对象的状态,然后第二个线程接管并改变对象的状态,然后再次第一个线程接管却不知道它被其他人改变的情况。
  • 好的例子是ArrayList - 如果一个线程遍历其元素而另一个线程删除其中一些元素,则第一个线程会抛出某种并发异常。使用不可变列表可以防止这种情况发生。

复制构造函数

  • 这并不意味着它不能有复制构造函数。它是一个构造函数,你向它传递同类型的对象,并创建一个给定对象的副本作为新对象。这只是一个猜测,但你为什么要复制总是处于相同状态的对象呢?
public class A
{
    private int a;

    public A(int a)
    {
        this.a = a;
    }

    public A(A original)
    {
        this.a = original.a;
    }

}  

克隆实现

  • 同样的问题,在克隆对象方面,通常只是占用内存空间。但是如果你想要将不可变的对象转换成可变的对象,也是可以做到的。
  • 一个很好的例子是集合,你可以从不可变的集合生成可变的集合。

防御性拷贝

  • 防御性拷贝意味着,当你把一个对象设置为一个字段时,你创建一个与原始对象相同类型的新对象副本。
  • 示例

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