什么,你竟然还不知道String为什么不可变

什么,你竟然还不知道String为什么不可变

String类是Java中一个非常重要的类 了解他的特性,有助于我们写出高质量的代码

String不可变解析

public final class String

implements java.io.Serializable, Comparable, 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对象并且返回

String str="a";

str="abc";

!

String不可变的原因是其在底层,应用中被广泛使用,将其设计为不可变,同时设置常量池缓存,能够减少内存损耗,保证重要数据的安全性

同时,由于其不可变性,使得它天然线程安全

String对象的创建

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

String a=new String("hello");

String b=new String("hello");

那么针对这段代码,你觉得有多少个对象,显然是三个

那么这个hello对象从何而来

实际上jvm内部维护了一个字符串常量池 用来复用,从而减少内存空间的浪费

假设字符串常量池有一个"hello",那么在创建上述对象的时候

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文件 即我们所说的字节码文件

//

// 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 可以看到该常量池有着字段名,方法名,字面量等各类数据

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."":()V

#2 = Class #25 // java/lang/String

#3 = String #26 // hello

#4 = Methodref #2.#27 // java/lang/String."":(Ljava/lang/String;)V

#5 = String #28 // world

#6 = Class #29 // java/lang/StringBuilder

#7 = Methodref #6.#24 // java/lang/StringBuilder."":()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

#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 // "":()V

#25 = Utf8 java/lang/String

#26 = Utf8 hello

#27 = NameAndType #13:#37 // "":(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."":()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."":(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."":(Ljava/lang/String;)V

19: astore_2

20: new #6 // class java/lang/StringBuilder

23: dup

24: invokespecial #7 // Method java/lang/StringBuilder."":()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

String a="a";

String b="b";

String c="c";

String d= "a"+"b"+"c";

编译后查看class文件常量池发现

#20 = Utf8 a

#21 = Utf8 b

#22 = Utf8 c

#23 = Utf8 abc

#24 = Utf8 javase/StringDemo

存在"abc"也就是其在编译时就确定,加入了class文件常量池,那么你觉得下面的问题答案是什么,显然是true 其地址均为字符串常量池的唯一常量

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实例调用,其逻辑大致为:去字符串常量池寻找一个等于该字符串的对象,如果存在则返回该对象的引用,如果不存在则在常量池创建一个字符串对象,然后返回引用

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

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

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 堆),而不是内容是否相同

相关尊享内容

武魂怎么样 超全武魂玩法介绍
mobile365体育

武魂怎么样 超全武魂玩法介绍

📅 11-22 👑 294
認識 502 錯誤:Bad Gateway 的意義與解決方法
365bet游戏下载

認識 502 錯誤:Bad Gateway 的意義與解決方法

📅 10-29 👑 143
《DNF》拜师方法
mobile365体育

《DNF》拜师方法

📅 08-18 👑 176