Java 字符串及相关类(String/StringBuilder/StringJoiner)底层原理 + 内存分配 超详细讲解

5 阅读12分钟

内容会包含:String 的底层原理 + 内存分配StringBuilder/StringBuffer 底层原理StringJoiner 底层本质、三者的内存效率对比、还有你之前遇到的字符串拼接、常量池、new String () 的坑,全部讲透,零基础易懂

一、核心铺垫:Java 的两大内存区域(必须先懂,重中之重)

所有字符串相关类的内存分配,都围绕这两块核心区域,记住这两点,后面所有内存问题迎刃而解

✅ 1. 堆内存(Heap)

  • 存储内容:所有通过new关键字创建的对象new String()new StringBuilder()、数组、自定义对象如 CarDriver/MyMath)、字符串的真实字符数据;
  • 核心特点:内存空间大、创建的对象都是独一无二的、对象用完后由 GC 垃圾回收器回收、同一个内容的对象,在堆中会存多份

✅ 2. 方法区(Method Area)- 字符串常量池(String Pool)【核心核心】

  • 字符串常量池是 方法区的一块专属小区域,JDK8 后移到堆内存的一块独立空间中;
  • 存储内容:字符串字面量 / 常量(比如"张三""abc""123"这种写死在代码里的字符串)、通过String.intern()手动入池的字符串;
  • 核心特点:常量池的核心规则 → 【字符串内容唯一,不会存重复值】 ;比如代码里写了 100 个"abc",常量池中只会存 1 份"abc",所有地方引用的都是这一份,目的是节省内存

二、String 字符串 底层原理 + 内存分配(最核心,必考)

✅ 1. String 类的底层源码核心(Java8)

java

运行

public final class String {
    // 1. 底层存储的核心:一个【被final修饰的char[]字符数组】
    private final char value[];
    // 2. 字符串的哈希值缓存
    private int hash;
}

✅ 2. String 核心底层特性(为什么不可变?根源!)

从源码能直接得出 2 个决定性特性,也是所有 String 特性的根源:

✔ 特性①:Stringfinal修饰 → String 类不能被继承,没有子类,保证了字符串的安全性;

✔ 特性②:存储字符的数组char[] valueprivate final修饰 → String 是不可变字符串

  • private:外部类无法直接操作这个字符数组;
  • final:数组的引用地址一旦确定,永远不能改变(不能指向新的数组);
  • 👉 最终结果:一个 String 对象一旦创建,它的字符内容永远不能修改!

✅ 3. String 最关键的【内存分配规则】(分 3 种场景,全是考点,必须背)

String 的内存分配分 3 种场景,对应 3 种写法,内存完全不同,也是新手最容易踩坑的点,结合你的代码场景举例:

✔ 场景 1:直接赋值字符串常量 【推荐写法】String str = "张三";

java

运行

String str1 = "张三";
String str2 = "张三";

👉 内存分配过程:

  1. JVM 会先去 字符串常量池 中查找,有没有 "张三" 这个字符串;
  2. 如果没有,就在常量池中创建 "张三" 这个字符串对象,存入常量池;
  3. 如果有(比如 str2),直接让 str2 指向常量池中已有的 "张三"不会创建新对象;👉 内存图结论:str1str2 指向常量池中的同一个对象,内存地址相同 → str1 == str2 结果为true

✔ 场景 2:通过 new 关键字创建 【不推荐,浪费内存】String str = new String("张三");

java

运行

String str3 = new String("张三");

👉 内存分配过程:一定会在内存中创建【2 个对象】,内存浪费!

  1. JVM 先去字符串常量池查找 "张三",没有则创建常量池的"张三"对象;
  2. 执行new关键字,在堆内存中创建一个新的 String 对象
  3. 堆中的这个 String 对象,内部的 char [] 数组,会复制常量池里的字符数据
  4. 变量 str3 最终指向的是 堆内存中的这个新对象;👉 内存图结论:str1 == str3 结果为false(一个指向常量池,一个指向堆),str1.equals(str3)为 true(内容相同)。

✔ 场景 3:字符串拼接 String str = "张" + "三"; / String str = a + b;

这是你之前写代码经常用到的场景,分两种子情况,内存完全不同:

✨ 子情况 A:常量拼接(两边都是字面量)String str = "张" + "三";
  • JVM 在编译阶段就会做优化,直接把拼接结果变成 "张三"
  • 内存分配和场景 1 完全一样,指向常量池的"张三"无内存浪费,效率高
✨ 子情况 B:变量拼接(有变量参与)String a = "张"; String b = "三"; String str = a + b;
  • 这是String 的性能大坑!JVM 编译时无法确定变量的值,会做如下操作:

    1. 底层自动创建一个StringBuilder对象;
    2. 调用append(a)append(b)拼接;
    3. 调用toString()方法,把拼接后的内容在堆内存中创建一个新的 String 对象
    4. 变量 str 指向堆中的这个新对象;
  • 👉 核心问题:每一次变量拼接,都会创建新的 StringBuilder 和 String 对象,循环拼接时(比如循环 100 次),会创建 100 个对象,内存爆炸,效率极低 → 这就是为什么需要 StringBuilder!

