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

String不可变解析

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对象并且返回

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."<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

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