跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava

Java 基础:char、String、StringBuilder 与 StringBuffer 核心解析

Java 字符处理核心在于 char 原始类型及 String、StringBuilder、StringBuffer 三大类的选择。String 基于不可变设计,天然线程安全且支持常量池优化,适合作为键或常量;JDK 9 后底层由 char[] 转为 byte[] 节省内存。StringBuilder 非线程安全,单线程下性能最优;StringBuffer 同步方法保证多线程安全但开销大。开发中应依据并发场景和修改频率灵活选用,避免循环内频繁使用 String 拼接。

GopherDev发布于 2026/3/22更新于 2026/11/5113 浏览
Java 基础:char、String、StringBuilder 与 StringBuffer 核心解析

在 Java 编程中,char、String、StringBuilder 和 StringBuffer 是处理字符和字符串的四个基石。理解它们的设计哲学、底层实现和性能差异,对于编写高效、健壮的代码至关重要。

一切的基础——char 原始类型

在探讨复杂的字符串类之前,我们首先需要了解构成字符串的最基本单元:char。

定义与本质

char 是 Java 中的一种原始数据类型(Primitive Type),用于表示一个单一的 16 位 Unicode 字符。在 Java 诞生之初,设计者就采用了 Unicode 字符集,这使得 Java 天生具有良好的国际化支持。

  • 大小:16 位(2 个字节),范围从 0 到 65,535(\u0000 到 \uffff)。
  • 无符号性:char 是一个无符号类型,这意味着它不能表示负数。

编码演变

在 JDK 9 之前,String 类的内部实现也是采用 char[] 数组来存储字符。然而,大多数应用程序使用的字符串主要由 Latin-1 字符集(如英文、数字)构成,这些字符仅需一个字节(8 位)即可表示,用两个字节的 char 来存储会造成一半的内存浪费。

因此,从 JDK 9 开始,为了优化内存占用,String(以及 StringBuilder 和 StringBuffer 的底层)不再使用 char[],而是改用了 byte[] 数组,并引入一个 coder(编码器)字段来标识使用的是 LATIN1(每个字符 1 字节)还是 UTF16(每个字符 2 字节)编码。这是一个非常重要的底层变化,但对开发者来说是透明的,我们在逻辑上依然可以将它们视为字符序列。

初始化与赋值

char 的赋值方式非常灵活,可以通过以下几种方式:

转义字符:表示一些特殊功能字符。

char c7 = '\n'; // 换行符
char c8 = '\''; // 单引号字符本身
char c9 = '\\'; // 反斜杠字符本身

Unicode 转义序列:使用 \u 前缀加上 4 位十六进制数。

char c6 = '\u0041'; // 对应 'A'

整数编码值:直接赋值为字符在 Unicode 表中的码点(整数)。

char c3 = 65; // 十进制,对应 'A'
char c4 = 0101; // 八进制,对应 'A'
char c5 = 0x41; // 十六进制,对应 'A'

字符字面量:用单引号括起来的单个字符。可以是英文字母,也可以是中文字符。

char c1 = 'A';
char c2 = '中';

运算规则

由于 char 底层存储的是整数值,因此它可以进行算术运算和比较。

char ch = 'A';
System.out.println("ch is " + ch); // 输出:ch is A
ch = (char) (ch + 1); // 将 'A' 的码点 (65) 加 1,得到 66,再强转为 char
System.out.println("ch is now " + ch); // 输出:ch is now B

char ch2 = 'a' + 'b'; // 'a'(97) + 'b'(98) = 195,结果在 int 范围内
System.out.println(ch2); // 输出:195 对应的字符?这里实际上输出的是 195 作为 char 类型的字符,需要查码表。
int sum = 'a' + 'b';
System.out.println(sum); // 输出:195

关键点:当 char 和 char 或 char 和 int 进行运算时,结果会被提升为 int 类型。如果需要重新赋给 char 变量,必须进行显式的强制类型转换 (char)。

