在Java中连接空字符串

188
为什么下面的代码可以正常运行?我原本会期待会抛出一个NullPointerException异常。
String s = null;
s = s + "hello";
System.out.println(s); // prints "nullhello"

4
在编译时,s的类型已知,并且字符串类型重载了+运算符(例如,请参见Jonathan的回答)。在s + "hello"这一行中没有方法调用,因此不存在NPE的可能性,因为没有对象接收器(并且'代码转换'必须遵守此协议)。祝你编程愉快。 - user166390
3
我同意你的思路,yavoh。自动将 null 转换为字符串不是 Java 的好特性。太容易出错了,Sun 做这样一件事情真丢人。 - B T
1
@user166390 很好的解释,不是吗?但在我看来,打印 nullhello 仍然是一种反直觉、无用的行为。 - aliopi
1
我认为许多开发人员误解了,认为如果我使用一个空对象,它就会崩溃。实际上,只有当您调用空对象的属性或方法时,才会导致崩溃。 - Spark.Bao
1
如果明智的人将 null 转换为空字符串 "",我还能有点理解,但是转换为一个字面上的 4 个字符的字符串 "null"?! - vesperto
因此,当应用于String值时,它是+运算符的定义。最终,除非操作数是不同类型的,否则它不会尝试在任何操作数上调用方法,此时将调用隐式的toString。 显然,+运算符的意图是简化值的显示,而不以任何方式引用结果长度等。 - tishma
5个回答

204

为什么它必须工作?

JLS 5,第15.18.1.1节 JLS 8 § 15.18.1 “字符串连接运算符+”,导致JLS 8,§ 5.1.11“字符串转换”,要求此操作成功而不失败:

...现在只需要考虑引用值。 如果引用为null,则将其转换为字符串“null”(四个ASCII字符n,u,l,l)。否则,执行与无参数调用所引用对象的toString方法相同的转换;但是,如果调用toString方法的结果为null,则使用字符串“null”代替。

是怎么工作的?

让我们看一下字节码!编译器获取您的代码:

String s = null;
s = s + "hello";
System.out.println(s); // prints "nullhello"

将其编译为字节码,就像您实际编写了以下内容一样:

String s = null;
s = new StringBuilder(String.valueOf(s)).append("hello").toString();
System.out.println(s); // prints "nullhello"

(你可以使用 javap -c 命令自己查看)

StringBuilder 的 append 方法可以很好地处理 null,因为在这种情况下第一个参数是 null,所以会调用 String.valueOf() 方法,因为 StringBuilder 没有接受任意引用类型的构造函数。

如果你使用 s = "hello" + s,等效的代码将会是:

s = new StringBuilder("hello").append(s).toString();

在这种情况下,append 方法会将 null 传递给 String.valueOf() 进行委托。

注意:字符串拼接实际上是编译器可以决定执行哪些优化的少数几个地方之一。因此,“完全等效”的代码可能因编译器而异。这种优化是允许的,根据JLS, Section 15.18.1.2:

 

为了提高重复字符串连接的性能,Java编译器可以使用StringBuffer类或类似的技术来减少通过求值表达式而创建的中间String对象的数量。

我用于确定上面“等效代码”的编译器是Eclipse的编译器ecj


3
如果调用 StringBuilder 的任何一个 append 方法(如 append(String)append(CharSequence))而非 append(Object),则StringBuilder 在技术上不会使用String.valueOf,尽管无论如何,将 null 改为 "null" - ColinD
针对某些表现出意外错误的内容进行更详细的说明。它应该打印“hello”。 - aliopi
@aliopi 我非常尊重您的意见,但我认为如果行为发生任何变化,它应该抛出 NPE 异常,但我不认为这真的有什么帮助。 - Adowrath
1
@Adowrath 我改变了主意,你是对的,访问null应该抛出NPE。我认为打印null就像一种“元”行为,例如调试时,当我想看看变量里面有什么时,但默认的“非元”行为应该是抛出NPE。 - aliopi
1
@aliopi:我认为他们在这里做出了一个可辩护的决定,使连接操作具有空值安全性。这可能违反了最小惊讶原则,但他们可能试图避免在日志记录和诊断代码内部出现大量的NPE或显式空值检查(例如logger.print("doing something to " + obj);)。现在我们有比字符串连接更好的日志组合方式,但这在1995年并不一定成立。 - Mark Peters
显示剩余5条评论

29
请参阅 Java 语言规范的第 5.4 和第 15.18 节:
只有当二元 + 运算符的操作数中至少有一个是字符串时,才会应用字符串转换。在这一特定情况下,+ 的另一个参数将被转换为字符串,并将两个字符串连接成新的字符串作为 + 的结果。字符串转换在字符串连接 + 运算符的描述中详细说明。
如果只有一个操作数表达式的类型是 String,则在运行时对另一个操作数执行字符串转换以生成一个字符串。结果是一个指向 String 对象的引用(除非表达式是编译时常量表达式 (§15.28)),该对象是两个操作数字符串的连接。左操作数的字符先于右操作数的字符出现在新创建的字符串中。如果 String 操作数为 null,则使用字符串“null”代替该操作数。

6
好的!终于有一个回答给出了一些好的引用,而不是直接使用“StringBuilder”(它是编译器可能或可能不执行的优化)。 - user166390

14
第二行被转换为以下代码:
s = (new StringBuilder()).append((String)null).append("hello").toString();

追加方法可以处理null参数。


12

你没有使用 "null",因此你不会得到异常。如果你想要 NullPointer 异常,只需执行以下操作:

String s = null;
s = s.toString() + "hello";

我认为你想做的是:

String s = "";
s = s + "hello";

你正在使用 null,只是没有试图取消引用它。 - GregT

7
这是Java API中String.valueOf(Object)方法中指定的行为。当您进行连接操作时,使用valueOf来获取String表示形式。如果对象是null,则有一个特殊情况,此时使用字符串"null"

public static String valueOf(Object obj)

返回Object参数的字符串表示形式。

参数: obj - 一个Object。

返回值:

如果参数为空,则返回等于"null"的字符串;否则,返回obj.toString()的值。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接