Java - 修改对象的内部数据,或使用修改后的数据创建新对象

Java - Modify object's internal data, or create new object with modified data

问题

在Java中,根据标准的代码风格和设计原则,什么时候修改对象的内部数据更合适,什么时候使用修改后的数据创建新对象更合适?

例如,假设我有一些 class,Vector2D,它包含一个 x 分量和一个 y 分量,我想旋转该向量:

方法 A

public class Vector2D{
    private int x;
    private int y;
    public Vector2D(int x, int y){
        this.x = x;
        this.y = y;
    }
    public void rotate(double angle){
        //code for finding rotated vector
        x = rotated_x;
        y = rotated_y;
    }
}

然后可以使用 vector1.rotate(angle)

旋转 Vector2D 对象

方法 B

public class Vector2D{
    private final int x;
    private final int y;
    public Vector2D(int x, int y){
        this.x = x;
        this.y = y;
    }
    public Vector2D rotate(double angle){
        //code for finding rotated vector
        Vector2D rotated = new Vector2D(rotated_x, rotated_y);
        return(rotated);
    }
}

然后可以使用 vector1 = vector1.rotate(angle)

旋转 Vector2D 对象

目前的调查结果

到目前为止,我对该问题的研究使我相信方法 B 对于允许方法链接很有用,例如vector1 = vector1.rotate(angle).scale(scalar),它有它自己的好处,更多的是在事物的可读性方面。

这也让我意识到方法 B 允许您使对象不可变,所以我想推而广之我的问题也是:

什么时候使对象不可变是合适的,尤其是在这个例子中? (一般来说,对于点、向量、多边形等。所有这些类型都应该是不可变的吗?)

编辑:

我在创建此 post 时想到的项目是一个游戏,其中实体由多边形表示,并使用向量进行操作。

我相信,由于预期的应用,确保 classes 是线程安全的应该不是问题,因为 Vector 和 Polygon 数据无论如何只能由逻辑线程更改。

这也意味着对于每个实体,这些操作可能每秒执行 60 次(每个游戏刻度)。这是否意味着每次实例化新对象的任何开销都会迅速增加?

这取决于您要实施的应用程序类型。

如果是多线程的情况,你有很多线程并行执行任务,第二个选项会派上用场,因为不可变对象本质上是线程安全的,你会避免很多潜在的头痛。但这种方法也有一个折衷,你将有为对象内部状态的每次修改创建新对象的开销。

另一方面,如果您确定可以避免并行访问风险,则可以使用第一种方法,这种方法在直接比较中当然更快,因为它避免了很多对象实例化。

对我来说这取决于。可变对象对于多线程应用程序是危险的,而且当您要通过方法传递对象并且不想更改它们时,您应该将其设为不可变。

所以在 umif 类中你没有任何特殊问题,你可以使用可变对象来避免你自己、jvm 和 gc 的额外工作:)

my research into the issue so far has led me to believe that approach B is useful for allowing method chaining, e.g.

使用 A 方法,您可以同时拥有 setter 和 return 这些方法的修改对象。所以你也会有 "chaining" 功能。

所以真正的问题是:

When is it appropriate to make an object immutable ?

因为你想为一个对象保证多项内容,例如:

  • 没有副作用,因为任何可能的修改方法都会创建 return 一个新实例
  • 也不需要为 class 的任何实例制作防御副本,因为它实际上是由 class 本身保证的。
  • 不用担心竞争条件:无法修改实例
  • 不变规则设置

Java 8 日期 classes(LocalDateLocalTime)是不变性相关用法的好例子。

在您的实际示例中,假设 Vector2D 是一个 API class,可以传递给不同类型对象的不同方法。
使用可变 class,您可以在将对象传递给某些方法时对实例产生任何副作用。因此,它可能会以一种不需要的方式更改 class.
的某些客户端的对象状态 为了避免这种情况,class 的客户可以制作防御性副本。但它需要为客户提供样板和重复代码。 使用不可变 class,就不会出现此问题。客户端可以根据需要使用该实例,而不必担心其他客户端的使用情况。

在方法 A 中,您仍然可以 return 正在修改的对象并使用流畅的 API。它不是第二种方法所独有的。

关于不变性,主要有两个原因:

  1. 不可变对象更简单,因为一旦创建就没有 改变他们状态的方法,所以没有必要做 对增变器方法的验证。

  2. 不可变对象本质上是线程安全的。他们不能 被同时访问它们的多个线程损坏。

我建议您阅读 Effective Java - 第三版,第 17 项以了解有关该主题的更多信息。我相信对于所有想成为更好的开发人员的人来说,这都是一本了不起的书。

一般来说,您应该更喜欢使对象不可变。您可能想要选择可变对象的原因是:

  1. 对象有很多状态,因此创建起来很昂贵。例如。想象一个巨大的矩阵。如果您不允许突变,则每次都必须复制矩阵,这可能会非常昂贵(例如,参见 pandas - 它提供了两种选择)。
  2. 使其不可变会使 API 非常不直观。这也有点取决于语言的历史。例如,大多数排序库在 e.g. Java 改变集合而不是 return 创建一个副本。如果你要实现 shuffle,即使你不担心 (1),最好改变列表,因为如果你 return 一个副本会令人惊讶。

一些语言和库提出了第三种方式,其中突变 return 是一个新的 "version" 对象,同时共享以前的状态(参见 this). This solves (1), thereby allowing you to benefit from immutability without the cost of copying a lot. Some argue this means you should practically always opt for immutability because with (1) gone, there is no longer any reason to opt for mutability other than familiarity. In reality, some algorithms are very hard to express without mutable states (e.g. see this)。所以我们可能会继续拥有可变状态。

最好尽可能使用不可变对象,这样更易​​于维护和使用,您可以以更实用的方式编写代码。