不可变的字符串——String 类

String 类是 Java 中使用频率最高的类之一,其'不可变性'是其最核心的特征。

不可变性实现

查看 String 类的源码(以 JDK 8 为例,后续版本底层数组变为 byte[],但逻辑一致),我们可以清晰地看到其不可变性的实现:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[]; // JDK 9 之后变为 private final byte[] value
    /** Cache the hash code for the string */
    private int hash;
    // Default to 0
    // ... 其他代码
}
  • 类被 final 修饰:这意味着 String 类不能被继承,防止子类破坏其不可变行为。
  • 存储数组被 private final 修饰:value 数组的引用不可变,且无法从外部访问。final 保证了数组的引用一旦指向某个地址后就不能再改变。虽然没有直接的语法阻止数组内部元素的变化,但 String 类没有提供任何可以修改数组元素的方法,从而确保了内部的字符数组也'不可变'。

优势与常量池

这种精心的设计带来了许多好处:

  • 线程安全:由于对象内容不可变,它可以在多个线程之间自由共享,无需任何同步措施。
  • 哈希值缓存:如上源码所示,String 类中有一个 hash 字段。因为字符串不可变,其哈希值在第一次计算后就可以被缓存起来,之后直接返回。这使得 String 非常适合作为 HashMap 或 HashTable 的键,提高了查找效率。

字符串常量池(String Pool):这是不可变性带来的最大性能优化之一。当创建一个字符串字面量(如 String s = "hello";)时,JVM 会检查常量池中是否已存在相同内容的字符串。如果存在,则直接返回其引用;如果不存在,则在池中创建新字符串并返回引用。这种机制极大地节省了内存。

String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // 输出 true,因为指向常量池中的同一个对象

创建方式对比

  • 方式一:字面量赋值
    String str1 = "abc";
    这种方式可能会从字符串常量池中获取对象。

  • 方式二:new 关键字
    String str2 = new String("abc");
    这种方式一定会在堆(Heap)中创建一个新的 String 对象。如果常量池中还没有 "abc" 这个字符串,JVM 会先在常量池中创建,然后再在堆中创建对象。因此,这种方式通常会创建 1 个或 2 个对象。

String s3 = new String("hello");
String s4 = new String("hello");
System.out.println(s1 == s3); // 输出 false,s1 指向常量池,s3 指向堆
System.out.println(s3 == s4); // 输出 false,s3 和 s4 指向堆中不同的对象

拼接陷阱

理解了不可变性,就不难明白,对 String 对象的任何修改操作(如拼接、替换、截取),都不是在原对象上进行的,而是返回一个新的 String 对象。

String original = "Hello";
String modified = original.concat(" World");
System.out.println(original); // 输出:Hello (原对象未变)
System.out.println(modified); // 输出:Hello World (新对象)
String upper = original.toUpperCase();
System.out.println(original); // 输出:Hello
System.out.println(upper); // 输出:HELLO

正是由于上述特性,在循环中使用 + 进行字符串拼接会带来严重的性能问题。

// 低效的写法
String result = "";
for (int i = 0; i < 1000; i++) {
    result = result + i; // 每次循环都会创建新的 String 对象
}

在 JDK 5 之后,Java 编译器会对 + 运算符进行优化,自动将其转换为 StringBuilder 的 append 操作。例如 String c = a + b; 会被编译为 (new StringBuilder()).append(a).append(b).toString();。但是,在循环体内,这种优化依然会导致每次循环都新建一个 StringBuilder 对象,反编译后的字节码可以清晰地证明这一点。因此,在循环或频繁修改字符串的场景下,我们必须手动使用 StringBuilder 或 StringBuffer。

可变的字符序列——StringBuilder 与 StringBuffer

为了解决 String 不可变带来的性能问题,Java 提供了两个可变的字符序列类:StringBuilder 和 StringBuffer。它们都继承自 AbstractStringBuilder 类,底层使用可变的字符数组(JDK 9 后为 byte[])来存储数据。

