Java 记录和空对象模式?

Java Records and Null Object Pattern?

有什么方法可以用 Java 记录来处理空对象吗?使用 类 我会这样做:

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,它显然应该是。

空对象与空对象

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

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

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

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

没那么快 - 在你回答之前:嗯,也许 "" - 这会导致各种问题。如果您编写的代码错误地认为所使用的 id 肯定在此映射中,那么这就是既成事实:此代码已被窃听。时期。我们现在所能做的就是确保尽可能'nicely'处理这个错误。

并且在这里说 namenull 是绝对优越的:错误现在将是明确的,堆栈跟踪指向有问题的代码。没有堆栈跟踪并不是无错误代码的证明——根本不是。如果此代码 return 是空字符串,然后将电子邮件发送到一个空白邮件地址,其正文中包含一个空字符串,而名称应该是空字符串,这比抛出 NPE 的代码要糟糕得多。

对于这样的值(未找到/未知/不适用),java 中没有任何东西比 null 更有价值。

但是,在使用 API 时经常发生的事情可能 return null(即 API 可能 return 'not applicable', 'no value', 或 'not found'), 是 调用者想把它当作一个已知的方便对象.

例如,如果我总是大写并且 trim 学生姓名,并且一些 ids 已经映射到 'not enrolled anymore' 而这显示为已映射到空字符串,那么它可以对于调用者希望 对于此特定用例 来说,未找到的应该被视为空字符串真的很方便。幸运的是, Map API 迎合了这一点:

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

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

空对象应该很方便。你的不是。

所以,既然我们已经确定 'null object' 不是您想要的,但是 'empty object' 很好,请注意它们应该很方便。调用者已经决定了他们想要的一些特定行为;他们明确选择了这一点。他们不想然后仍然必须处理需要特殊处理的唯一值,并且 Id 字段的 id 字段为 null 的实例无法通过便利测试。

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

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

简单:

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

您可能需要 EMPTY 值具有某些特定行为。例如,有时您希望 EMPTY 对象也具有唯一的 属性;没有其他 Id 实例可以被认为等于它。

您可以通过两种方式解决该问题:

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

我认为 'hidden boolean' 已经足够明显了。私有构造函数可以初始化为 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)是真的。

如果这就是您希望它工作的方式(有疑问,但在某些用例中,规定空对象是唯一的是有意义的),那就去做吧。

不,Java14 中的当前记录定义无法满足您的要求。每个记录类型都有一个规范的构造函数,隐式或显式定义。每个非规范构造函数都必须以调用此记录类型的另一个构造函数开始。这基本上意味着,对任何其他构造函数的调用肯定会导致对规范构造函数的调用。 [8.10.4 Record Constructor Declarations in Java 14]

如果此规范构造函数执行参数验证(它应该这样做,因为它是 public),您的选择将受到限制。您要么遵循已经提到的 suggestions/workarounds 之一,要么只允许您的用户通过界面访问 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 {}

我不知道你的用例,所以这可能不适合你。但同样,你想要的现在是不可能的。

关于Java15,我只能找到JavaDoc for Records in Java 15, which seems to not have changed. I couldn't find the actual specification, the link to it in the JavaDoc leads to a 404, so maybe they have already relaxed the rules, because some people complained about them