9

我真的很喜欢在 Java 14 中添加记录,至少作为预览功能,因为它有助于减少我对简单、不可变的“数据持有者”使用 lombok 的需要。但是我在实现可空组件时遇到了问题。我试图避免null在我的代码库中返回以表明一个值可能不存在。因此,我目前经常在 lombok 中使用类似以下模式的东西。

@Value
public class MyClass {
 String id;
 @Nullable String value;

 Optional<String> getValue() { // overwrite the generated getter
  return Optional.ofNullable(this.value);
 }
}

当我现在对记录尝试相同的模式时,不允许声明incorrect component accessor return type.

record MyRecord (String id, @Nullable String value){
 Optional<String> value(){
  return Optional.ofNullable(this.value); 
 }
}

因为我认为Optional现在首选使用 s 作为返回类型,所以我真的想知道为什么会有这个限制。我对用法的理解是错误的吗?如何在不添加另一个具有不隐藏默认签名的另一个签名的访问器的情况下实现相同的目标?Optional在这种情况下根本不应该使用吗?

4

4 回答 4

10

Arecord包含主要定义其状态的属性。访问器、构造器等的派生完全基于记录的这种状态。

现在在您的示例中,属性的状态valuenull,因此使用默认实现的访问最终提供了真实状态。要提供对该属性的自定义访问,您需要寻找一个覆盖实际状态并进一步提供Optional返回类型的重写 API。

当然,正如您提到的处理它的一种方法是在记录定义本身中包含一个自定义实现

record MyClass(String id, String value) {
    
    Optional<String> getValue() {
        return Optional.ofNullable(value());
    }
}

或者,您可以在单独的类中将读取和写入 API 与数据载体分离,并将记录实例传递给它们以进行自定义访问。

JEP 384中最相关的引用:我发现的记录是(格式化我的):

一条记录声明它的状态——一组变量——并提交给与该状态匹配的 API。这意味着记录放弃了类通常享有的自由——将类的 API 与其内部表示分离的能力——但作为回报,记录变得更加简洁。

于 2020-07-17T07:18:38.227 回答
2

学分去霍尔格!我真的很喜欢他提出的质疑null. 因此,通过一个简短的示例,我想给他的方法更多的空间,即使对于这个用例有点复杂。

interface ConversionResult<T> {
    String raw();

    default Optional<T> value(){
        return Optional.empty();
    }

    default Optional<String> error(){
        return Optional.empty();
    }

    default void ifOk(Consumer<T> okAction) {
        value().ifPresent(okAction);
    }

    default void okOrError(Consumer<T> okAction, Consumer<String> errorAction){
        value().ifPresent(okAction);
        error().ifPresent(errorAction);
    }

    static ConversionResult<LocalDate> ofDate(String raw, String pattern){
        try {
            var value = LocalDate.parse(raw, DateTimeFormatter.ofPattern(pattern));
            return new Ok<>(raw, value);  
        } catch (Exception e){
            var error = String.format("Invalid date value '%s'. Expected pattern '%s'.", raw, pattern);
            return new Error<>(raw, error);
        }
    }

    // more conversion operations

}

record Ok<T>(String raw, T actualValue) implements ConversionResult<T> {
    public Optional<T> value(){
        return Optional.of(actualValue);
    }
}

record Error<T>(String raw, String actualError) implements ConversionResult<T> {
    public Optional<String> error(){
        return Optional.of(actualError);
    }
}

用法类似于

var okConv = ConversionResult.ofDate("12.03.2020", "dd.MM.yyyy");
okConv.okOrError(
    v -> System.out.println("SUCCESS: "+v), 
    e -> System.err.println("FAILURE: "+e)
);
System.out.println(okConv);


System.out.println();
var failedConv = ConversionResult.ofDate("12.03.2020", "yyyy-MM-dd");
failedConv.okOrError(
    v -> System.out.println("SUCCESS: "+v), 
    e -> System.err.println("FAILURE: "+e)
);
System.out.println(failedConv);

这导致以下输出...

SUCCESS: 2020-03-12
Ok[raw=12.03.2020, actualValue=2020-03-12]

FAILURE: Invalid date value '12.03.2020'. Expected pattern 'yyyy-MM-dd'.
Error[raw=12.03.2020, actualError=Invalid date value '12.03.2020'. Expected pattern 'yyyy-MM-dd'.]

唯一的小问题是toString现在打印的actual...变体。当然,我们不需要为此使用记录。

于 2020-07-17T22:59:21.877 回答
0

没有代表发表评论,但我只想指出,您基本上重新发明了 Either 数据类型。https://hackage.haskell.org/package/base-4.14.0.0/docs/Data-Either.htmlhttps://www.scala-lang.org/api/2.9.3/scala/Either.html。我发现 Try、Either 和 Validation 对于解析非常有用,并且我使用了一些具有此功能的 java 库:https ://github.com/aol/cyclops/tree/master/cyclops和https:// www.vavr.io/vavr-docs/#_either

不幸的是,我认为您的主要问题仍然悬而未决(我有兴趣找到答案)。

做类似的事情

RecordA(String a)
RecordAandB(String a, Integer b)

处理带有 null b 的不可变数据载体似乎很糟糕,但是将 recordA(String a, Integer b) 包装为在其他地方有一个 Optional getB 似乎适得其反。那时的记录类几乎没有意义,我认为 lombok @Value 仍然是最好的答案。我只是担心它不能很好地用于模式匹配的解构。

于 2020-08-27T21:04:43.583 回答
0

由于对记录的限制,即规范的构造函数类型需要匹配访问器类型,使用Optional记录的实用方法是将其定义为属性类型:

record MyRecord (String id, Optional<String> value){
}

有人指出这是有问题的,因为 null 可能作为值传递给构造函数。这可以MyRecord通过规范构造函数禁止此类不变量来解决:

record MyRecord(String id, Optional<String> value) {

    MyRecord(String id, Optional<String> value) {
        this.id = id;
        this.value = Objects.requireNonNull(value);
    }
}

在实践中,大多数常见的库或框架(例如 Jackson、Spring)都支持识别 Optional 类型并将 nullOptional.empty()自动转换为,因此这是否是需要在您的特定实例中解决的问题取决于上下文。我建议在你的代码混乱之前,在你的代码库中研究对 Optional 的支持,这可能是不必要的。

于 2021-07-28T06:35:12.803 回答