✅ 4. String 不可变的优缺点(面试必问)

✔ 优点:

  1. 节省内存:常量池复用相同内容的字符串;
  2. 线程安全:内容不可变,多线程环境下不会出现并发修改问题;
  3. 哈希值缓存:hash 值缓存,作为 HashMap 的键时效率高。

✔ 缺点:

字符串的任何修改(拼接、替换、截取)都会创建新对象,频繁修改时效率极低、内存浪费 → 这就是StringBuilder诞生的唯一原因!


三、StringBuilder 底层原理 + 内存分配(你的作业高频用,核心)

✅ 1. StringBuilder 类的底层源码核心(Java8)

java

运行

public final class StringBuilder extends AbstractStringBuilder {
    // 无参构造
    public StringBuilder() {
        super(16); // 调用父类构造,默认初始容量16个字符
    }
    // 有参构造,传入初始字符串
    public StringBuilder(String str) {
        super(str.length() + 16); // 初始容量=字符串长度+16
        append(str); // 追加字符串
    }
}
// 父类 AbstractStringBuilder 核心源码
abstract class AbstractStringBuilder {
    // 核心:一个【没有被final修饰的char[]字符数组】
    char[] value;
    // 记录数组中实际存储的字符个数
    int count;
}

✅ 2. StringBuilder 核心底层特性(为什么可变?根源!)

和 String 的不可变形成鲜明对比,根源就在父类的数组:

存储字符的数组 char[] value普通的数组,没有 final 修饰,数组的引用地址可以改变,数组的内容可以直接修改!

👉 最终结果:StringBuilder 是可变字符串缓冲区,所有的追加、插入、删除操作,都是直接修改这个 char [] 数组的内容不会创建新对象,这就是效率高的根源!

✅ 3. StringBuilder 内存分配规则(核心 + 简单,只有 1 条)

java

运行

StringBuilder sb = new StringBuilder();
sb.append("张三").append(20).append("男");

👉 内存分配过程:永远只在【堆内存】中创建 1 个对象!

  1. 执行new StringBuilder(),JVM 在堆内存中创建一个 StringBuilder 对象;
  2. 这个对象内部的 char [] 数组,默认初始容量是 16 个字符
  3. 调用append()方法时,直接把字符写入这个 char [] 数组,没有任何新对象创建;
  4. 直到数组装满了(count >= 数组长度),才会触发【数组扩容】;
  5. 扩容完成后,还是在原来的对象中操作,只是数组变大了,对象地址不变。

✅ 4. StringBuilder 核心机制:数组扩容(为什么效率还高?)

扩容是 StringBuilder 的核心优化,也是为什么它比 String 拼接快的关键:

✔ 扩容触发条件:当追加的字符数量,超过了当前 char [] 数组的容量时;

✔ 扩容规则:默认扩容为 原容量 * 2 + 2(比如初始 16 → 扩容后 34 → 再扩容 70);

✔ 扩容本质:创建一个新的更大的 char [] 数组,把原数组的内容复制过去,然后让 value 指向新数组;

✔ 关键:扩容只改变数组的引用地址,StringBuilder 对象的地址永远不变,而且扩容的频率极低(比如拼接 100 个字符,只需要扩容 2-3 次),所以效率依然极高!

✅ 5. StringBuilder 的 toString () 方法内存分配(必懂)

你每次调用sb.toString()时,底层源码是:

java

运行

public String toString() {
    return new String(value, 0, count);
}

👉 内存规则:会在堆内存中创建一个新的 String 对象,把 char [] 数组的内容复制过去,返回这个 String 对象。✅ 注意:这是 StringBuilder 唯一创建新对象的地方,但是只创建 1 次,不管你 append 多少次,toString 只调用 1 次,这就是为什么循环拼接用 StringBuilder 效率天差地别!

✅ 6. StringBuilder vs StringBuffer 底层区别(面试必问)

两者的底层原理、内存分配、所有方法完全一样,唯一的区别只有一个:

  • StringBuilder:所有方法都没有加 synchronized 锁 → 线程不安全,效率极高单线程环境(作业 / 练习 / 开发 99% 场景)必用
  • StringBuffer:所有方法都加了 synchronized 锁 → 线程安全,效率稍低 → 只有多线程开发时才用;

一句话总结:日常写代码,无脑用StringBuilder就对了!


四、StringJoiner 底层原理 + 内存分配(最简单,必懂)

✅ 1. StringJoiner 核心底层本质(一句话讲透)

StringJoiner 就是 Java8 为我们封装的一个「带分隔符的 StringBuilder 工具类」,底层完全依赖 StringBuilder 实现,没有任何新的底层逻辑!

✅ 2. StringJoiner 底层源码核心(简化版)

java

运行

public final class StringJoiner {
    private final String delimiter; // 你传入的分隔符
    private final String prefix;     // 前缀
    private final String suffix;     // 后缀
    private StringBuilder value;     // 底层核心:就是一个StringBuilder对象!
    private String emptyValue;

