【java-String】理解String的不可变性,常量池,复用

注意:了解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 堆),而不是内容是否相同
相关推荐
不吃肉的羊8 小时前
log4j2使用
java·后端
王中阳Go8 小时前
为什么很多公司都开始使用Go语言了?为啥这个话题这么炸裂?
java·后端·go
廖广杰9 小时前
java虚拟机-句柄(Handle)与直接指针访问对象的优劣
后端
洛小豆9 小时前
为什么 Integer a = 100; 不创建新对象?从编译到运行的全流程拆解
java·后端·spring
汪不止9 小时前
Spring Boot 应用启动机制详解
java·spring boot·后端
FengyunSky9 小时前
高通Camx内存问题排查
android·linux·后端
咖啡啡不加糖9 小时前
贪心算法详解与应用
java·后端·算法·贪心算法
IT_陈寒10 小时前
Java性能优化:3个90%开发者都忽略的高效技巧,让你的应用提速50%!
前端·人工智能·后端
thginWalker10 小时前
软件的基础原理
后端