这个 Java 类是不可变的吗?

3
我正在实现一个不可变类,其结构如下:

public final class A{
    private final B bOb;
    public A(){
        bOb = new B();
    }
    public A(A a){
        bOb = new B(a.bOb);
    }
    public A addData(Type data){ // Type - Integer,String,char,etc.
        A newA = new A(this); //making a copy of the object that is calling addData method
        newA.bOb.add(data);
        return newA;
    }
}

这个实现是否正确?假设对象bOb是一个列表。


可能的重复问题:https://dev59.com/Rk7Sa4cB1Zd3GeqP6L-I,https://dev59.com/mHA75IYBdhLWcg3wrrJS。 - Nandkumar Tekale
注意,不安全的发布或维护是不安全的。 - Tom Hawtin - tackline
@TomHawtin-tackline 这样怎么样? - h4ck3d
5个回答

2

您的实现:

public A addData(int data){
    A newA = new A(this);
    newA.bOb.add(data); // <--- bOb is mutable
    return newA;
}

一个 bOb 正在被更改,因此如果您暴露任何对 bOb 的直接访问,它可能会被更改。(由于您没有暴露有关 bOb 的任何内容,因此该情况毫无意义。)

提示:

  • Collections.emptyList() 返回一个不可变的列表。
  • Collections.unmodifiableList() 返回给定列表的不可变浅拷贝。

考虑这种“更安全”的实现:

import java.util.*;

public final class A<T>{ //T must also be immutable (String, integer, char, ...)
    private final List<T> list;
    public A(){
        this.list = Collections.emptyList();
    }
    public A(List<T> list){
        this.list = Collections.unmodifiableList(list);
    }
    public A<T> addData(T data){
        List<T> shallowCopy = new ArrayList<T>(this.list);
        shallowCopy.add(data);
        return new A<T>(shallowCopy);
    }
    public List<T> getItems() {
        return this.list;
    }
}

这取决于 new B(a.bOb); 是如何实现的。还是不确定? - gkuzmin
尝试使用 Collections.unmodifiableList()... 它还有效吗? - EthanB
在类内部改变 bOb 通常被认为是可以接受的,只要状态变化在 A 外不可见。请参阅 Scala 中不可变队列的 此描述 以获取类似的示例。 - DaoWen

2

它并不是完全不可变的,因为B本身似乎是可变的(通过add方法),而A包含一个B的实例。

但是,只要满足以下条件,我认为它就是有效不可变的(即从外部观察者的角度看,它的行为就像是不可变的):

  • new B(B) 执行 B 的完整深拷贝(如果没有,则 addData 可能会改变原始的 B 中的某些内容)
  • 您没有通过其他方式泄露对 bOb 的引用
  • 添加的元素本身是不可变的

有效不可变性可以获得大部分不可变性的好处,因此我认为这个设计是可以的,前提是Type是不可变的 - 在对象构造期间对其进行修改是可以的,只要在将引用传递给其他人之后就不再修改。一个很好的例子是 java.lang.String - 在内部它包含一个可变数组,在构建String时对其进行写入,但在那之后就不再进行更改。


是的,调用 setDate 会影响 Date 对象和 bob,因为 List 只保留对该对象的引用,而不是副本。但是,由于您的类不共享 bob 或其内容,因此在 A 之外没有人可以看到它。 - gkuzmin
@gkuzmin 您忽略了一个事实,即日期对象来自外部,因此它被广泛共享。 - Marko Topolnik
2
@sTEAK。你不可能拥有一个不可变对象,它接受外部对象并使其可观察状态以任何方式取决于这些对象的状态。因此问题仍然存在:你的A的可观察状态是否以任何方式取决于LinkedList中对象的状态? - Marko Topolnik
1
防御性复制技术可以帮助您。例如,您可以添加一些接口 Copyable<T extends Copyable<T>>,其中将具有 public T getCopy() 方法。您的类可以将此接口作为泛型参数并将其用于防御性复制。但是,这种解决方案有缺点-只有您的类可以实现此接口。 - gkuzmin
@MarkoTopolnik:这是非常正确的,尽管许多人未能考虑到“可观察状态”短语的重要性。如果认为持有引用的对象的状态包括所引用对象的标识符而不是其当前状态,那么对于不可变对象来持有对公开可变对象的引用是完全有效的。很遗憾,Java和大多数.NET语言都没有区分逻辑上“拥有”所引用对象的字段与标识由其他东西拥有的对象的字段之间的区别。 - supercat
显示剩余6条评论

1
如果bOb是一个包含可变内容的列表,那么不行。但是看起来你只使用int作为内容,这样就解决了问题。

例如,如果您将可变内容放入bOb中(比如另一个List),那么它可以被修改,这将影响A实例的状态。 - gkuzmin
但是如果我通过addData方法将一个Integer添加到列表中,那么呢? - h4ck3d
Integer是不可变的,所以对于这种类型,你的类也将是不可变的。但是如果你使用Date,例如,它可以被修改并影响实例状态。例如,A a=new A();Date d=new Date();a.addData(d);d.setTime(1L);。在最后一个操作符中,d对象将被修改,a对象的状态也将被修改。但是这个修改不会影响可见状态,因为你的类没有可见状态。这是有趣的部分 - 你的类没有可见状态,所以它是无用的,但是不可变的。如果你共享它的状态(包括bob的值),那么可见状态将受到影响。 - gkuzmin
那么,它在所有情况下都是不可变的吗? - h4ck3d
这就是我的观点 - 如果你不分享状态,那么你的类就是不可变且无用的。但是当你分享bob或其内容时,你的类将变得可变但有用。 - gkuzmin
显示剩余2条评论

1

0

这取决于 B(B b) 构造函数的工作方式。如果它是一个复制构造函数,它应该对 b 字段进行深度复制。在这种情况下,A 是不可变的。

相反,如果构造函数只是获取对同一 b 实例的引用,则对其进行的任何更改都将反映在 bOb 属性上,因此 A 类不是不可变的...


好的,所以使用 LinkedList(Collection c) 可以获取具有相同元素的新列表。正如您所说 Type 是一个基本类型(或包装类型),因此您的 A 类是不可变的。 - davioooh

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