这有点奇怪;通常,x.foo() 运行由 x 引用指向的 object 定义的 foo() 方法。您提出的是一种后备机制,如果 x 是 null (没有引用任何内容),那么我们不会查看 x 指向的对象(它没有指向任何内容;因此,这是不可能的),但是我们看x的类型,变量本身,然后问这个类型:嘿,你能给我foo()的默认impl吗?
核心问题是您正在为 null 分配一个它没有的定义。您的想法需要重新定义 null 的含义,这意味着整个社区都需要重返校园。我认为当前 java 社区中对null 的定义是一些模糊不清的混乱云,所以这可能是一个好主意,但它是一个巨大的承诺,OpenJDK 团队非常容易指定一个方向并让社区忽略它。 OpenJDK 团队应该在试图通过引入语言功能来“解决”这个问题时非常犹豫,而他们确实如此。
让我们来谈谈有意义的 null 定义,您的想法专门迎合的 null 定义(不利于其他解释!),以及如何在当前的 java 中已经很容易地迎合这个特定的想法,即 - 你提出的建议对我来说听起来很愚蠢,因为这是不必要的,并且会无缘无故地迫使每个人对null 的含义提出意见。
不适用/未定义/未设置
null的这个定义正是SQL如何定义它的,它具有以下属性:
没有默认实现可用。根据定义!如何定义一个未设置列表的大小?你不能说0。你不知道列表应该是什么。关键是与未设置/不适用/未知值的交互应该立即导致表示程序员搞砸的[A]的结果,他们认为他们可以与这个值交互的事实意味着他们编写了一个错误 -他们对不成立的系统状态做出了假设,或者 [B] 未设置的性质具有传染性:操作返回概念“未知/未设置/不适用”作为结果。
SQL 选择了 B 路线:在 SQL 领域中任何与 NULL 的交互都是具有传染性的。例如,即使 SQL 中的NULL = NULL 是NULL,而不是FALSE。这也意味着 SQL 中的所有布尔值都是三态的,但这实际上是“有效的”,因为人们可以诚实地理解这个概念。如果我问你:嘿,灯亮了吗?那么有3个合理的答案:是,不是,我现在不能告诉你;我不知道。
在我看来,java 作为一门语言也是为了这个定义,但大多选择了 [A] 路线:抛出一个 NPE 让大家知道:有一个错误,并让程序员到达相关线路极快。 NPE 很容易解决,这就是为什么我不明白为什么每个人都讨厌 NPE。我喜欢 NPE。比一些通常但并不总是我想要的默认行为要好得多(客观地说,最好有 50 个错误,每个错误需要 3 分钟才能解决,而不是一个错误需要一整天!) – 这个定义“适用”于语言:
- 数组中未初始化的字段和未初始化的值以
null开头,在没有进一步信息的情况下,将其视为unset是正确的。
- 事实上,它们是具有传染性的错误:几乎所有与它们交互的尝试都会导致异常,
== 除外,但这是有意为之,出于同样的原因,在 SQL 中 IS NULL 将返回 TRUE 或 FALSE 而不是NULL:现在我们实际上是在谈论对象本身的指针性质("foo" == "foo" 如果两个字符串不相同,则可能为假 ref:显然 == 在 java 中的对象之间是关于引用本身而不是关于引用的对象)。
其中一个关键方面是 null 完全没有语义意义。它缺乏语义意义是重点。换句话说,null 并不意味着一个值是短的或长的或空白的或表示任何特定的东西。它唯一的意思是它没有任何意义。你无法从中获得任何信息。因此,当foo 未设置/未知时,foo.size() 不是 0 - 在此定义中,“foo 指向的对象的大小是多少”的问题是无法回答的,因此 NPE 是完全正确的。
你的想法会损害这种解释 - 它会通过回答无法回答的问题来混淆问题。
哨兵/'空'
null 有时用作确实具有语义意义的值。具体的东西。例如,如果你曾经写过这个,你就是在使用这种解释:
if (x == null || x.isEmpty()) return false;
在这里,您已为 null 分配了语义含义 - 与您分配给空字符串的含义相同。这在 java 中很常见,可能源于一些低音 ackwards 的性能概念。比如在eclipseecjjava解析器系统中,所有的空数组都是用空指针完成的。例如,方法的定义有一个字段Argument[] arguments(用于方法参数;使用argument是稍微错误的词,但它用于存储参数定义);然而,对于零参数的方法,语义上正确的选择是显然 new Argument[0]。但是,这不是 ecj 填充抽象语法树的内容,如果您正在修改 ecj 代码并将 new Argument[0] 分配给它,其他代码将会搞砸,因为它不是为了处理而编写的有了这个。
在我看来这是对null的不好使用,但很常见。而且,在ecj 的辩护中,它比javac 快4 倍左右,所以我认为对他们看似过时的代码实践进行中伤是不公平的。如果它很愚蠢并且有效,那么它并不愚蠢,对吧? ecj 的跟踪记录也比 javac 好(主要是根据个人经验;这些年来我在 ecj 中发现了 3 个错误,在 javac 中发现了 12 个)。
如果我们实施您的想法,这种null确实会变得更好。
更好的解决方案
ecj 应该做的,两全其美:为它创建一个公共常量! new Argument[0],这个对象,是完全不可变的。您需要为整个 JVM 运行创建一个实例,一次,一次。 JVM 本身就是这样做的;试试看:List.of() 返回“单例空列表”。 Collections.emptyList() 对于人群中的老前辈也是如此。所有使用Collections.emptyList()“制作”的列表实际上只是对同一个单例“空列表”对象的引用。这是可行的,因为这些方法创建的列表是完全不可变的。
同样可以并且通常应该适用于您!
如果你写过这个:
if (x == null || x.isEmpty())
如果我们按照null 的第一个定义,那么您就搞砸了,如果我们按照第二个定义,您只是在编写不必要的冗长但正确的代码
定义。您已经提出了解决此问题的解决方案,但还有更好的解决方案!
找到x 获得其值的地方,并解决决定返回null 而不是"" 的愚蠢代码。实际上你应该强调不在你的代码中添加空检查,因为进入这种模式太容易了,你几乎总是这样做,因此你实际上很少有空引用,但这只是瑞士奶酪相互叠放:可能还有洞,然后你会得到 NPE。最好不要检查,这样您就可以在开发过程中非常快速地获得 NPE - 有人返回了 null,而他们应该返回 ""。
有时,导致错误 null ref 的代码超出了您的控制范围。在这种情况下,在使用设计不佳的 API 时,您应该始终做同样的事情:尽快修复它。如果必须的话,写一个包装器。但是,如果您可以提交修复程序,请改为执行此操作。这可能需要制作这样一个对象。
哨兵真棒
有时哨点对象(“代表”此默认/空白片段的对象,例如 "" 用于字符串,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) 为真;这将元数据与价值混为一谈:你实际上不能告诉我 2 个缺失的日期是相等的。根据定义:缺少日期。你不知道它们是否相等。这不是一个可以回答的问题。但是我们不能将missingDate 的equals 方法实现为return false;(或者,更好的是,因为您也不能真正知道它们也不相等,所以抛出异常),因为这会破坏合同(equals 方法必须具有标识属性并且不能抛出,根据它自己的 javadoc,所以我们不能做任何这些事情。
更好地处理 null
有几件事可以让处理 null 变得容易得多:
-
注解:API 可以并且应该非常清楚地传达它们的方法何时可以返回 null 以及这意味着什么。将该文档转换为经过编译器检查的文档的注释非常棒。您的 IDE 可以在您键入时开始警告您可能会出现 null 以及这意味着什么,并且也会在自动完成对话框中这样说。而且它在所有意义上都完全向后兼容:无需开始将 Java 生态系统的大片区域视为“过时”(不像 Optional,后者大多很烂)。
-
可选,除非这是一个非解决方案。该类型不是正交的(您不能编写一个采用List<MaybeOptionalorNot<String>> 的方法,该方法适用于List<String> 和List<Optional<String>>,即使一个方法检查“它是一些还是没有?”状态在所有列表成员中并且不添加任何内容(除了可能随机播放之外)将在这两种方法上同样有效,但您无法编写它。这很糟糕,这意味着可选的所有用法都必须“展开”当场,例如Optional<X> 应该几乎永远不会作为参数类型或字段类型出现。仅作为返回类型,即使那是可疑的 - 我只是坚持 Optional 的用途:作为返回类型流式终端操作。
采用它也不向后兼容。例如,hashMap.get(key) 在对 Optional 用途的所有可能解释中,显然应该返回一个 Optional<V>,但它不会,而且永远不会,因为 java 不会轻易破坏向后兼容性,而破坏它显然是影响太大了。唯一真正的解决方案是引入java.util2 和对集合API 的完全不兼容的重新设计,这将Java 生态系统一分为二。询问 python 社区(python2 与 python3)进展如何。
-
使用哨兵,大量使用它们,让它们可用。如果我在设计 LocalDate,我会创建 LocalDate.FAR_FUTURE 和 LocalDate_DISTANT_PAST(但要明确一点,我认为设计 JSR310 的 Stephen Colebourne 可能是最好的 API 设计师。但没有什么比它更完美了不能抱怨,对吧?)
-
使用允许默认设置的 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,请确保getOrDefault、computeIfAbsent 等可用,这样您的 API 用户就不必与 null 打交道。