共同祖先 AbstractStringBuilder

虽然我们不能直接使用 AbstractStringBuilder,但它是理解这两个类的关键。

// 以 JDK 8 为例
abstract class AbstractStringBuilder implements Appendable, CharSequence {
    char[] value; // 非 final,存储字符序列,JDK 9 后变为 byte[]
    int count; // 已使用的字符个数
    // 扩容机制
    public void ensureCapacity(int minimumCapacity) {
        if (minimumCapacity > value.length) {
            expandCapacity(minimumCapacity);
        }
    }
    void expandCapacity(int minimumCapacity) {
        int newCapacity = value.length * 2 + 2; // 新容量通常是旧容量的 2 倍 +2
        if (newCapacity < minimumCapacity) {
            newCapacity = minimumCapacity;
        }
        // 创建新数组并复制原数据
        value = Arrays.copyOf(value, newCapacity);
    }
    // ...
}
  • 可变性:value 数组没有被 final 修饰,可以被重新赋值指向一个新的数组地址,也可以修改数组内部元素。
  • 自动扩容:当调用 append 或 insert 方法时,如果当前字符序列的长度超过了底层数组的容量,它会自动触发 expandCapacity 方法,创建一个更大的新数组(通常是原容量的 2 倍 +2),并将原内容复制过去。

StringBuilder vs StringBuffer

  • StringBuilder:

    • 诞生时间:JDK 1.5 引入。
    • 核心特点:非线程安全。它的所有方法(如 append, insert, delete 等)都没有使用 synchronized 关键字进行同步。
    • 适用场景:单线程环境下操作字符串缓冲区。因为避免了锁的竞争和获取开销,它通常拥有最好的性能,是单线程字符串操作的默认选择。
  • StringBuffer:

    • 诞生时间:JDK 1.0 就已存在。
    • 核心特点:线程安全。它的绝大多数方法都使用了 synchronized 关键字修饰,确保在多线程并发访问时,不会出现数据错乱的问题。
    • 适用场景:多线程环境下,多个线程可能同时操作同一个 StringBuffer 对象时。在这种场景下,为了保证数据的正确性,必须使用 StringBuffer。
// StringBuffer 的 append 方法
@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

API 对比

三个类都实现了 CharSequence 接口,因此它们的方法非常相似。StringBuilder 和 StringBuffer 的 API 是兼容的,可以无缝替换。

方法分类常用方法描述
构造器StringBuilder() / StringBuffer()创建一个初始容量为 16 字符的空对象。
StringBuilder(int capacity)指定初始容量。
StringBuilder(String str)根据字符串创建,初始容量为 16 + str.length()。
追加append(任意类型 x)将参数的字符串表示形式追加到序列末尾。这是最常用的方法,支持重载。
插入insert(int offset, 任意类型 x)在指定位置插入参数的字符串表示形式。
删除delete(int start, int end)删除从 start 到 end-1 的子序列。
deleteCharAt(int index)删除指定位置的字符。
替换与反转replace(int start, int end, String str)用 str 替换指定范围的字符。
reverse()将序列反转。
修改setCharAt(int index, char ch)修改指定位置的字符。
查询charAt(int index) / length()获取指定字符/长度。
indexOf(String str) / lastIndexOf(String str)查找子串位置。
转 StringtoString()返回此序列中数据的字符串表示形式。

性能实测

下面通过一个简单的性能测试来直观感受三者的差异:

public class PerformanceTest {
    private static final int TIMES = 20000;

    public static void main(String[] args) {
        testString();
        testStringBuffer();
        testStringBuilder();
    }

    public static void testString() {
        long start = System.currentTimeMillis();
        String str = "";
        for (int i = 0; i < TIMES; i++) {
            str += "java";
        }
        long end = System.currentTimeMillis();
        System.out.println("String 拼接耗时:" + (end - start) + "ms");
    }

