Java记录和空对象模式



有什么方法可以用Java记录处理Null对象吗?对于课程,我会这样做:

public class Id {
public static final Id NULL_ID = new Id();
private String id;
public Id(String id) {
this.id = Objects.requireNonNull(id);
}
private Id() {}
}

但这不起作用,因为每个构造函数都需要经过规范的(Id(String id)构造函数,而我不能只调用super()来绕过不变量。

public record Id(String id) {
public static final Id NULL_ID = null; // how?
public Id {
Objects.requireNonNull(id);
// ...
}
}

现在我用解决这个问题

public Id {
if (NULL_OBJECT != null)
Objects.requireNonNull(id);
}

但这感觉不对,容易出现并发问题。

我还没有发现很多关于唱片背后设计理念的讨论,这可能已经讨论过了。如果是这样保持简单,那是可以理解的,但这感觉很尴尬,我已经在小样本中多次遇到这个问题了。

我强烈建议您停止使用此模式。它有各种各样的问题:

代码中的基本错误

你的NULL_ID字段不是final,它显然应该是。

空对象与空对象

有两个概念看起来相似甚至相同,但事实并非如此。

存在未知/未找到/不适用概念。例如:

Map<String, Id> studentIdToName = ...;
String name = studentIdToName.get("foo");

如果"foo"不在地图中,name应该是什么?

不要那么快——在你回答之前:好吧,也许""——这会导致各种各样的问题。如果您编写的代码错误地认为所使用的id肯定在这个地图中,那么这就是既成事实:这个代码被窃听了。时期我们现在所能做的就是确保这个bug得到尽可能好的处理。

这里说name就是null,这是绝对优越的:这个bug现在将是显式的,堆栈跟踪指向有问题的代码。没有堆栈跟踪并不能证明代码是无错误的——根本不是。如果这段代码返回空字符串,然后向一个空邮件地址发送一封电子邮件,邮件正文中包含一个空字符串,名称应该在其中,这比代码抛出NPE要糟糕得多。

对于这样一个值(未找到/未知/不适用),java中没有什么能比得上null

然而,当使用记录为可能返回null的API(即可能返回"不适用"、"无值"或"未找到"的API)时,经常发生的情况是,调用方希望将其视为已知的方便对象

例如,如果我总是大写并修剪学生名称,并且一些id已经映射到"不再注册",并且这显示为已经映射到空字符串,那么调用方可以非常方便地为这个特定用例希望,而未找到的用例应该被视为空字符串。幸运的是,MapAPI迎合了这一点:

String name = map.getOrDefault(key, "").toUpperCase().trim();
if (name.isEmpty()) return;
// do stuff here, knowing all is well.

API设计者应该提供的关键工具是一个空对象

空物体应该很方便。你的不是

所以,既然我们已经确定了"null对象"不是你想要的,但"空对象"是很好的,请注意它们应该很方便。打电话的人已经决定了他们想要的一些具体行为;他们明确选择了这样做。他们不想,然后仍然必须处理需要特殊处理的唯一值,并且id字段为null的Id实例无法通过便利性测试。

您想要的大概是一个快速、不可变、易于访问的Id,并且Id有一个空字符串,而不是null。像"",或者像List.of()"".length()工作,返回0。someListIHave.retainAll(List.of())工作,并清除列表。这就是工作的便利。这是一种危险的便利性(因为,如果你不期望有一个具有某些众所周知行为的虚拟对象,那么不当场出错可能会隐藏错误),但这就是为什么调用者必须明确选择使用它,例如使用getOrDefault(k, THE_DUMMY)

那么,你应该在这里写些什么呢

简单:

private static final Id EMPTY = new Id("");

EMPTY值可能需要特定的行为。例如,有时您希望EMPTY对象也具有它是唯一的属性;Id的任何其他实例都不能被认为与之相等。

你可以用两种方法来解决这个问题:

  1. 隐藏布尔值
  2. 通过使用EMPTY作为显式标识

我认为"隐藏布尔值"足够明显。私有构造函数可以初始化为true的私有布尔字段,并且所有可公开访问的构造函数都设置为false。

使用EMPTY作为身份有点棘手。例如,它看起来像这样:

@Override public boolean equals(Object other) {
if (other == null || !other.getClass() == Id.class) return false;
if (other == this) return true;
if (other == EMPTY || this == EMPTY) return false;
return ((Id) other).id.equals(this.id);
}

这里,EMPTY.equals(new Id(""))实际上是假的,但EMPTY.equals(EMPTY)是真的。

如果这就是你希望它工作的方式(有问题,但在某些用例中,判定空对象是唯一的是有意义的),那就试试吧

不,在Java 14中的当前记录定义中,您想要的是不可能的。每个记录类型都有一个规范构造函数,可以隐式定义,也可以显式定义。每个非规范构造函数都必须从调用该记录类型的另一个构造函数开始。这基本上意味着,对任何其他构造函数的调用肯定会导致对规范构造函数的调用。[8.10.4 Java中的记录构造函数声明14]

如果这个规范构造函数进行参数验证(因为它是公共的,所以应该进行验证),那么您的选择是有限的。您要么遵循前面提到的建议/解决方法之一,要么只允许用户通过界面访问API。如果您选择最后一种方法,则必须从记录类型中删除参数验证,并将其放入接口中,如下所示:

public interface Id {
Id NULL_ID = new IdImpl(null);
String id();
static Id newIdFrom(String id) {
Objects.requireNonNull(id);
return new IdImpl(id);
}
}
record IdImpl(String id) implements Id {}

我不知道你的用例,所以这可能不是你的选择。但是,你想要的现在是不可能的。

关于Java 15,我只能在Java 15中找到JavaDoc for Records,它似乎没有改变。我找不到实际的规范,JavaDoc中的链接导致了404,所以也许他们已经放松了规则,因为有些人抱怨他们。

相关内容

  • 没有找到相关文章

最新更新