从字节码(ByteCode)角度理解 String 的连接

首先来看一道题。

题目描述

问下面两种赋值方式有何区别?

public class Demo {
public static void main(String[] args) {

String s = "1" + "2" + "3";

String s1 = "1";
String s2 = s1 + "2";
String s3 = s2 + "3";

System.out.println(s);
System.out.println(s3);
}
}

分析与解答

从表面其实看不出什么,我们可以通过 Class 文件反编译成的字节码(Byte Code)来分析。

如果你在使用 IDEA,请先在 IDEA 中安装 ASMified Bytecode Outline 插件,点击 安装详细教程,如果是其他集成环境,请自行 Google 安装插件教程。

ASMified Bytecode Online 插件作用:用于将 Class 文件反编译成字节码(Byte Code)形式。将上面代码在 IDEA 中运行后,生成的字节码(Byte Code)如下图所示:

...
public static void main(String[] a) {
ldc "123"
//astore 1
ldc "1"
//astore 2
_new 'java/lang/StringBuilder'
//dup
//INVOKESPECIAL java/lang/StringBuilder.<init> ()V
//aload 2
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ldc "2"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
//astore 3
_new 'java/lang/StringBuilder'
//dup
//INVOKESPECIAL java/lang/StringBuilder.<init> ()V
//aload 3
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ldc "3"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
....
}

看不懂,没关系!你只需要知道几个指令就能理解了。根据 《深入理解 Java 虚拟机(第二版)》— 周志明著 的第六章知识可知:

  • ldc 指令:将一个常量加载到操作数栈。
  • _new 指令:实例化对象
  • invokevirtual 指令:用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是 Java 语言中最常见的方法分派方式。

我们根据字节码顺序来看:

// "1"+"2"+"3" 被 JVM 转换成了字符串 "123" 存储到操作数栈
String s = "1" + "2" + "3";

跳过 astore、dup 等指令(不是本节重点)

/**
* JVM 将 "1" 存储到操作数栈
* JVM 用_new 指令实例化一个 StringBuilder 对象,调用 append() 方法连接 "1"
* JVM 将 "2" 存储到操作数栈
* 调用 append() 方法连接 "2"
* 调用 toString() 转换成 String 类型
* JVM_new 指令再实例化一个 StringBuilder 对象,调用 append() 方法连接 "12"
* JVM 将 "3" 存储到操作数栈
* 调用 append() 方法连接 "3"
* 调用 toString() 转换成 String 类型
* */
String s1 = "1";
String s2 = s1 + "2";
String s3 = s2 + "3";

当时用使用 + 操作符连接字符串时,为什么两者有无字符串对象就有区别呢?

是因为如果不出现字符串引用,字符串常量的值在编译期时就可以确定下来,所以不会使用到 StringBuilder;如果出现字符串引用,JVM 不能将字符串引用和字符串常量直接连接,所以将在运行期间动态生成 StringBuilder 对象,让它去实现连接。

说了 StringBuilder,就不能不提 StringBuffer,两者最大的区别是,前者线程不安全的,后者是线程安全的。不能一看是线程不安全就觉得不好,其实线程不安全比线程安全效率高,再者,因为我们写的一些代码不用线程安全这样多此一举。

特别注意!在循环中连接字符串时,最好不要出现字符串引用,因为每次循环都会新建 StringBuilder,即使 Java 有垃圾回收机制,这样也很浪费资源。这时候就可以用 StringBuilder 来连接字符串。

public class Demo {
public static void main(String[] args) {

// 我们这样写
StringBuilder builder = new StringBuilder();
for (int i = 1; i < 10; i++) {
builder.append(i);
}
System.out.println(builder.toString());

// 而不是这样写
String s = "";
for (int i = 1; i < 10; i++) {
s = s + i;
}
System.out.println(s);

}
}

总结

如果此博文谬误,还望各位路过的朋友指正!

参考网址

Depp Wang wechat
个人公众号