【问题标题】:rewrite a class with two fields and a constructor with a single parameter as a record class将具有两个字段的类和具有单个参数的构造函数重写为记录类
【发布时间】:2022-08-23 02:27:23
【问题描述】:

我想知道如何重写这个类

public class ClassA {
    private final String foo;
    private final String bar;

    public ClassA(String foo) {
        this.foo = foo;
        this.bar = foo.toUpperCase();
    }

    // getters...
}

作为记录类。
我能做到的最好的就是这个

public record ClassA(String foo, String bar) {
    public ClassA(String foo) {
        this(foo, foo.toUpperCase());
    }
}

问题是这个解决方案创建了两个构造函数,而我只想要一个接受字符串 foo

  • public String bar() { return foo.toUpperCase(; } 尽管没有缓存 toUpperCase,但它似乎是最不巴洛克的。
  • 问题是你试图使用记录,就好像它们只是关于样板减少,而不是使用记录来做它们的用途(建模纯聚合(元组)),这就是为什么你有麻烦。您的ClassA 不是纯聚合;您希望拥有不属于聚合类模型的私有状态(本质上是缓存从记录组件派生的属性。)

标签: java constructor record java-17


【解决方案1】:

这更像是对https://stackoverflow.com/a/73137529/3553087 的回应,而不是关于如何执行此操作的任何建议。引用的答案比OP的版本好,但它仍然在颠覆记录的精神,那就是它们是名义元组,也许有一些约束组件的不变量。

这是一个很好地使用具有不变量的记录的示例:

record Range(int low, int hi) { 
    public Range { 
        if (low > hi) throw new IllegalArgumentException();
    }
}

规范的构造函数验证参数,拒绝无效的参数,然后,整个事情就像一个透明的、不可变的容器一样用于某些元组,派生出有用的 API(构造函数、解构模式(从 Java 19 开始)、访问器、equals、hashCode、 toString) 来自元组。

老实说,这里的等价物是承认您正在编写的是任意字符串的元组及其大写版本:

record StringWithCachedUppercase(String value, String uppercased) { 
    public StringWithCachedUppercase {
        if (!uppercased.equals(value.toUpperCase(Local.ROOT)))
            throw new IllegalArgumentException();
    }

    public StringWithCachedUppercase(String value) { 
        this(value, value.toUpperCase(Locale.ROOT));
    }
}

为什么链接的答案不太理想?因为规范的构造函数拉得很快,并且破坏了new XyRecord(x, y).y() 应该返回与传递给构造函数的y 相关的东西的合理直觉。也许它是一个标准化版本,但它应该是可识别的——在链接的答案中,它被完全忽略了。

有些人可能会犹豫“但是你计算了两次大写版本”,但这不是使用错误机制的借口。 (而且,由于这种事情的全部理由是“我想缓存这个看似昂贵的计算”,所以使用它的唯一一点是如果你要多次请求大写版本。在这种情况下,O(1) 的额外建设成本是不相关的。)

此示例说明了记录是“几乎”的常见情况,即“元组,但缓存派生数量”。我们在记录设计过程中对这个案例进行了很长时间的考虑,但最终得出的结论是,它应该保留在记录设计中心之外。

如果您真的对缓存派生量感兴趣,那么它们可以被延迟计算并缓存在 WHM 中:

record StringWrapper(String s) { 
    static Map<StringWrapper, String> uppers = Collections.synchronizedMap(new WeakHashMap<>());

    public String uppercase() { 
        return uppers.computeIfAbsent(this, r -> r.s.toUpperCase(Locale.ROOT));
    }
}

【讨论】:

  • 谢谢,现在我完全理解了你的信息。惰性填充静态 WeakHashMap 的示例非常有趣。
【解决方案2】:

记录类总是有一个所谓的规范构造函数,它需要您声明的所有字段的参数。这个构造函数将默认由编译器生成,但您可以提供自己的,关键点:a规范构造函数在运行时可用于每条记录。

这是来自Java Language Specification 的引用:

为确保其记录组件的正确初始化,记录类不会隐式声明默认构造函数(第 8.8.9 节)。相反,一个记录类有个规范构造函数,显式或隐式声明,即初始化所有组件字段的记录类。

全部非规范构造函数,它们是具有不同于典范,就像您的案例构造函数需要一个参数ClassA(String),应该将调用委托给规范构造函数使用所谓的显式构造函数调用,即使用this() (正如你所做的那样),否则这样的构造函数将无法编译。

记录声明可能包含非规范构造函数的构造函数声明。这每个非规范构造函数的主体在记录声明中必须从备用构造函数调用开始(§8.8.7.1),或者发生编译时错误。

结论:因为你已经声明了一个记录字段,并且您还需要一个非规范的构造函数期望参数,将有两个构造函数:典范和一个非规范的.没有解决方法。


此外,正如@Brian Goetz 正确指出的那样,值得覆盖规范构造函数也是。否则,记录的第二个参数是否是第一个参数的大写版本会产生歧义,因为可能会使用具有不同逻辑的两个构造函数。

如果我们定义记录如下:

public record ClassA(String foo, String bar) {
    public ClassA(String foo) {
        this(foo, foo.toUpperCase(Locale.ROOT));
    }
}

下面的代码:

System.out.println(new ClassA("foo", "arbitrary string which not FOO"));
System.out.println(new ClassA("foo"));

将产生输出:

ClassA[foo=foo, bar=arbitrary string which not FOO] // unexpected result
ClassA[foo=foo, bar=FOO]                            // desired result

为了确保第二个参数在任何情况下都等于第一个参数转为大写,我们必须重写规范构造函数

public record ClassA(String foo, String bar) {
    public ClassA(String foo) {
        this(foo, foo); // doesn't matter what would be provided as the second argument
    }
    
    public ClassA(String foo, String bar) {
        this.foo = foo;
        this.bar = foo.toUpperCase(Locale.ROOT);
    }
}

【讨论】:

  • 如果你要做双构造函数,你还必须重写规范构造函数,以至少断言第二个组件是第一个组件的大写。否则你可能会得到令人惊讶的行为。
  • @BrianGoetz 谢谢,这是一个很好的观点。我错过了如果我们不覆盖规范的构造函数,这两个构造函数的行为会有所不同。更新了答案。
猜你喜欢
  • 2014-10-07
  • 1970-01-01
  • 1970-01-01
  • 2017-12-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-01-01
  • 2020-08-11
相关资源
最近更新 更多