第二篇:String、StringBuilder、StringBuffer深度剖析

  1. 第一篇:Java基础概念四连问,==与equals、hashCode约定、接口vs抽象类、深拷贝vs浅拷贝
  2. 第二篇:String、StringBuilder、StringBuffer深度剖析

前言

在上一篇文章《Java基础概念四连问》中,我们学习了==equals()的区别、hashCode()equals()的约定等基础概念。但有一个类在Java开发中使用频率最高,却也最容易被误解------String

String a = "hello"String b = new String("hello")有什么区别?字符串拼接到底用+还是StringBuilderStringBufferStringBuilder谁更快?

这些问题不仅是面试高频题,更直接影响着你的代码性能和内存使用。今天,我们就来彻底揭开String家族的神秘面纱。读完本文,你将能回答:

  • 字符串常量池在JDK 7前后有什么变化?
  • intern()方法到底做了什么?
  • 为什么说String是不可变的?
  • StringBuilderStringBuffer的源码差异是什么?

下一篇,我们将进入集合框架的核心------HashMap源码深度剖析。


一、String的不可变性

1.1 源码验证

先看String类的源码(JDK 8):

java 复制代码
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    
    // 存储字符数组(final修饰,不可变)
    private final char value[];
    
    // 哈希码缓存
    private int hash;
    
    // 构造函数
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
    
    // 替换操作返回新String对象
    public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value;
            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            if (i < len) {
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                return new String(buf, true);  // 返回新对象
            }
        }
        return this;
    }
}

关键设计

  • final class:不能被继承
  • final char[] value:字符数组引用不可变(但数组内容可变?)
  • 所有修改操作(replacesubstringtoLowerCase等)都返回新String对象

1.2 为什么说String是不可变的?

虽然final char[] value只能保证引用地址不变,但数组内容理论上可以修改(通过反射)。然而,String类没有提供任何修改内部数组的方法,所有对外API都不会改变原字符串内容。

java 复制代码
// 通过反射可以修改String内部值(证明不可变性是通过封装实现的)
String str = "hello";
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[]) field.get(str);
value[0] = 'H';
System.out.println(str);  // "Hello"

结论 :String的不可变性是通过封装实现的,而非绝对的物理不可变。

1.3 为什么要设计成不可变?

原因 说明
字符串常量池 只有不可变才能安全地共享,否则一个引用修改会影响所有
线程安全 不可变对象天然线程安全,无需同步
哈希码缓存 哈希码只需计算一次,可作为HashMap的Key
安全 避免被恶意修改(如文件路径、数据库URL等)

二、字符串常量池

2.1 常量池的位置演进

字符串常量池是理解String内存行为的关键。

JDK版本 常量池位置 原因
JDK 6及之前 永久代(PermGen) 默认空间小,容易OOM
JDK 7 堆(Heap) 永久代空间不足,移到堆中
JDK 8+ 堆(Heap) 元空间替代永久代,常量池仍在堆

2.2 两种创建方式的区别

java 复制代码
// 方式1:字面量创建
String s1 = "hello";
String s2 = "hello";

// 方式2:new创建
String s3 = new String("hello");
String s4 = new String("hello");

System.out.println(s1 == s2);  // true,指向常量池同一对象
System.out.println(s1 == s3);  // false,s3在堆中
System.out.println(s3 == s4);  // false,两个不同的堆对象

内存图解

复制代码
JDK 7+ 内存布局:
┌─────────────────────────────────────────────────────────────────────┐
│                              堆内存                                  │
├─────────────────────────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                    字符串常量池                              │   │
│  │  ┌─────────────────────────────────────────────────────┐    │   │
│  │  │                    "hello"                          │    │   │
│  │  └─────────────────────────────────────────────────────┘    │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                    普通堆对象                                │   │
│  │  ┌─────────┐    ┌─────────┐                                │   │
│  │  │ String  │    │ String  │                                │   │
│  │  │  s3     │    │  s4     │                                │   │
│  │  │  value ─┼───→│  value ─┼───→ 都指向常量池的"hello"       │   │
│  │  └─────────┘    └─────────┘                                │   │
│  └─────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘

栈:
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│ s1  │ │ s2  │ │ s3  │ │ s4  │
│  ↓  │ │  ↓  │ │  ↓  │ │  ↓  │
└─────┘ └─────┘ └─────┘ └─────┘
   │       │       │       │
   ↓       ↓       ↓       ↓
  指向常量池    指向堆对象  指向堆对象

2.3 intern()方法详解

java 复制代码
public native String intern();

作用:将字符串对象放入常量池,如果常量池中已有相同内容的字符串,则返回常量池中的引用。

