7

I wish to alert the developer when he attempts to mutate an immutable object. The immutable object is actually an extension of a mutable object, and overrides the setters on said object to make it immutable.

Mutable base class: Vector3

public class Vector3 {
    public static final Vector3 Zero = new ImmutableVector3(0, 0, 0);

    private float x;
    private float y;
    private float z;

    public Vector3(float x, float y, float z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public void set(float x, float y, float z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
}

Immutable version: ImmutableVector3

public final class ImmutableVector3 extends Vector3 {
    public ImmutableVector3(float x, float y, float z) {
        super(x, y, z);
    }

    //Made immutable by overriding any set functionality.
    @Override
    public void set(float x, float y, float z) {
        throw new Exception("Immutable object."); //Should I even throw an exception?
    }
}

My use case is as follows:

public class MyObject {
    //position is set to a flyweight instance of a zero filled vector.
    //Since this is shared and static, I don't want it mutable.
    private Vector3 position = Vector3.Zero;
}

Let's say that a developer on my team decides he needs to manipulate the position of the object, but currently it's set to the static, immutable Vector3.Zero shared vector instance.

It would be best if he knew ahead of time that he needs to create a new instance of a mutable vector if the position vector is set to the shared instance of Vector3.Zero:

if (position == Vector3.Zero)
    position = new Vector3(x, y, z);

But, let's say he doesn't check this first. I think, in this case, it would be good to throw an Exception as shown above in the ImmutableVector3.set(x,y,z) method. I wanted to use a standard Java exception, but I wasn't sure which would be most fitting, and I'm not 100% convinced this is the best way to handle this.

The suggestions I've seen on (this question)[Is IllegalStateException appropriate for an immutable object? seem to suggest the following:

  • IllegalStateException - but it's an immutable object, thus only has a single state
  • UnsupportedOperationException - the set() method is unsupported since it overrides the base class, so this might be a good choice.
  • NoSuchElementException - I'm not sure how this would be a good choice unless the method is considered an element.

Furthermore, I'm not even sure I should throw an exception here. I could simply not change the object, and not alert the developer that they are trying to change an immutable object, but that seems troublesome as well. The problem with throwing an exception in ImmutableVector3.set(x,y,z) is that set has to throw an Exception on both ImmutableVector3 and Vector3. So now, even though Vector3.set(x,y,z) will never throw an exception, it has to be placed in a try/catch block.

Questions
  • Am I overlooking another option besides throwing exceptions?
  • If exceptions are the best way to go, which exception should I choose?
4

4 回答 4

10

抛出异常通常是要走的路,你应该使用UnsupportedOperationException.

Java API 本身确实在集合框架中推荐了这个:

此接口中包含的“破坏性”方法,即修改其操作的集合的方法,指定在此集合不支持操作时抛出 UnsupportedOperationException。

如果您创建完整的类层次结构,另一种方法是创建一个Vector不可修改且没有修改方法的超类,如set(),以及两个子类ImmutableVector(保证不变性)和MutableVector(可修改)。这会更好,因为您可以获得编译时安全性(您甚至不能尝试修改ImmutableVector),但根据情况可能会过度设计(并且您最终可能会得到许多类)。

例如,为Scala 集合选择了这种方法,它们都存在三种变体。

在所有要修改向量的情况下,都使用 type MutableVector。在您不关心修改并且只想从中读取的所有情况下,您只需使用Vector. 在您希望保证不变性的所有情况下(例如,将Vector实例传递给构造函数并且您希望确保以后没有人会修改它)您使用ImmutableVector(通常会提供一个复制方法,所以您只需调用ImmutableVector.copyOf(vector))。Vector永远不需要向下转换,而是指定您需要的正确子类型作为类型。这个解决方案的重点(需要更多代码)是关于类型安全和编译时检查,所以向下转换破坏了这个好处。但也有例外,例如ImmutableVector.copyOf()作为一种优化,方法通常如下所示:

public static ImmutableVector copyOf(Vector v) {
  if (v instanceof ImmutableVector) {
    return (ImmutableVector)v;
  } else {
    return new ImmutableVector(v);
  }
}

这仍然是安全的,并且为您提供了不可变类型的另一个好处,即避免了不必要的复制。

Guava 库有大量遵循这些模式的不可变集合实现(尽管它们仍然扩展了可变标准 Java 集合接口,因此不提供编译时安全性)。您应该阅读它们的描述以了解有关不可变类型的更多信息。

第三种选择是只拥有不可变向量类,而根本没有可变类。出于性能原因而使用的可变类通常是一种过早的优化,因为Java 非常擅长处理大量的短期临时对象(由于分代垃圾收集器,对象分配非常便宜,在最好的情况下,对象清理甚至可以不花费任何成本)。当然,对于像列表这样的非常大的对象,这不是解决方案,但对于 3 维向量,这肯定是一种选择(并且可能是最好的组合,因为它简单、安全,并且比其他对象需要更少的代码)。我不知道这个替代方案的 Java API 中的任何示例,因为在创建 API 的大部分部分的日子里,VM 并没有那么优化,所以太多的对象可能是一个性能问题。但这不应该阻止你今天这样做。

于 2013-08-29T17:39:48.543 回答
3

鉴于你的情况,我认为你应该抛出一个UnsupportedOperationException. 原因如下:

package com.sandbox;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Sandbox {

    public static void main(String[] args) throws IOException {
        List<Object> objects = Collections.unmodifiableList(new ArrayList<Object>());
        objects.add(new Object());
    }

}

这将抛出一个UnsupportedOperationException与你的情况非常相似的情况。

但是更好的事情(这可能是不可能的)是设计您的不可变类,因此首先没有要调用的变异方法。作为开发人员,我不想在运行时学习不能使用方法,我想在编译时或之前学习。

你的set方法应该返回一个ImmutableVector不会改变任何东西的新方法。

您可能想采用@SotiriosDelimanolis 的想法,让两个类都实现一个没有设置器的接口,然后让您的代码在接口周围传递。同样,鉴于您的遗留代码,这可能是不可能的。

于 2013-08-29T17:41:11.490 回答
3

如果你试图改变一个不可变的类,你应该抛出一个编译器错误。

不可变类不应扩展可变类。

于 2013-08-29T17:40:01.090 回答
1

不可变类不应派生自具体的可变类,反之亦然。相反,您应该有一个包含具体和派生类的ReadableFoo抽象类。这样,想要一个可以改变的对象的方法可以请求,想要一个对象的当前状态可以仅通过持久化对对象的引用来持久化的方法可以请求,以及不想改变对象状态的方法。并且不在乎状态返回后做什么,可以自由接受,而不必担心它们是可变的还是不可变的。MutableFooImmutableFooMutableFooImmutableFooReadableFoo

最棘手的方面是决定是否最好让基类包含一个检查它是否可变的方法以及一个“set”方法,如果它不是,就会抛出,或者让它简单地省略“set”方法。我倾向于省略“set”方法,除非该类型包含促进写入时复制模式的功能。

这种模式可以通过让基类包含抽象方法AsImmutableAsMutable具体方法来促进AsNewMutable。给定ReadableFoo名称George并希望捕获其当前状态的方法可以存储到字段中,或者GeorgeField取决于它是否认为要修改它。如果确实需要修改,可以设置. 如果需要分享,可以返回;如果希望在再次更改之前共享多次,则可以设置. 如果许多对象需要共享状态,并且它们中的大多数根本不需要修改它们,但有些需要大量修改它们,那么这种方法可能比单独使用可变或不可变对象更有效。George.AsImmutable()George.AsNewMutable()GeorgeField = GeorgeField.AsMutable()GeorgeField.AsImmutable()GeorgeField = GeorgeField.AsImmutable()...Foo

于 2013-08-29T18:06:08.677 回答