【问题标题】:Java: pseudo-setter method for immutable classesJava:不可变类的伪设置方法
【发布时间】:2012-03-05 16:13:59
【问题描述】:

假设我在 Java 中有一个 Foo 类,它具有不可变的数据:

class Foo {
    final private int x;
    public int getX() { return this.x; }
    final private OtherStuff otherstuff;
    public Foo(int x, OtherStuff otherstuff) { 
       this.x = x;
       this.otherstuff = otherstuff;
    }   
    // lots of other stuff...
}

现在我想添加一个实用方法,它创建一个具有相同状态但新值 x 的“兄弟”值。我可以称之为setX()

class Foo
{
    ...
    Foo setX(int newX) { return new Foo(newX, this.otherstuff); }
    ...
}

setX() 的语义不同于可变 bean 对象的标准 setter 约定,所以不知何故这感觉不对。

这个方法最好的名字是什么?

我应该叫它withX()newX() 还是别的什么?


编辑:在我的情况下的额外优先级:我有脚本客户端(通过 JSR-223 和我导出的对象模型)可以轻松获取 Foo 对象。然而,调用构造函数或创建构建器或其他任何东西都很麻烦。因此,我希望提供此方法以方便脚本客户端。

【问题讨论】:

标签: java immutability


【解决方案1】:

原文:Immutable Setters: Naming Conventions(来自 Programming.Guide)


withX(...)

这是不可变 setter 的事实上的标准命名约定。例如,这是 Immutables 框架生成的 setter 的默认名称。这是一个例子:

Foo newFoo = foo.withX(1047);

有一个@Value.Style 选项可以更改此模式,但该选项本身称为with="...",它强调了默认约定是什么。

作为最普遍的惯例,很容易找到这方面的例子。 Guava 和 Java time package 是两个。

只要x(...)

另一种方法是根本没有前缀。您可以在 Immutables 框架生成的构建器中看到这一点:

Foo foo = ImmutableFoo.builder()
                      .x(1047)
                      .y("Hello World")
                      .build();

如果您直接在不可变类上使用此方法(即不涉及构建器),您通常会将其作为 getter 的重载:

Foo newFoo = foo.x(5);  // setter - one argument
int x = newFoo.x();     // getter - no arguments

此约定用于例如Java Spark 框架。

setX(...)

某些 API 使用与可变类中的 setter 相同的命名约定。这有一个明显的缺点,即当您不熟悉代码库时可能会感到惊讶。与BigInteger 合作并写作……

bigInt.setBit(2);

...例如会是一个错误,因为返回的对象被丢弃。使用这种命名模式你必须习惯写作

BigInteger newBigInt = bigInt.setBit(2);

deriveX(...)

要强调新值是从现有对象派生这一事实,您可以使用deriveX(...)。 Java API 中的不可变 Font 类遵循此模式。如果你想创建一个新字体,例如,你使用的特定大小

Font newFont = font.deriveFont(newSize);

Font 类从一开始就存在。这种约定在今天还不是很普遍。

不可变对象是操作数

当不可变对象本身是转换的操作数时,它实际上并不是传统意义上的 setter,并且不需要为方法加上前缀。比如……

BigDecimal newBigDec = bigDec.multiply(BigDecimal.TEN);

...与 setter 具有相同的签名,但 multiply 显然是比任何其他替代方法更好的方法名称。

String.substringPath.resolve 等相同。

【讨论】:

    【解决方案2】:

    withX() 听起来不错,因为它是用于某些 Builder 模式的约定。

    这更像是“部分克隆”或“构建器”而不是“设置器”...

    如果您查看java.lang.String(也是不可变的),那么有各种各样的方法会根据旧字符串返回一个新字符串(子字符串、toLowerCase() 等)...

    更新:另请参阅我喜欢的 aioobe [deriveFoo()] 的回答 - 它可能更清晰,尤其是对于不熟悉 Builder 模式的任何人。

    【讨论】:

      【解决方案3】:

      我会称之为withX(value)。它说它将是x = value

      如果班级有很多领域,我会害怕:

      obj.withX(1).withY(2).withZ(3).withU(1)...
      

      所以我可能会使用构建器模式——引入给定类的可变变体,仅使用数据和方法来创建具有当前状态的原始类。在那里我会调用这些方法x()y()z(),并让它们返回this。所以它看起来像:

      Immutable im2 = new Mutable(im1).x(1).y(2).z(3).build();
      

      【讨论】:

      • 一种方法有时可以使with 样式方法高效,即使在底层数据项很大且复制成本高昂的情况下,也可以向底层抽象对象添加额外的间接级别类型,并让该抽象类型包含一个虚拟的“扁平化”方法。然后,WithXX 方法可能会返回一个新包装器,该包装器指向一个 WrapWithXX 对象,该对象通常包含对原始对象的引用和新属性的值,除非它检测到对象的“链”变得过度长在这种情况下...
      • ...它将调用Flatten()Flatten 方法将生成一个新的底层对象实例,同时考虑到它可以解析的任何 WithXX 方法。在一个对象上调用Flatten 不会改变它的observable 状态,但是会用一个不太“复杂”的形式来代替它的实现。请注意,额外的间接级别并非没有成本,但也并非没有优势。例如,如果发现两个包装器具有比较相同的不同对象,则可以将一个或两个包装器更改为指向“规范”实例,从而加快比较。
      【解决方案4】:

      它肯定不是一个setter,因为它实际上构造并返回一个new对象。我认为在这种情况下工厂语义将是更合适的选择

      public Foo newFooWith(int x) {
         return new Foo(x, other);
      }
      

      替代方法可能是复制构造函数的变体

      class Foo {
          public Foo(Foo foo, int x) {
            return new Foo(x, foo.getOtherStuff());
          }
      }
      

      【讨论】:

      • 在这种情况下如何复制“其他东西”?
      • 我同意,但我的问题部分源于脚本客户端,在这些客户端中获取Foo 对象很容易,但获取FooFactory 对象很麻烦。
      • 这不会复制其他实例数据
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-07-04
      • 1970-01-01
      • 2014-10-16
      • 1970-01-01
      • 1970-01-01
      • 2014-02-28
      • 1970-01-01
      相关资源
      最近更新 更多