Java 记录和空对象模式?

ata*_*man 11 java null-object-pattern java-14 java-record

有没有办法用 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() {}
}
Run Code Online (Sandbox Code Playgroud)

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

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

  public Id {
    Objects.requireNonNull(id);
    // ...
  }
}
Run Code Online (Sandbox Code Playgroud)

现在我解决了这个问题

public Id {
  if (NULL_OBJECT != null)
    Objects.requireNonNull(id);
}
Run Code Online (Sandbox Code Playgroud)

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

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

rzw*_*oot 5

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

代码中的基本错误

您的 NULL_ID 字段显然不final应该是这样的。

空对象与空对象

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

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

Map<String, Id> studentIdToName = ...;
String name = studentIdToName.get("foo");
Run Code Online (Sandbox Code Playgroud)

name如果"foo"不在地图上应该怎么办?

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

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

对于这样的值(未找到/未知/不适用),java 中没有什么null比值更好的了。

然而,在使用记录为可能返回的 API null(即可能返回“不适用”、“无值”或“未找到”的 API)时,经常发生的情况是调用者想要处理此问题与已知的便利对象相同

例如,如果我总是大写并修剪学生姓名,并且某些 id 已经映射为“不再注册”,并且这显示为已映射到空字符串,那么对于调用者来说,需要非常方便未找到的这个特定用例应被视为空字符串。幸运的是,MapAPI 可以满足这一点:

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

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

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

因此,既然我们已经确定“空对象”不是您想要的,但拥有“空对象”是很好的选择,请注意它们应该很方便。呼叫者已经决定了他们想要的一些特定行为;他们明确选择了这一点。他们不想仍然必须处理需要特殊处理的唯一值,并且字段为 null 的Id实例id无法通过便利性测试。

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

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

简单的:

private static final Id EMPTY = new Id("");
Run Code Online (Sandbox Code Playgroud)

您可能需要 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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

  • @atamanroman,我鼓励改变这个例子。空对象实现默认行为。记录通常没有行为。两者似乎不太可能重叠。 (5认同)
  • 感谢您的反馈(我修复了缺失的决赛)。但我真的不想讨论模式本身。有趣的是技术限制(这可能会影响记录的总体有用性)。我可能会将示例更改得更合理一些,这只是我能想象到的最简洁的事情。 (3认同)

Ale*_*x R 3

不,使用 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 {}
Run Code Online (Sandbox Code Playgroud)

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

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

  • 那么,为什么投反对票呢?如果我的回答有误,我会删除它。 (5认同)