未初始化类的静态默认方法



有时有一种简单的方法来完成以下操作会很方便:

Foo a = dosomething();
if (a != null){
if (a.isValid()){
...
}
}

我的想法是为未初始化的变量提供一些静态的"默认"方法,比如

class Foo{
public boolean isValid(){
return true;
}
public static boolean isValid(){
return false;
}
}

现在我可以这么做了…

Foo a = dosomething();
if (a.isValid()){
// In our example case -> variable is initialized and the "normal" method gets called
}else{
// In our example case -> variable is null
}

因此,如果调用类中的静态"默认"方法a == null,则调用对象的方法。

是不是我遗漏了一些关键字来做到这一点,或者是因为没有在java/c#等编程语言中实现这一点?

注意:如果这个例子能起作用的话,那就不太令人惊叹了,但也有一些例子确实非常好。

这有点奇怪;通常,x.foo()运行foo()方法,该方法由x引用所指向的对象定义。您提出的是一种回退机制,如果xnull(没有引用任何内容),则我们不查看x所指向的对象(没有指向任何内容;因此,这是不可能的),而是查看x的类型,变量本身,并问这种类型:嘿,你能给我foo()的默认impl吗?

核心问题是,您正在为null分配一个它根本没有的定义。你的想法需要重新定义null的含义,这意味着整个社区都需要回到学校。我认为目前在java社区中对null的定义是一些模糊的、定义不清的混乱云,所以这可能是一个好主意,但这是一个巨大的承诺,OpenJDK团队很容易指定方向,而社区则很容易忽略它。OpenJDK团队应该在尝试通过引入语言功能来"解决"这个问题时非常犹豫,他们确实如此。