    public static void testStringBuffer() {
        long start = System.currentTimeMillis();
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < TIMES; i++) {
            sb.append("java");
        }
        String str = sb.toString();
        long end = System.currentTimeMillis();
        System.out.println("StringBuffer 拼接耗时:" + (end - start) + "ms");
    }

    public static void testStringBuilder() {
        long start = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < TIMES; i++) {
            sb.append("java");
        }
        String str = sb.toString();
        long end = System.currentTimeMillis();
        System.out.println("StringBuilder 拼接耗时:" + (end - start) + "ms");
    }
}

典型输出结果(不同机器配置有差异,但趋势一致):

String 拼接耗时:1500ms
StringBuffer 拼接耗时:3ms
StringBuilder 拼接耗时:1ms

结论:

  1. String 的 + 操作在大量拼接时性能最差,差了几个数量级。
  2. StringBuilder 通常比 StringBuffer 快(本例中快 2-3 倍),因为省去了同步开销。

横向对比与选型指南

为了让你一目了然,这里将四个核心概念进行横向对比:

特性charStringStringBuilderStringBuffer
类型原始数据类型类类类
不可变性/不可变 (Immutable)可变 (Mutable)可变 (Mutable)
线程安全/线程安全 (通过不可变性)非线程安全线程安全 (通过 synchronized)
底层存储16 位 Unicode 值byte[] (JDK 9+)byte[] (JDK 9+)byte[] (JDK 9+)
性能(修改操作)N/A极差(创建大量对象)最高中等(有同步开销)
适用场景存储单个字符操作少的字符串、常量、作为键的 HashMap单线程下大量字符串操作(如循环拼接)多线程下共享的字符串缓冲区

选型建议

  1. 处理单个字符:毫无疑问,使用 char。
  2. 操作少量、不变的字符串:使用 String。例如配置项、常量、不经常变化的文本。
  3. 单线程环境下,需要大量操作字符串内容(拼接、删除、修改):首选 StringBuilder。例如,在方法内部构建复杂的 SQL 语句、处理 JSON 字符串、日志组装等。这是最常见的场景。
  4. 多线程环境下,多个线程需要操作同一个字符串缓冲区:必须使用 StringBuffer 来保证数据同步和安全。例如,一个全局的日志缓冲区,多个线程都要向其追加内容。
  5. 字符串作为 HashMap 的键:必须使用 String,因为其不可变性和正确的 hashCode() 实现。

常见面试题深度剖析

1. 谈谈你对 String 的理解,它为什么是不可变的?

答:String 的不可变性体现在:

  1. 类本身被 final 修饰,不可继承,防止子类破坏。
  2. 底层存储字符的 byte[] (或 char[]) 数组被 private final 修饰,引用不可变,且 String 类没有提供任何可以修改该数组内部元素的方法(如 setCharAt())。所有看似修改的操作,如 concat(), substring(), replace() 等,都是返回一个新的 String 对象。 这种设计带来了线程安全、字符串常量池的内存复用、哈希值缓存等巨大优势。

2. String、StringBuilder、StringBuffer 的区别?

答:三者都是用来处理字符串的,主要区别如下:

  • 可变性:String 是不可变类,操作后产生新对象。StringBuilder 和 StringBuffer 是可变类,可以在原对象上修改。
  • 线程安全:String 是线程安全的。StringBuffer 的方法使用了 synchronized 修饰,是线程安全的。StringBuilder 的方法没有同步,是线程不安全的。
  • 性能:在单线程环境下,StringBuilder 的性能最好,因为它没有同步开销。StringBuffer 次之。String 在进行大量修改时性能最差。

3. String s = new String("xyz"); 创建了几个对象?

答:这取决于'xyz'这个字符串常量是否已经存在于字符串常量池中。

  • 如果常量池中已经存在"xyz":那么只在堆中创建一个 String 对象。答案是 1 个。
  • 如果常量池中还没有"xyz":JVM 会先在常量池中创建字面量"xyz",然后再在堆中创建一个 String 对象。答案是 2 个。

