.. EOF
关于 Java 字符串的秘密
最近对 java 字符串(java.lang.String
)的部分行为感到困惑,抽空查阅资料后豁然开朗。忍不住写一篇博客纪念一下
一、不变性
从源码中不难看出:
class
用 final 修饰:不能被继承 & overridevalue
变量是 final + private 的:一旦被赋值,无法被更改内存地址(禁止重新赋值),同时外部无法访问内部数组进行修改。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
}
二、新的困惑
既然 java.lang.String
是一个类(对象),为什么通过新的引用赋值后,实际值未发生改变呢??
// string
String stringFoo = "foo";
String stringBar = stringFoo;
stringBar += " bar";
System.out.printf("[intFoo]:%s%n[intBar]:%s%n%n", stringFoo, stringBar);
// [intFoo]:foo
// [intBar]:foo bar
揭开谜团前,先来看看 primary type 与 reference type 的区别:
// int
int intFoo = 1;
int intBar = intFoo;
intBar++;
// array
char[] arrayFoo = new char[]{'f', 'o', 'o', ' ', ' ', ' '};
char[] arrayBar = arrayFoo;
arrayBar[4] = 'x';
arrayBar[5] = 'x';
// 输出
// [intFoo]:1
// [intBar]:2
// [arrayFoo]:[f, o, o, , x, x]
// [arrayBar]:[f, o, o, , x, x]
不难理解,int 作为原始型別,在 stackframe 中,变量与 value 一一对应,而引用类型(reference type)顾名思义,仅保存堆(Heap)中实例对象的内存地址。
参考去年发布的博客:
三、揭开谜团
官方教程 中提到由于 string 不可变的特性,针对它的任何修改操作会返回一个新的 string 对象:
参考 java.lang.String#concat
实现:
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
但是相比于 concat 方法,String 两两相加(+
)的操作符,具体发生了什么呢?
编译后我们发现,原来在 java8 中,两两相加会被编译器自动优化为 StringBuilder
实现,所以最终返回一个新的 String
对象。
// 1. javac Scratch.java
// 源代码转化为字节码(byte code = 1111_1111),
// 2. javap -v Scratch.class
// The `javap` tool is used to get the information of any class or interface.
public static void main(java.lang.String[]);
descriptor:([Ljava/lang/String;)V
flags:ACC_PUBLIC,ACC_STATIC
Code:
stack=2,locals=3,args_size=1
0:ldc #2 // 常量池获取 "foo"
2:astore_1 // 赋值引用:String stringFoo = "foo";
3:aload_1 // 加载引用
4:astore_2 // 赋值引用: String stringBar = stringFoo;
5:new #3 // sb = new StringBuilder();
8:dup // 返回实例引用
9:invokespecial #4 // 初始化
12:aload_2
13:invokevirtual #5 // sb.app(stringBar)
16:ldc #6 // 常量池获取 "bar"
18:invokevirtual #5 // sb.app("bar")
21:invokevirtual #7 // sb.toString() 新建 String 对象
24:astore_2
25:return
Python 实现对比
有趣发现 Python 中,字符串 string 类型逻辑,与 java 惊人的保持一致:
// 不可变性
>>> a = '123'
>>> a[0] = '1'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment
>>> b = a
>>> b += " 123
>>> b
'123 123'
>>> a
'123'
>>> a = '123' * 100000
>>> b = '123' * 100000
>>> a is b
False
>>> a = '123' * 10
>>> b = '123' * 10
>>> a is b
True
四、总结
基于对 JVM 内存管理,与字节码的探索,终于进一步理解 java 不可变的特性,以及为什么针对它的修改操作会返回一个新的 string 对象。
期望你也有所收获 :)