让我们来谈谈有意义的null的定义,你的想法具体迎合了哪个null的定义(损害了其他解释,因为这是不必要的,并且毫无理由地迫使每个人都对null的含义发表意见。

不适用/未定义/未设置

null的定义正是SQL定义它的方式,并且它具有以下属性:

没有默认实现可用。根据定义!如何定义未设置列表的大小?你不能说0。你不知道这个列表应该是什么。关键是,与一个未设置/不适用/未知值的交互应该立即导致一个结果,表示程序员搞砸了,他们认为自己可以与这个值交互的事实意味着他们编程了一个错误-他们对系统的状态做出了假设,或者[B]未设置的性质是感染性的:该操作返回"未知/未设置/不适用"的概念作为结果。

SQL选择了B路由:在SQL中与NULL的任何交互都是有感染力的。例如,即使SQL中的NULL = NULL也是NULL,而不是FALSE。这也意味着SQL中的所有布尔都是三态的,但这实际上是"有效的",因为人们可以诚实地理解这个概念。如果我问你:嘿,灯开着吗?,那么有三个合理的答案:是,不是,我现在不能告诉你;我不知道。

在我看来,java作为一种语言也适用于这个定义,但它主要选择了[a]路线:抛出一个NPE让每个人都知道:有一个bug,并让程序员非常快地到达相关的行。NPE很容易解决,这就是为什么我不明白为什么每个人都讨厌NPE。我喜欢NPE。这比一些通常但并不总是我想要的默认行为要好得多(客观地说,最好有50个bug,每个bug需要3分钟才能解决,而不是一个bug需要整个工作日,很大程度上!)——这个定义"适用"于以下语言:

  • 未初始化的字段和数组中未初始化的值以null开头,在没有进一步信息的情况下,将其视为unset是正确的
  • 事实上,它们是极其错误的:几乎所有与它们交互的尝试都会导致异常,除了==,但这是故意的,出于同样的原因,在SQL中IS NULL将返回TRUE或FALSE而不是NULL:现在我们实际上在讨论对象本身的指针性质(如果两个字符串不是相同的ref,"foo" == "foo"可能是FALSE:很明显,在java中,对象之间的==是关于引用本身的,而不是关于被引用的对象)

其中一个关键方面是null完全没有语义。它缺乏语义是关键。换句话说,null并不意味着一个值是短的、长的、空白的或指示任何特定的东西。它唯一的意义是它毫无意义。因此,当foo未设置/未知时,foo.size()而不是0——在这个定义中,"foo指向的对象的大小"这个问题是无法回答的,因此NPE是完全正确的。

你的想法会损害这种解释——它会对无法回答的问题给出答案,从而混淆问题。

哨兵/"空">

null有时被用作确实具有语义的值。一些特定的东西。例如,如果你写过这篇文章,你会使用这样的解释:

if (x == null || x.isEmpty()) return false;

在这里,您为null指定了语义,与您为空字符串指定的含义相同。这在java中很常见,可能源于一些bass-ackward的性能概念。例如,在eclipseecjjava解析器系统中,所有空数组都使用空指针。例如,方法的定义有一个字段Argument[] arguments(用于方法参数;using argument是一个稍有错误的词,但它用于存储参数定义);然而,对于零参数的方法,语义正确的选择显然是new Argument[0]。然而,这并不是ecj在抽象语法树中填充的内容,如果你正在破解ecj代码并将new Argument[0]分配给它,那么其他代码就会出错,因为它并不是为了处理这个问题而编写的。

在我看来,这是对null的不良使用,但很常见。而且,在ecj的辩护中,它的速度大约是javac的4倍,所以我认为诋毁他们看似过时的代码实践是不公平的。如果它很愚蠢而且有效,那就不愚蠢,对吧?ecj也比javac有更好的跟踪记录(主要是根据个人经验;多年来,我在ecj中发现了3个bug,在javac中发现了12个bug)。

如果我们实现您的想法,这种null确实会变得更好。

更好的解决方案

ecj应该做的是两全其美:为它创建一个公共常量!对象new Argument[0]是完全不可变的。您需要为整个JVM运行创建一个实例,一次,一次。JVM本身就是这样做的;试试看:List.of()返回"singleton空列表"。人群中老前辈的Collections.emptyList()也是如此。使用Collections.emptyList()"生成"的所有列表实际上只是对同一个单例"空列表"对象的引用。这是因为这些方法生成的列表是完全不可变的。

同样的道理也适用于你!

如果你曾经写过:

if (x == null || x.isEmpty())

如果我们按照null的第一个定义,那么你就搞砸了,如果我们按照第二个定义,你只是在写不必要的冗长但正确的代码释义你已经想出了一个解决方案来解决这个问题,但还有一个更好的解决方案!

找到x获得其值的位置,并寻址决定返回null而不是""的愚蠢代码。事实上,你应该强调NOT在代码中添加空检查,因为进入这种模式太容易了,你几乎总是这样做,因此你很少有空引用,但这只是一块又一块的瑞士奶酪:可能仍然有漏洞,然后你会得到NPE。最好不要检查,这样你就可以在开发过程中很快得到NPE——有人返回了null,而他们应该返回""

有时,导致错误空引用的代码超出了您的控制范围。在这种情况下,在处理设计糟糕的API时,要做同样的事情:尽快修复。如果必须的话,写一个包装。但如果你能提交一个修复程序,那就改为这样做。这可能需要制作这样的对象。

哨兵太棒了

有时sentinel对象("替代"此默认/空白获取的对象,例如字符串的""、列表的List.of()等)可能比这更花哨。例如,可以想象使用LocalDate.of(1800, 1, 1)作为丢失出生日期的哨兵,但请注意,这个例子不是一个好主意。它做一些疯狂的事情。例如,如果您编写代码来确定一个人的年龄,然后它开始给出完全错误的答案(这比抛出异常要糟糕得多。有了异常,你知道你有一个错误的速度会更快,你会得到一个堆栈竞速,可以在500毫秒内找到它(只需点击行,瞧。这正是你现在需要查看的行来解决问题)。它会说某人突然间就212岁了。

但您可以创建一个LocalDate对象来执行某些操作(例如:它可以打印自己;sentinel.toString()不抛出NPE,而是打印类似于"未设置日期"的内容),但对于其他操作,它会抛出异常。例如,.getYear()会抛出。

您也可以制作多个哨兵。如果你想要一个意思是"遥远的未来"的哨兵,那是微不足道的(LocalDate.of(9999, 12, 31)已经很好了),你也可以有一个"只要有人记得",例如"遥远的过去"。这太酷了,你的求婚根本做不到!

不过,你将不得不处理后果。在一些小方面,java生态系统的定义与此不一致,null可能是一个更好的替代品。例如,equals契约明确规定a.equals(a)必须始终保持,但是,就像SQL中NULL = NULL不是TRUE一样,您可能不希望missingDate.equals(missingDate)为真;这就是把元和值混为一谈:你不能告诉我两个缺失的日期是相等的。根据定义:日期不见了。你不知道他们是否平等。这不是一个可以回答的问题。然而,我们不能将missingDate的equals方法实现为return false;(或者,更好的是,因为你也不能真正知道它们不相等,抛出异常),因为这会破坏契约(equals方法必须具有identity属性,并且不能抛出,根据它自己的javadoc,所以我们不能做这两件事)。

更好地处理无效

有一些事情可以让处理null变得更容易:

  1. Annotations:API可以并且应该在通信中非常清楚它们的方法何时可以返回null以及这意味着什么。将文档转换为编译器检查文档的注释非常棒。当您键入时,IDE可以开始警告您,可能会出现null以及这意味着什么,并且在自动完成对话框中也会这样说。它在所有意义上都是完全向后兼容的:没有必要开始将java生态系统的大片视为"过时的"(不像Optional,它大多很糟糕)。

  2. 可选,但这不是解决方案。类型不是正交的(你不能写一个同时使用List<String>List<Optional<String>>List<MaybeOptionalorNot<String>>的方法,即使一个检查所有列表成员的"是有还是没有?"状态并且不添加任何内容(除了可能四处乱洗之外)的方法在这两个方法上都能起到同样的作用,但你就是写不出来。这很糟糕,这意味着可选的所有用法都必须当场"展开",例如Optional<X>应该几乎永远不会显示为参数类型或字段类型。仅作为返回类型,甚至这是可疑的——我只会坚持Optional的用途:作为Stream终端操作的返回类型。

采用它也不向后兼容。例如,在对Optional的所有可能解释中,hashMap.get(key)显然应该返回Optional<V>,但它没有,也永远不会,因为java不会轻易破坏向后兼容性,而破坏向后兼容性显然影响太大。唯一真正的解决方案是引入java.util2和对集合API进行完全不兼容的重新设计,这将java生态系统一分为二。询问python社区(python2vs.python3)情况如何。

  1. 使用哨兵,大量使用,使其可用。如果我在设计LocalDate,我会创建LocalDate.FAR_FUTURELocalDate_DISTANT_PAST(但要明确的是,我认为JSR310的设计者Stephen Colebourne可能是最好的API设计师。但没有什么完美到不能抱怨的程度,对吧?)

  2. 使用允许默认的API调用。地图上有这个。

不要写这个代码:

String phoneNr = phoneNumbers.get(userId);
if (phoneNr == null) return "Unknown phone number";
return phoneNr;

但一定要这样写:

return phoneNumbers.getOrDefault(userId, "Unknown phone number");

不要写:

Map<Course, List<Student>> participants;
void enrollStudent(Student student) {
List<Student> participating = participants.get(econ101);
if (participating == null) {
participating = new ArrayList<Student>();
participants.put(econ101, participating);
}
participating.add(student);
}

改为写入:

Map<Course, List<Student>> participants;
void enrollStudent(Student student) {
participants.computeIfAbsent(econ101,
k -> new ArrayList<Student>())
.add(student);
}

而且,至关重要的是,如果您正在编写API,请确保getOrDefaultcomputeIfAbsent等内容可用,这样API的用户几乎不必处理null

您可以编写这样的静态test()方法:

static <T> boolean test(T object, Predicate<T> validation) {
return object != null && validation.test(object);
}

static class Foo {
public boolean isValid() {
return true;
}
}
static Foo dosomething() {
return new Foo();
}
public static void main(String[] args) {
Foo a = dosomething();
if (test(a, Foo::isValid))
System.out.println("OK");
else
System.out.println("NG");
}

输出:

OK

如果dosomething()返回null,则打印NG

不完全如此,但请查看Optional:

Optional.ofNullable(dosomething())
.filter(Foo::isValid)
.ifPresent(a -> ...);

最新更新