在 Java 中,将对象的非原始包含字段传递给作为对象句柄传递的方法,如果是这样,这将如何影响其可变性?

In Java, is passing an object's non-primitive containing field to a method passed as an object handle and if so how does that affect its mutability?

如果对象的非原始包含字段作为引用该字段对象的对象句柄传递,如果最初传递的字段是 updated/changed,它是否容易在事后被更改?

public class MutableDog
{
    public String name;
    public String color;

    public MutableDog(String name, String color)
    {
        this.name = name;
        this.color = color;
    }
}

public class ImmutableDog // are fields of these objects truly safe from changing?
{
    private final String name;
    private final String color;

    public ImmutableDog(MutableDog doggy)
    {
        this.name = doggy.name;
        this.color = doggy.color;
    }

    public String getColor()
    {
        return this.color;
    }
}

public static void main(String[] args)
{
    MutableDog aMutableDog = new MutableDog("Courage", "Pink");

    ImmutableDog anImmutableDog = new ImmutableDog(aMutableDog);

    aMutableDog.color = "Pink/Black";

    anImmutableDog.getColor().equals(aMutableDog.color); // true or false?
}

本质上,ImmutableDog真的是不可变的吗?在示例中,使用了字符串。使用可变对象(例如 Collection 会有所不同吗?

此问题是对 this 答案的回应。

ImmutableDog 是真正不可变的,即使它可以从可变对象接收字符串。这是因为 Strings 是不可变的。这也证明了不可变性的一大好处 - 您可以传递不可变对象而不用担心它们会突然改变。

您可能认为 ImmutableDog 中的字段可以通过设置 MutableDog 实例的字段以某种方式更改:

aMutableDog.color = "Pink/Black";

但是,"Pink/Black" 与此处分配给 ImmutableDog 的字符串实例不同,因此 ImmutableDog 不会更改。


另一方面,如果 ImmutableDog 有一个可变类型的字段,那么它就不再是真正不可变的了。

例如,这是您的相同代码,但 StringBuilder:

public class MutableDog
{
    public StringBuilder name;
    public StringBuilder color;

    public MutableDog(StringBuilder name, StringBuilder color)
    {
        this.name = name;
        this.color = color;
    }
}

public class ImmutableDog // are fields of these objects truly safe from changing?
{
    private final StringBuilder name;
    private final StringBuilder color;

    public ImmutableDog(MutableDog doggy)
    {
        this.name = doggy.name;
        this.color = doggy.color;
    }

    public String getColor()
    {
        return this.color.toString();
    }
}

public static void main(String[] args)
{
    MutableDog aMutableDog = new MutableDog("Courage", "Pink");

    ImmutableDog anImmutableDog = new ImmutableDog(aMutableDog);

    aMutableDog.color.append(" and Black");

    anImmutableDog.getColor().equals(aMutableDog.color);
}

现在不可变狗的颜色似乎会发生变化。您仍然可以通过在构造函数中复制字符串生成器来防御此问题:

public ImmutableDog(MutableDog doggy)
{
    this.name = new StringBuilder(doggy.name);
    this.color = new StringBuilder(doggy.color);
}

但是,这仍然允许您(意外地)修改 ImmutableDog class.

中的字符串生成器

所以不要将可变 classes 存储在不可变 classes 中。 :)

很简单:java中的任意引用类型,最后指向某个对象。

如果该对象具有可变状态,那么 任何 对它的引用都可用于更改该状态。

因此,你是对的:在每个字段声明之前放置 private final 并不一定会使 class 本身不可变。

在您的示例中,字符串 class 是不可变的(除了使用 Unsafe 的非常晦涩的技巧)。 namecolor赋值后,引用不能改变,指向的对象也不能改变。

但是当然:例如,如果类型是 List,则底层对象很可能会在 其他地方 发生更改。例如,如果你想防止这种情况发生,你必须创建一个 copy 该传入列表,并保留对该列表的引用。

它真的在所有感官和情况下都是不变的吗? 没有。它在某种程度上是不可变的吗? 是的。

只要你只做变量赋值,不可变狗确实总是会保持不变。这一般是由于 pass-by-value 语义,对此有很大的解释 here.

当您在 ImmutableDog 狗中复制 color 的引用时,实际上是在复制句柄。然后,当您通过可变狗修改颜色并为其分配 Pink/Black 值时,可变狗的句柄发生变化,但不可变狗仍保持原始句柄,指向原始颜色。

对于 String 类型,这更进一步,因为字符串是不可变的。因此,保证在字符串上调用任何方法都不会修改原始值。所以就字符串而言,是的,不可变的狗是真正的不可变的,可以信任。

Collections 有所作为。 如果我们稍微更改一下您的 类 的设计:

public class MutableDog {
    public String name;
    public List<String> acceptedMeals;

    public MutableDog(String name, List<String> acceptedMeals) {
        this.name = name;
        this.acceptedMeals = new ArrayList<>(acceptedMeals);
    }
}

public class ImmutableDog {
    private final String name;
    private final Iterable<String> acceptedMeals;

    public ImmutableDog(MutableDog doggy) {
        this.name = doggy.name;
        this.acceptedMeals = doggy.acceptedMeals;
    }

    public Iterable<String> getAcceptedMeals() {
        return this.acceptedMeals;
    }
}

并执行以下代码:

public static void main(String[] args) throws IOException {
    MutableDog mutableDog = new MutableDog("Spot", Collections.singletonList("Chicken"));
    ImmutableDog immutableDog = new ImmutableDog(mutableDog);

    immutableDog.getAcceptedMeals().forEach(System.out::println);

    mutableDog.acceptedMeals.add("Pasta");

    immutableDog.getAcceptedMeals().forEach(System.out::println);
}

打印出以下内容:

Chicken
Chicken
Pasta

这是由于 ImmutableDog 的构造函数将句柄复制到 acceptedMeals collection,但新句柄指向与原始 collection 相同的位置.所以当你通过mutable dog调用modification时,由于ImmutableDog在内存中指向同一个地方,所以它的值估计也被修改了

在这种特定情况下,您可以通过执行 collection 的深层复制来避免这种副作用,而不是简单地复制引用句柄:

public ImmutableDog(MutableDog doggy) {
    this.name = doggy.name;
    this.acceptedMeals = new ArrayList<>(doggy.acceptedMeals);
}

通过这样做,上述相同的 main 方法仅打印以下内容:

Chicken
Chicken