    // 构造方法,传入分隔符
    public StringJoiner(CharSequence delimiter) {
        this(delimiter, "", "");
    }
    // 构造方法,传入分隔符+前缀+后缀
    public StringJoiner(CharSequence delimiter, CharSequence prefix, CharSequence suffix) {
        this.delimiter = delimiter.toString();
        this.prefix = prefix.toString();
        this.suffix = suffix.toString();
    }
    // add方法本质:调用StringBuilder的append方法,先加分隔符再加内容
    public StringJoiner add(CharSequence newElement) {
        prepareBuilder().append(newElement);
        return this;
    }
    // toString方法本质:调用StringBuilder的toString()
    public String toString() {
        // ... 拼接前缀后缀
        return value.toString();
    }
}

✅ 3. StringJoiner 内存分配规则

因为底层是 StringBuilder,所以内存分配和 StringBuilder完全一样

  1. new StringJoiner(",") → 底层创建一个 StringBuilder 对象,存在堆内存中;
  2. add()方法 → 调用 StringBuilder 的 append,直接修改 char [] 数组,无新对象;
  3. toString() → 调用 StringBuilder 的 toString,在堆中创建一个 String 对象返回;

✅ 4. StringJoiner 的核心价值

它没有任何性能优势,也没有任何新的底层逻辑,核心价值只有一个:帮我们封装了「分隔符、前缀、后缀」的拼接逻辑,简化代码,避免手动写判断。比如你之前拼接数组时,要手动判断是不是最后一个元素,避免多一个逗号,用 StringJoiner 就不用了,它帮你做好了所有判断。


五、String.join () 底层原理(极简补充)

String.join(",", str1, str2, str3) 是 String 的静态方法,底层就是调用了 StringJoiner,源码如下:

java

运行

public static String join(CharSequence delimiter, CharSequence... elements) {
    Objects.requireNonNull(delimiter);
    Objects.requireNonNull(elements);
    // 底层创建一个StringJoiner对象,调用add方法拼接
    StringJoiner joiner = new StringJoiner(delimiter);
    for (CharSequence cs : elements) {
        joiner.add(cs);
    }
    return joiner.toString();
}

👉 结论:String.join() → StringJoiner → StringBuilder → char[],一层套一层的封装,本质都是同一个东西,只是为了简化开发。


六、三者底层 + 内存 核心对比总结表(必背,必考,精华)

类名底层存储可变性内存分配位置核心特点内存效率使用场景
Stringfinal char[]不可变常量池 + 堆内容不能改,拼接创建新对象极低(频繁拼接内存爆炸)字符串内容固定不变、少量拼接、常量定义
StringBuilderchar[]可变仅堆内存内容可改,所有操作在一个对象完成极高(只创建 1 个对象,扩容少)所有字符串修改场景:循环拼接、追加、插入、删除、toString 重写
StringJoiner封装 StringBuilder 的char[]可变仅堆内存带分隔符拼接,自动处理前缀后缀和 StringBuilder 一致带格式的拼接:数组打印、有序列表、带分隔符的字符串拼接

七、开发 / 作业 避坑指南(结合你的代码,必看)

✅ 坑 1:循环拼接字符串用 String,不用 StringBuilder

java

运行

// 错误 ❌ 效率极低,创建大量对象
String str = "";
for(int i=0;i<100;i++){str += i;}
// 正确 ✅ 效率极高,只创建1个对象
StringBuilder sb = new StringBuilder();
for(int i=0;i<100;i++){sb.append(i);}
String str = sb.toString();

✅ 坑 2:用 new String ("张三") 创建字符串,浪费内存

java

运行

// 错误 ❌ 创建2个对象,浪费内存
String str = new String("张三");
// 正确 ✅ 只创建1个对象,常量池复用
String str = "张三";

✅ 坑 3:重写 toString 方法时,用 String 拼接不用 StringBuilder

java

运行

// 错误 ❌ 拼接多了效率低
@Override
public String toString() {
    return "姓名:"+name+"性别:"+gender+"年龄:"+age;
}
// 正确 ✅ 效率高,推荐写法
@Override
public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append("姓名:").append(name).append("性别:").append(gender).append("年龄:").append(age);
    return sb.toString();
}

八、最终总结(所有知识点精华,背下来 = 掌握所有考点)

  1. String 的底层是final char[]不可变,内存分配在常量池 + 堆,拼接创建新对象,效率低;
  2. StringBuilder 的底层是char[]可变,内存只在堆中,所有操作在一个对象完成,效率极高,是字符串修改的首选;
  3. StringJoiner 是 StringBuilder 的封装,底层一样,只是简化了带分隔符的拼接,代码更简洁;
  4. String.join () 是 StringJoiner 的封装,极简写法,适合简单的分隔拼接;
  5. 所有字符串相关类的底层,最终都是 char [] 字符数组,只是封装了不同的功能;
  6. 核心选型原则:内容不变用 String,内容要改 / StringBuilder,带分隔符拼接用 StringJoiner/String.join ()