java 复制代码
// intern()示例
String s1 = new String("hello");  // 堆中对象
String s2 = s1.intern();          // 常量池对象
String s3 = "hello";              // 常量池对象

System.out.println(s1 == s2);  // false(堆 vs 常量池)
System.out.println(s2 == s3);  // true(都是常量池对象)

2.4 JDK 6 vs JDK 7+ 的intern()差异

这是一个经典的面试陷阱:

java 复制代码
// JDK 6
String s1 = new String("a") + new String("b");  // "ab"在堆中
s1.intern();  // 在永久代中创建"ab",并返回
String s2 = "ab";
System.out.println(s1 == s2);  // false(堆 vs 永久代)

// JDK 7+
String s1 = new String("a") + new String("b");  // "ab"在堆中
s1.intern();  // 常量池中直接存储堆中"ab"的引用
String s2 = "ab";
System.out.println(s1 == s2);  // true!都指向堆中同一对象

JDK 7+的变化 :常量池移到堆中后,intern()不再复制字符串内容,而是将堆中对象的引用存入常量池。


三、字符串拼接的编译器优化

3.1 编译期优化

java 复制代码
// 源码
String s = "hello" + " " + "world";

// 编译后(javap -c)
String s = "hello world";  // 编译期直接拼接!

常量表达式(编译期可知的值)会在编译时直接拼接。

3.2 运行期优化

java 复制代码
// 源码
String s1 = "hello";
String s2 = s1 + " world";

// 编译后(JDK 5-8)
String s2 = new StringBuilder().append(s1).append(" world").toString();

// JDK 9+ 使用invokedynamic优化

注意 :循环中使用+拼接会创建多个StringBuilder对象:

java 复制代码
// 错误写法
String result = "";
for (int i = 0; i < 1000; i++) {
    result += i;  // 每次循环都new StringBuilder
}

// 编译后等价于
for (int i = 0; i < 1000; i++) {
    result = new StringBuilder().append(result).append(i).toString();
}
// 创建了1000个StringBuilder对象和1000个String对象

3.3 正确写法

java 复制代码
// 正确写法:显式使用StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append(i);
}
String result = sb.toString();

四、StringBuilder与StringBuffer源码对比

4.1 继承体系

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                        AbstractStringBuilder                        │
│  (可变字符序列的抽象父类)                                           │
│  ├─ char[] value        // 存储字符(非final,可修改)               │
│  └─ int count           // 已使用长度                               │
└─────────────────────────────────────────────────────────────────────┘
                              ↑
              ┌───────────────┴───────────────┐
              │                               │
    ┌─────────────────┐             ┌─────────────────┐
    │  StringBuilder  │             │  StringBuffer   │
    │  (线程不安全)   │             │  (线程安全)    │
    │  无同步          │             │  所有方法加锁   │
    └─────────────────┘             └─────────────────┘

4.2 StringBuilder源码

java 复制代码
public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence {
    
    // 无锁,性能高
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }
}

4.3 StringBuffer源码

java 复制代码
public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence {
    
    // 所有public方法都加了synchronized
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }
    
    public synchronized String toString() {
        // 使用缓存优化toString性能
        if (toStringCache == null) {
            toStringCache = Arrays.copyOfRange(value, 0, count);
        }
        return new String(toStringCache, true);
    }
}

4.4 性能对比

java 复制代码
// 性能测试
public class StringVsBuilderBenchmark {
    public static void main(String[] args) {
        int iterations = 100000;
        
        // String拼接(最慢)
        long start = System.nanoTime();
        String s = "";
        for (int i = 0; i < iterations; i++) {
            s += i;
        }
        long time1 = System.nanoTime() - start;
        
        // StringBuilder(最快)
        start = System.nanoTime();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < iterations; i++) {
            sb.append(i);
        }
        long time2 = System.nanoTime() - start;
        
        // StringBuffer(中等)
        start = System.nanoTime();
        StringBuffer sbf = new StringBuffer();
        for (int i = 0; i < iterations; i++) {
            sbf.append(i);
        }
        long time3 = System.nanoTime() - start;
        
        System.out.println("String: " + time1 / 1000000 + "ms");
        System.out.println("StringBuilder: " + time2 / 1000000 + "ms");
        System.out.println("StringBuffer: " + time3 / 1000000 + "ms");
    }
}

典型输出(10万次拼接):

复制代码
String: 28500ms      (最慢,约28秒)
StringBuilder: 8ms   (最快)
StringBuffer: 12ms   (中等,略慢于StringBuilder)

4.5 使用场景总结

场景 推荐 原因
单线程字符串拼接 StringBuilder 性能最高,无锁开销
多线程共享可变字符串 StringBuffer 线程安全
简单的固定字符串拼接 +(String) 编译器会优化,代码简洁
循环中大量拼接 StringBuilder 避免创建大量临时对象
方法内局部变量拼接 StringBuilder 线程安全不需要同步

