在开始之前,我们先了解一下字符串驻留的概念。
当同时创建多个相同的字符串字面量时,只有一个字符串常量被存储在运行时常量池中。
例如:
Java String 对象创建涉及字符串驻留池、堆内存分配及常量折叠机制。使用双引号赋值时,若常量池存在则复用,否则新建;使用 new 关键字则在堆中强制创建新对象。字符串拼接在编译期若均为字面量会进行常量折叠,合并为一个对象;若包含变量或 new 对象,则通过 StringBuilder 运行时拼接,生成额外对象。掌握这些机制有助于优化内存使用,避免不必要的对象创建。

在开始之前,我们先了解一下字符串驻留的概念。
当同时创建多个相同的字符串字面量时,只有一个字符串常量被存储在运行时常量池中。
例如:
String a = "abc";
String b = "abc";
// String c = new String("abc");
此时虽然创建了两次 String 引用,但在运行时常量池中只有一个 "abc"。对于 String c = new String("abc"),可以通过 c.intern() 获取该字符串在运行时常量池中的引用对象,因此:
String a = new String("abc");
String b = new String("abc");
System.out.println(a == b); // false
System.out.println(a.intern() == b.intern()); // true
首先是不一样的,也不是语法糖。我们看以下代码:
public static void main(String[] args) {
String a = "abc";
String b = "abc";
String c = new String("Hello world");
String d = new String("abc");
int length = "abc".length();
}
从字节码分析可以看到,String a = "abc" 只创建了一个对象(即 "abc",位于运行时常量池)。而 String a = new String("abc") 会创建额外的对象。使用 new 关键字意味着在堆内存中也创建了一个对象。
a、b 两个对象都是通过 ldc 指令将常量 "abc" 加载进操作数栈的,所以 a == b 为 true。然而 String d = new String("abc") 会在堆中新建一个 String 对象,所以 d == b 为 false。
通常创建 String 对象时,推荐使用双引号方式,因为 new 的方式会额外创建一个对象。
通过上面的解释可知,String a = "abc" 会创建一个对象(在运行时常量池),而 String a = new String("abc") 会创建两个对象(一个在运行时常量池的 "abc",一个在堆里面)。需要注意的是,这是在两者不同时出现的情况下。
理解上述内容后便容易解答。当 String a = "abc" 与 String b = new String("abc") 同时出现时,总共会创建 2 个对象。
首先
String a = "abc"在运行时常量池中创建了一个对象。当执行String b = new String("abc")时,由于运行时常量池中已存在对象"abc",则不需要在常量池中新建,只需在堆中创建一个对象即可。
结果为 true。正如前文所述,a 与 b 都指向了运行时常量池中的 "abc"。
这里涉及常量折叠概念。这是在早期编译阶段的一种优化。例如:
int a = 1 + 2;
经过常量折叠后,1、+、2 变成了字面量 3,所以在代码中定义 int a = 1 + 2 并不比 int a = 3 增加程序运行期的运算量。
相应的,"hello"、"world" 也是常量,经过常量折叠后与 String b = "helloworld" 无异。所以 a == b 为 true。因此 String a = "hello" + "world" 也只会产生一个对象。
查看字节码验证:
public class Main {
public static void main(String[] args) {
String a = "helloWorld";
String b = "hello" + "World";
System.out.println(a == b);
}
}
使用 javap -verbose Main 查看,只有合并后的常量,而没有单独的 hello 和 world。且 ldc 加载的常量索引相同,说明 String a = "helloworld" 与 String b = "hello" + "world" 没有区别。
由于经过常量折叠,"hello" + "world" 与 "helloworld" 没有区别,所以:
String c = new String("hello" + "world");
与
String c = new String("helloworld");
效果一致,String c = new String("hello" + "world") 也只产生两个对象(一个常量池,一个堆)。
String a = "hello" + "world";
String b = new String("hello") + new String("world");
对于 String a = "hello" + "world",肯定只创建一个对象。
而对于 String b = new String("hello") + new String("world"),会产生 7 个对象:
"hello" 与 "world" 两个在运行时常量池的对象。new String("hello") 与 new String("world") 两个在堆中的对象。StringBuilder 实现,需要创建一个 StringBuilder 对象。StringBuilder 内部有一个 char[] 数组,数组本身也是一个对象。StringBuilder 对象的 toString() 方法时,又会 new 一个 String 对象。综上,共产生 7 个对象。
查看 StringBuilder.append(String) 源码:
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
它调用了父类方法 super.append(str):
public AbstractStringBuilder append(String str) {
if (str == null) return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count); // 这里的 value 是一个 char[] 数组
count += len;
return this;
}
看到 value 了吗?它是一个 char[] 数组,数组也算对象:
/**
* The value is used for character storage.
*/
char[] value;
至此第 6 个对象找到了。再看第 7 个对象,随后调用 toString() 方法:
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count); // 这里产生了一个对象
}
至此第 7 个对象就出现了。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online