Java 条包含可为空组件的记录

Java records with nullable components

我真的很喜欢 Java14 中添加的记录,至少作为预览功能,因为它有助于减少我对使用 lombok 的简单、不可变的“数据持有者”的需要。但是我在执行可为空的组件时遇到了问题。我试图避免在我的代码库中使用 returning 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); 
 }
}

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

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

现在在您的示例中,属性 value 的状态是 null,因此使用默认实现的访问最终会提供真实状态。为了提供对此属性的自定义访问,您正在寻找一个覆盖实际状态并进一步提供 Optional return 类型的覆盖 API。

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

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

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

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

A record declares its state -- the group of variables -- and commits to an API that matches that state. This means that records give up a freedom that classes usually enjoy -- the ability to decouple a class's API from its internal representation -- but in return, records become significantly more concise.

感谢Holger!我真的很喜欢他提出的质疑 null 实际需求的方式。因此,通过一个简短的例子,我想更详细地介绍他的方法 space,即使这个用例有点令人费解。

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... 变体。当然,我们不需要为此使用记录。

没有代表发表评论,但我只想指出,您实际上已经重新发明了 Either 数据类型。 https://hackage.haskell.org/package/base-4.14.0.0/docs/Data-Either.html or https://www.scala-lang.org/api/2.9.3/scala/Either.html. I find Try, Either, and Validation to be incredibly useful for parsing and there are a few java libraries with this functionality that I use: https://github.com/aol/cyclops/tree/master/cyclops and https://www.vavr.io/vavr-docs/#_either.

不幸的是,我认为你的主要问题仍然悬而未决(我很想找到答案)。

做类似

的事情
RecordA(String a)
RecordAandB(String a, Integer b)

处理具有空 b 的不可变数据载体似乎很糟糕,但包装 recordA(String a, Integer b) 以在其他地方具有可选的 getB 似乎 contra-productive。那时记录 class 几乎没有意义,我认为 lombok @Value 仍然是最好的答案。我只是担心它不能很好地与模式匹配的解构一起使用。

由于对记录的限制,即规范构造函数类型需要匹配访问器类型,将 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 类型并将 null 自动转换为 Optional.empty() 所以这是否是一个需要解决的问题您的特定实例取决于上下文。我建议在可能不必要地使代码混乱之前研究代码库中对 Optional 的支持。