Java记录和空对象模式?

Java记录和空对象模式?,java,java-14,null-object-pattern,java-record,Java,Java 14,Null Object Pattern,Java Record,有没有办法对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(stringid)一次

有没有办法对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(stringid
)一次,我不能调用
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);
}
但这感觉是错误的,并且容易出现并发问题


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

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

代码中的基本错误 您的NULL_ID字段不是它显然应该是的
final

空对象与空对象 有两个概念看起来相似甚至相同,但它们不是

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

Map<String, Id> studentIdToName = ...;
String name = studentIdToName.get("foo");
API设计器应该提供的关键工具是空对象

空对象应该是方便的,而你的则不是。 因此,既然我们已经确定“空对象”不是您想要的,但是“空对象”很好拥有,请注意它们应该是方便的。调用方已经决定了他们想要的某些特定行为;他们明确选择了这种行为。他们不想再处理需要特殊处理的唯一值,并且如果有一个
Id
实例的
Id
字段为空,则便利性测试失败

您可能想要的是一个快速、不可变、易于访问的Id,并且Id有一个空字符串。不为null。可以像
”,或者像
List.of()
工作,并返回0。
someListIHave.retainal(List.of())
工作,并清除列表。这是工作中的便利。这是危险的便利(因为,如果您不希望虚拟对象具有某些众所周知的行为,不当场出错可以隐藏错误),但这就是调用方必须显式选择它的原因,例如,通过使用
getOrDefault(k,the_dummy)

那么,你应该在这里写什么? 简单:

private static final Id EMPTY = new Id("");
您可能需要空值具有某些特定的行为。例如,有时您希望空对象也具有其唯一性的属性;没有其他Id实例可以被视为等同于它

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

  • 隐藏布尔值
  • 通过使用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(新Id(“”))实际上是false,但是
    EMPTY.equals(EMPTY)
    是true


    如果您希望它是这样工作的(有疑问,但在某些用例中,判定空对象是唯一的是有意义的)不,您想要的是Java 14中当前的记录定义。每个记录类型都有一个规范构造函数,隐式或显式定义。每个非规范构造函数都必须从调用此记录类型的另一个构造函数开始。这基本上意味着,调用任何其他构造函数肯定会导致调用规范构造函数

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

    公共接口Id{
    Id NULL_Id=新的IdImpl(NULL);
    字符串id();
    静态Id newIdFrom(字符串Id){
    对象。requirennull(id);
    返回新的IdImpl(id);
    }
    }
    记录IdImpl(字符串id)实现id{}
    
    我不知道您的用例,所以这可能不是您的选择。但是,您想要的现在是不可能的


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

    我感谢你的反馈(我修复了缺失的最终版本).但我真的不想讨论模式本身。有趣的是技术上的局限性(这可能会影响到一般记录的有用性)。我可能会把这个例子改得更合理一些,这是我能想象到的最简洁的事情。@atamanroman,我鼓励更改这个例子。空对象实现默认行为。记录通常没有行为。两者似乎不太可能重叠。(一)记录的目的是不让null作为有效值,所以这个问题没有真正的意义。是吗?这就是我想要的论证,但根本没有提到。我在不同的代码库中有很多值对象,其中记录可能很有用,但它们的局限性让它很尴尬。也许你可以举个例子mple哪里需要“null”记录?既然您将
    null\u ID
    声明为
    static final
    ,就没有并发问题了。条件
    null\u OBJECT!=null
    只能在类初始值设定项中计算为
    false
    ,这是安全的。但这不是“null OBJECT”设计模式。空对象设计模式应该防止客户端代码遇到与引用相关的问题
    @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);
    }