注意:了解java的类的加载机制有助于了解本章
String不可变解析
arduino
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
从java的源码可以看到 String持有一个char[]/byte[](不同jdk版本中)数组的引用
同时String是final修饰,说明他是不可以被继承的
char[]数组被final修饰,说明该引用指向的地址不可以被改变
并且String在源码中没有提供任何修改该char[]数组的方法(查看其源码可知,大部分方法都是返回了一个新的String对象),所以我们没法修改一个String对象内部的值,这就是我们说String不可变的原因
String不可变,我们又是如何频繁在代码中改变String的呢,通过源码可知,实际上大部分情况是通过创建了一个新的String对象并且返回
ini
String str="a";
str="abc";
String不可变的原因是其在底层,应用中被广泛使用,将其设计为不可变,同时设置常量池缓存,能够减少内存损耗,保证重要数据的安全性
同时,由于其不可变性,使得它天然线程安全
String对象的创建
ini
String a=new String("hello");
public String(String original) {
this.value = original.value;
this.coder = original.coder;
this.hash = original.hash;
}
针对这段代码。我们可以看到"hello"实际上是一个字符串对象
也就是我们创建一个String对象的时候,新创建的 String
对象会直接复用传入对象(即 "hello"
)的 char[]
数组引用,而不会重新分配新的内存去存储字符数组
通过代码Demo进一步了解String
ini
String a=new String("hello");
String b=new String("hello");
那么针对这段代码,你觉得有多少个对象,显然是三个
那么这个hello对象从何而来
实际上jvm内部维护了一个字符串常量池 用来复用,从而减少内存空间的浪费
假设字符串常量池有一个"hello",那么在创建上述对象的时候
ini
public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException {
String a=new String("hello");
String b=new String("world");
String c=a+b;
String d="hello"+"world"+"!";
}
经过javac指令编译后 可以得到.class文件 即我们所说的字节码文件
java
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package javase;
public class StringDemo {
public StringDemo() {
}
public static void main(String[] var0) throws IllegalAccessException, NoSuchFieldException {
String var1 = new String("hello");
String var2 = new String("world");
(new StringBuilder()).append(var1).append(var2).toString();
String var4 = "helloworld!";
}
}
Class文件的重要组成之一是常量池,存储着符号引用和字变量
我们可以看到hello 和world 还有helloworld!都存在于class文件常量池
这是因为这些字符串在编译时期就可以确定了
而 String c=a+b; 的"helloworld"在运行期才生效因此不会被加入到class文件常量池
通过 javap -v 我们可以查看Class文件的常量池
在Constant pool 可以看到该常量池有着字段名,方法名,字面量等各类数据
less
Last modified 2025-10-9; size 670 bytes
MD5 checksum 4ff176a1a6d818594cdc2f6693735502
Compiled from "StringDemo.java"
public class javase.StringDemo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #12.#24 // java/lang/Object."<init>":()V
#2 = Class #25 // java/lang/String
#3 = String #26 // hello
#4 = Methodref #2.#27 // java/lang/String."<init>":(Ljava/lang/String;)V
#5 = String #28 // world
#6 = Class #29 // java/lang/StringBuilder
#7 = Methodref #6.#24 // java/lang/StringBuilder."<init>":()V
#8 = Methodref #6.#30 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#9 = Methodref #6.#31 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#10 = String #32 // helloworld!
#11 = Class #33 // javase/StringDemo
#12 = Class #34 // java/lang/Object
#13 = Utf8 <init>
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 main
#18 = Utf8 ([Ljava/lang/String;)V
#19 = Utf8 Exceptions
#20 = Class #35 // java/lang/IllegalAccessException
#21 = Class #36 // java/lang/NoSuchFieldException
#22 = Utf8 SourceFile
#23 = Utf8 StringDemo.java
#24 = NameAndType #13:#14 // "<init>":()V
#25 = Utf8 java/lang/String
#26 = Utf8 hello
#27 = NameAndType #13:#37 // "<init>":(Ljava/lang/String;)V
#28 = Utf8 world
#29 = Utf8 java/lang/StringBuilder
#30 = NameAndType #38:#39 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#31 = NameAndType #40:#41 // toString:()Ljava/lang/String;
#32 = Utf8 helloworld!
#33 = Utf8 javase/StringDemo
#34 = Utf8 java/lang/Object
#35 = Utf8 java/lang/IllegalAccessException
#36 = Utf8 java/lang/NoSuchFieldException
#37 = Utf8 (Ljava/lang/String;)V
#38 = Utf8 append
#39 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#40 = Utf8 toString
#41 = Utf8 ()Ljava/lang/String;
{
public javase.StringDemo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 11: 0
public static void main(java.lang.String[]) throws java.lang.IllegalAccessException, java.lang.NoSuchFieldException;
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=5, args_size=1
0: new #2 // class java/lang/String
3: dup
4: ldc #3 // String hello
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 #5 // String world
16: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
19: astore_2
20: new #6 // class java/lang/StringBuilder
23: dup
24: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
27: aload_1
28: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
31: aload_2
32: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
35: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
38: astore_3
39: ldc #10 // String helloworld!
41: astore 4
43: return
LineNumberTable:
line 15: 0
line 16: 10
line 17: 20
line 18: 39
line 21: 43
Exceptions:
throws java.lang.IllegalAccessException, java.lang.NoSuchFieldException
}
SourceFile: "StringDemo.java"
在运行期的时候,对于HotSpot虚拟机,并不会立即加入到运行期常量池,而是懒加载,只有第一次用到该字符串常量的时候,采用将其加载到字符串常量池中,在字符串常量池创建出一个String对象,创建一个char[]/byte[],同时其引用指向这个数组
因此,当你在系统中第一次使用"a"时就会创建出一个String对象,存在于字符串常量池中
String a="a";
String
ini
String a="a";
String b="b";
String c="c";
String d= "a"+"b"+"c";
编译后查看class文件常量池发现
less
#20 = Utf8 a
#21 = Utf8 b
#22 = Utf8 c
#23 = Utf8 abc
#24 = Utf8 javase/StringDemo
存在"abc"也就是其在编译时就确定,加入了class文件常量池,那么你觉得下面的问题答案是什么,显然是true 其地址均为字符串常量池的唯一常量
ini
String a="a";
String b="b";
String c="c";
String d= "a"+"b"+"c";
String e="abc";
System.out.println(d==e);//true
所以字符串常量池的对象,在编译期就确定了,那么有什么方法在运行期间向字符串常量池添加变量呢
我们知道,采用符号引用相加的时候,String c=a+b;在编译期是无法确定的,因此不会在class文件常量池创建其字面量
通过字节码可以看到,实际上是 (new StringBuilder()).append(var1).append(var2).toString();创建了一个StringBuilder()对象,通过对底层的数组进行拼接,最后生成一个String对象
intern
intern()方法由String实例调用,其逻辑大致为:去字符串常量池寻找一个等于该字符串的对象,如果存在则返回该对象的引用,如果不存在则在常量池创建一个字符串对象,然后返回引用
ini
String s1=new String("a");
s1.intern();
String s2="a";
System.out.println(s1==s2);//false
String s3=new String("a")+new String("a");
s3.intern();
String s4="aa";
System.out.println(s3==s4);//true
ini
String s1=new String("a");//s1指向的是内存中非字符串常量池的地址
s1.intern();
String s2="a";//指向的是字符串常量池的地址
System.out.println(s1==s2);//false
String s3=new String("a")+new String("a");
s3.intern();//字符串常量池没有"aa",该操作会使常量池引用了s3
String s4="aa";//获取常量池内的引用,即s3
System.out.println(s3==s4);//true
这是因为执行s3.intern();时,"aa"没有先行被运行,如果指向s3.intern()先出现了"aa",那么结果为false
ini
String s5="aa";//字符串常量池创建对象"aa"
String s3=new String("a")+new String("a");//内存中的aa
s3.intern();//字符串常量池存在"aa",返回"aa"的引用
String s4="aa";//获取常量池内的引用,即s5
System.out.println(s3==s4);//false
总结
String
是不可变的: 没有提供修改方法,也无法该变底层数组,看似修改的操作都返回新对象- 字符串常量池用于缓存字符串字面量,减少内存开销,保证安全与性能
- 编译期能确定的字符串(如
"a" + "b" + "c"
)会放入常量池,运行期拼接的不会 intern()
方法可用于手动将字符串放入常量池,但要注意调用时机对对象引用的影响s1 == s2
是否为 true,取决于它们是否指向同一个对象(常量池 or 堆),而不是内容是否相同