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);
}
}
因为我认为 Optional
s 作为 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 的支持。
我真的很喜欢 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);
}
}
因为我认为 Optional
s 作为 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 的支持。