4. 为什么用 StringBuilder 要好于用 String 的'+'拼接?

答:虽然编译器会将 + 优化为 StringBuilder,但在循环等场景中,优化是局部的。例如 for 循环内的 +,每次循环都会在循环体内生成一个新的 StringBuilder 对象,然后调用 append(),最后调用 toString() 返回。这会产生大量的中间对象,给垃圾回收带来巨大压力,严重影响性能。而手动使用 StringBuilder 可以在循环外部创建一个对象,在整个循环中复用这一个对象进行 append,极大地减少了对象创建,提升了效率。

总结

本文从微观的 char 原始类型讲起,深入剖析了其在 Java 中的 Unicode 特性和运算规则。随后,我们详细探讨了 String 类的不可变性及其带来的设计权衡,并由此引出为了解决性能问题而诞生的 StringBuilder 和 StringBuffer。通过对它们底层实现(继承自 AbstractStringBuilder 的可变数组与扩容机制)和线程安全性(synchronized 关键字的有无)的对比,我们明确了各自的优缺点和适用场景。

理解这些核心概念的区别与联系,不仅仅是应对面试,更是为了在日常开发中做出正确的技术选型,写出既高效又稳健的代码。希望这篇详解能帮助你彻底掌握 Java 中字符与字符串的奥秘。

目录

  1. 一切的基础——char 原始类型
  2. 定义与本质
  3. 编码演变
  4. 初始化与赋值
  5. 运算规则
  6. 不可变的字符串——String 类
  7. 不可变性实现
  8. 优势与常量池
  9. 创建方式对比
  10. 拼接陷阱
  11. 可变的字符序列——StringBuilder 与 StringBuffer
  12. 共同祖先 AbstractStringBuilder
  13. StringBuilder vs StringBuffer
  14. API 对比
  15. 性能实测
  16. 横向对比与选型指南
  17. 选型建议
  18. 常见面试题深度剖析
  19. 1. 谈谈你对 String 的理解,它为什么是不可变的?
  20. 2. String、StringBuilder、StringBuffer 的区别?
  21. 3. String s = new String("xyz"); 创建了几个对象?
  22. 4. 为什么用 StringBuilder 要好于用 String 的“+”拼接?
  23. 总结

更多推荐文章

查看全部
  • 后端开发入门:HTML 基础语法与实战
  • C++ 泛型编程与模板技术详解
  • 二分查找进阶:如何精准定位排序数组的边界
  • 从 SEO 到 GEO:315 晚会曝光的 AI 投毒黑产与技术防御
  • 系统架构师技术复盘:Flowable、软考备考与 AIGC 实践之路
  • 无人机与地面站通信距离及飞行高度估算方法
  • 基于协同过滤算法的 Flask 理财产品推荐系统实现
  • LRU 缓存机制设计与 JavaScript 实现
  • VR 多相电源架构与关键器件选型解析
  • Python Web 开发基础:爬虫、框架与接口服务实战
  • Photoshop 集成 ComfyUI AI 绘画功能指南
  • 前端开发转型 AI Agent 实战指南
  • SmolVLA 模型 TensorRT 加速实战:ONNX 导出与部署优化
  • 基于 Java 与 Leaflet 构建湖南省道路长度 WebGIS 系统
  • Stable Diffusion 常用模型详解:Checkpoint、LoRA 与 ControlNet 等
  • 基于 Meta Quest3 的 XLeRobot 机器人 VR 控制系统搭建与实操
  • RustDesk 服务端完整安装部署教程
  • UI-UX-Pro-Max Skill 使用指南:利用 AI 辅助前端界面开发
  • MySQL 内置函数指南:日期、字符串与数学函数实战
  • Ubuntu 系统安装 Docker 及配置国内镜像源

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online