Java String 对象深入理解
什么是字符串?
字符串是由引号所括起来的一系列字符序列。例如"String","Hello"就为一个字符串
String 的不可变性
"String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何操作都会生成新的对象“。
- 固定不变 - 从String 对象的源码中可以看出,String 类声明为 final,且它的属性和方法都被 final 所修饰
- 任何操作都会生成新对象 - String:: subString(),String::concat() 等方法都会生成一个新的String对象,不会在原对象上进行操作
从下面String源码部分中很容易得到上面的结论:
/** String 类源码 */
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
/**
* Class String is special cased within the Serialization Stream Protocol.
*
* A String instance is written into an ObjectOutputStream according to
* <a href="{@docRoot}/../platform/serialization/spec/output.html">
* Object Serialization Specification, Section 6.2, "Stream Elements"</a>
*/
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
……
}
接下来使用一段代码来揭示这个过程:
public class ImmutableStrings
{
public static void main(String[] args)
{
String start = "Hello"; // 1
String end = start.concat(" World!"); // 2
//String end = start + " World!"
System.out.println(end); // 3
System.out.println(start); // 4
}
}
// Output
Hello World!
World
在这段代码中,没有改变任何对象。首先在第一个代码中,会在堆内存中创建一个新的String 对象,并把它的引用赋值给 start,接着在第二个调用String:: concat()方法对字符串进行拼接,此时会创建一个新的String 对象,该对象是"Hello" 和 "World" 的串联。就如String:: concat() 源码所示,第三个/四个代码的输出结果分别为:"Hello World!", "World"。并且操作符 " + "完成了和String:: concat() 类似的事 - > 操作符 "+" 算是一个语法糖,查看编译之后的字节码可以知道最终会调用StringBuilder:: append() 来完成字符串的拼接。
/** concat() 源码 */
public String concat(String str) {
int otherLen = str.length(); // 拼接的字符串参数长度为0, 返回本身
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); // 创建一个新String对象来存储拼接之后的字符串
}
不可变性设计的初衷
- 字符串常量池的需要。String对象的不可变性为字符串常量池的实现提供了基础,使得常量池便于管理和优化。
- 多线程安全。同一个字符串对象可以被多个线程共享。
- 安全性考虑。字符串应用场景众多,设计成不可变性可以有效防止字符串被有意篡改。
- 由于String对象的不可变性,可以对其HashCode进行缓存,可以作为HashMap,HashTable等集合的key 值。
字符串常量池
很多文章都提及到字符串常量池是String对象的集合,这种说法很接近了,但是更准确来说,它是 String 对象引用的集合 (网上关于这个众说纷纭,我更加倾向于存储的是引用的集合~ 若有错误了请指出! 谢谢~)。 虽说String 是不变的,但是它还是和Java中的其他对象一样,是分配在堆中的,所以说 String 对象存在于堆中,字符串常量池存放了它们的引用。因为 String 对象是不可变的,所以多个引用 "共享" 同一个String 对象是安全的,这种安全性就是 字符串常量池所带来的。
字面量的形式创建字符串
public class ImmutableStrings{
public static void main(String[] args)
{
String one = "someString"; // 1
String two = "someString"; // 2
System.out.println(one.equals(two)); // String 对象是否相同内容
System.out.println(one == two); // String 对象是否相同的引用
}
}
// Output
true
true
执行完上面的第一句代码之后,会在堆上创建一个String 对象,并把String 对象的引用存放到字符串常量池中,并把引用返回给 one,那当第二句代码执行时,字符串常量池已经有对应内容的引用了,直接返回对象引用给 two。one.equals(two) / one == two 都为true。 图形化如下所示:
stringLiterals1.jpg
new 创建字符串
public class ImmutableStrings
{
public static void main(String[] args)
{
String one = "someString";
String two = new String("someString");
System.out.println(one.equals(two));
System.out.println(one == two);
}
}
// Output
true
false
在使用 new关键字时的情况会有稍微不同,关于这两个字符串的引用任然会存放字符串常量池中,但是关键字 new使得虚拟机在运行时会创建一个新的String对象,而不是使用字符串常量池中已经存在的引用,此时 two 指向 堆中这个新创建的对象,而one 是常量池中的引用。 one.equals(two) 为 true,而 one == two 都为false。
stringLiterals2.jpg
如果想要one,two都引用同一个对象,则可以使用 String:: intern()方法 - 当调用intern()方法时,如果字符串常量池中已经有了这个字符串,那么直接返回字符串常量池中它的引用,如果没有,那就将它的引用保存一份到字符串常量池中,然后直接返回这个引用。这个方法是有返回值的,是返回引用。
String one = "someString";
String two = new String("someString"); // 仍指向堆中new 出的新对象
String three = two.intern();
System.out.println(one.equals(two)); // true
System.out.println(one == two); // false
System.out.println(one == three); // true
System.out.println(two == three); // false
垃圾收集
当一个对象没有引用指向时,垃圾收集器便会对它进行收集操作。看下面的一个事例:
public class ImmutableStrings
{
public static void main(String[] args)
{
String one = "someString";
String two = new String("someString");
one = two = null;
}
}
当 one = two = null时,只有一个对象会被回收,String 对象总是有来自字符串常量池的引用,所以不会被回收
stringLiterals3.jpg
String 对象的创建和字符串常量池的放入
上面嘀咕了那么久,那到底什么时候会创建String 对象?什么时候引用放入到字符串常量池中呢?先需要提出三个常量池的概念:
- 静态常量池:常量池表(Constant Pool table,存放在Class文件中),也可称作为静态常量池,里面存放编译器生成的各种字面量和符号引用。其中有两个重要的常量类型为CONSTANT_String_info和CONSTANT_Utf8_info类型(具体描述可以看看《深入理解Java虚拟机》的p 219 啦~)
- 运行时常量池:运行时常量池属于方法区的一部分,常量池表中的内容会在类加载时存放在方法区的运行时常量池,运行时常量池相比于Class文件常量池一个重要特征是 动态性,运行期间也可以将新的常量放入到 运行时常量池中
- 字符串常量池:在HotSpot 虚拟机中,使用StringTable来存储 String 对象的引用,即来实现字符串常量池,StringTable 本质上是HashSet<String>,所以里面的内容是不可以重复的。一般来说,说一个字符串存储到了字符串常量池也就是说在StringTable中保存了对这个String 对象的引用
执行过程
有了上面的概念之后,便可来描述下述过程了
首先给出结论,"在类的解析阶段,虚拟机便会在创建String 对象,并把String对象的引用存储到字符串常量池中"。
- 当*.java 文件 编译为*.class 文件时,字符串会像其他常量一样存储到class 文件中的常量池表中,对应于CONSTANT_String_info和CONSTANT_Utf8_info类型;
- 类加载时,会把静态常量池中的内容存放到方法区中的运行时常量池中,其中CONSTANT_Utf8_info类型在类加载的时候就会全部被创建出来,即说明了加载类的时候,那些字符串字面量会进入到当前类的运行时常量池,但是此时StringTable(字符串常量池)并没有相应的引用,在堆中也没有相应的对象产生;
- 遇到ldc字节码指令(该指令将int、float或String型常量值从常量池中推送至栈顶)之前会触发解析阶段,进入到解析阶段,若在解析的过程中发现StringTable已经有与CONSTANT_String_info一样的引用,则返回该引用,若没有,则在堆中创建一个对应内容的String对象,并在StringTable中保存创建的对象的引用,然后返回;
具体示例
下面给出几个具体实例,来说下这个过程:
- 字面量的形式创建字符串
public class test{
public static void main(String[] args){
String name = "HB";
String name2 = "HB";
}
}
通过javap 反编译后的字节码代码如下所示
# 2 = String #14
#14 = utf8 HB
……
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=1
0: ldc #2 // String HB
2: astore_1
3: ldc #2 // String HB
5: astore_2
6: return
……
当编译成字节码文件后,字面量"HB" 会存储到常量类型 CONSTANT_Utf8_info中,类加载时,其也会随之加载到方法区中的运行时常量池中,接下来可以用此来在StringTable查询是否有匹配的String 对象引用(当然只是简化的说法,具体CONSTANT_Utf8_info还指向一个Symbol对象~);遇到第一个ldc字节码指令之前,解析过程中发现StringTable(字符串常量池)还没有与CONSTANT_String_info一样的引用,则在堆中创建一个对应内容的String对象,并在StringTable中保存创建的对象的引用,然后返回;astore_1指令把返回的引用存到本地变量name; 遇到二个ldc字节码指令之前,解析过程中发现StringTable(字符串常量池)已经有与CONSTANT_String_info一样的引用,则直接返回即可,并通过astore_2 指令将其返回的引用保存到本地变量 name2中
- new 创建字符串
public class test2{
public static void main(String[] args){
String name = new String("HB");
String name2 = new String("HB");
}
}
通过javap 反编译后的字节码代码如下所示
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: new #2 // class java/lang/String
3: dup
4: ldc #3 // String HB
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
10: new #2 // class java/lang/String
13: dup
14: ldc #3 // String HB
16: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
19: astore_2
20: return
使用了关键字new后,会有稍微不同,new 指令会在堆中创建一个新的String 对象,并将其引用值压入栈顶,通过dup指令 复制栈顶的新对象的引用值并把复制值压入栈顶,本地变量name 所保存的值就为该引用值;接下来在遇到第一个ldc字节码指令之前,解析过程中发现StringTable(字符串常量池)还没有与CONSTANT_String_info一样的引用,则在堆中创建一个对应内容的String对象,并在StringTable中保存创建的对象的引用, 所以在运行时,会创建两个String对象哦~接下来的过程和前面的差不多,就不一一叙述啦!
- 其他重要值得关注的示例
String s1 = new String("hb");
String s2 = "hb";
System.out.println(s1 == s2); // false
String s3 = s1.intern(); // 从字符串串常量池中得到相应引用
System.out.println(s2 == s3); // true
System.out.println(" ===== 分割线 ===== ");
String s5 = "hb" + "haha"; // 虚拟机会优化进行优化, 当成一个整体 "hbhaha"成立, 而不会用StringBuild::append()处理
String s6 = "hbhaha";
System.out.println(s5 == s6); // true
System.out.println(" ===== 分割线 ===== ");
String temp = "hb";
String s7 = temp + "haha"; // 采用StringBuilder::append()处理
System.out.println(s7 == s6); // false
String s8 = s7.intern(); // 从字符串串常量池中得到相应引用
System.out.println(s8 == s6); // true
System.out.println(" ===== 分割线 ===== ");
String s9 = new String("hb") + new String("haha"); //采用StringBuilder::append()处理
System.out.println(s9 == s6); // false
String s10 = s9.intern(); // 从字符串串常量池中得到相应引用
System.out.println(s10 == s6); // true
总结
- String 对象存在于堆中,字符串常量池存放了它们的引用
- 字符串常量池存储String对象的引用,且是全局共享的,相同的字符串都将指向同一个字符串对象
- 运行时创建的字符串(new)关键字 和 "" (字面量形式) 创建的字符串存在不同
- 检查字符串是否相同的最好方法是 equal()
- 可以通过String:: intern() 方法从常量池中得到String对象的引用,或 将String 对象的引用存入到 字符串常量池中
- 上述所有的实验都是在JDK 8 HotSpot虚拟机下进行的,在JDK 7 中HotSpot,字符串常量池移到了堆中哦~,所以不同JDK版本,不同虚拟机下可能存在差异
参考资料
[1] https://javaranch.com/journal/200409/ScjpTipLine-StringsLiterally.html
[2] https://www.iteye.com/blog/rednaxelafx-774673#comments
[3] https://www.zhihu.com/question/55994121/answer/408891707
[4] https://www.cnblogs.com/Kidezyq/p/8040338.html!