五、常见面试题

Q1:String为什么是不可变的?

:String类被声明为final,字符数组value被声明为final private,且没有提供任何修改内部状态的方法。这样设计的原因包括:字符串常量池可以安全共享、线程安全、哈希码可缓存、安全性(避免恶意修改)。

Q2:new String("hello")创建了几个对象?

:可能创建1个或2个对象:

  • 如果常量池中已有"hello",则只在堆中创建1个String对象
  • 如果常量池中没有"hello",则在常量池创建1个,堆中创建1个,共2个

Q3:StringBuilder和StringBuffer的区别?

  • StringBuilder:线程不安全,性能高,适合单线程场景
  • StringBuffer:线程安全(方法加synchronized),性能略低,适合多线程场景
  • 两者都继承自AbstractStringBuilder,底层都是可修改的char[]

Q4:String s = "a" + "b" + "c"创建了几个对象?

:只创建1个对象。编译器会优化为String s = "abc",常量池中创建"abc"

Q5:String s = a + b + c(a、b、c是变量)创建了几个对象?

:创建1个StringBuilder和1个结果String对象。编译后等价于:

java 复制代码
String s = new StringBuilder().append(a).append(b).append(c).toString();

Q6:intern()方法在JDK 6和JDK 7+有什么区别?

  • JDK 6 :常量池在永久代,intern()会在永久代中复制一份字符串内容
  • JDK 7+ :常量池在堆中,intern()将堆中对象的引用存入常量池,不复制内容

六、总结

6.1 核心要点

是否可变 线程安全 底层存储 适用场景
String 不可变 安全 final char[] 字符串常量、作为Key
StringBuilder 可变 不安全 char[] 单线程大量拼接
StringBuffer 可变 安全(同步) char[] 多线程共享拼接

6.2 字符串常量池演进速记

复制代码
JDK 6:永久代 → 空间小,容易OOM
JDK 7:移到堆 → 空间更大,intern()存引用
JDK 8:仍在堆 → 元空间独立,常量池不变

6.3 性能最佳实践

复制代码
单次拼接 → 用String(编译器优化)
循环拼接 → 用StringBuilder(显式创建)
多线程拼接 → 用StringBuffer(或用StringBuilder加外部锁)

6.4 面试金句

如果面试官问你"String、StringBuilder、StringBuffer的区别",你可以这样回答:

"String是不可变类,底层是final char[],所有修改操作都返回新对象,适合作为常量或HashMap的Key。StringBuilder和StringBuffer都是可变字符序列,底层是可修改的char[]。StringBuilder线程不安全但性能最高,适合单线程大量拼接;StringBuffer所有public方法都加了synchronized,线程安全但性能略低,适合多线程共享。字符串常量池在JDK 7后移到堆中,intern()方法不再复制字符串内容,而是存储堆中对象的引用,这减少了内存开销。"


下篇预告

理解了String家族的底层原理,我们掌握了Java中最常用的数据结构。但Java集合框架中还有一个使用频率极高的类------HashMap

它为什么能实现O(1)的查找?JDK 8为什么要引入红黑树?多线程下为什么不能用HashMap?

下一篇《HashMap源码深度剖析------从JDK 7到JDK 8的演进》将带你深入HashMap的源码,彻底搞懂哈希表的实现原理。


如果你觉得本文有帮助,欢迎点赞、评论、转发!

相关推荐
色空大师2 小时前
【阿里云部署服务问题指南】
java·mysql·阿里云·docker
Rsun045512 小时前
9、Java 外观模式从入门到实战
java·开发语言·外观模式
清心歌2 小时前
TreeSet 深度解析
java·开发语言
迷藏4942 小时前
**RISC-V生态下的嵌入式开发新范式:从指令集到自定义外设的全流程实战**在当前国产化
java·python·risc-v
小松加哲2 小时前
Tomcat 核心原理全解析(含请求流转+组件源码+多应用配置)
java·tomcat·firefox
Lyyaoo.2 小时前
【JAVA基础面经】juc包(java.util.concurrent)
java·开发语言
色空大师2 小时前
【nacos下载安装】
java·linux·nacos·ubantu
朱一头zcy2 小时前
Java基础复习08:IO流(File类与IO流概述、字节输入输出流、字符输入输出流、缓冲流、字符转换流、对象序列化、打印流、Commons-io包介绍)
java·笔记
一叶飘零_sweeeet2 小时前
击穿 Java 高并发性能瓶颈:伪共享底层原理、缓存行填充与 @Contended 注解全维度深度拆解
java·伪共享