跳到主要内容
Java 基础:char、String、StringBuilder 与 StringBuffer 核心解析 | 极客日志
Java java
Java 基础:char、String、StringBuilder 与 StringBuffer 核心解析 Java 字符处理核心在于 char 原始类型及 String、StringBuilder、StringBuffer 三大类的选择。String 基于不可变设计,天然线程安全且支持常量池优化,适合作为键或常量;JDK 9 后底层由 char[] 转为 byte[] 节省内存。StringBuilder 非线程安全,单线程下性能最优;StringBuffer 同步方法保证多线程安全但开销大。开发中应依据并发场景和修改频率灵活选用,避免循环内频繁使用 String 拼接。
GopherDev 发布于 2026/3/22 0 浏览在 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' ;
整数编码值 :直接赋值为字符在 Unicode 表中的码点(整数)。
char c3 = 65 ;
char c4 = 0101 ;
char c5 = 0x41 ;
字符字面量 :用单引号括起来的单个字符。可以是英文字母,也可以是中文字符。
char c1 = 'A' ;
char c2 = '中' ;
运算规则 由于 char 底层存储的是整数值,因此它可以进行算术运算和比较。
char ch = 'A' ;
System.out.println("ch is " + ch);
ch = (char ) (ch + 1 );
System.out.println("ch is now " + ch);
char ch2 = 'a' + 'b' ;
System.out.println(ch2);
int sum = 'a' + 'b' ;
System.out.println(sum);
关键点 :当 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 {
private final char value[];
private int hash;
}
类被 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);
创建方式对比
方式一:字面量赋值
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);
System.out.println(s3 == s4);
拼接陷阱 理解了不可变性,就不难明白,对 String 对象的任何修改操作(如拼接、替换、截取),都不是在原对象上进行的,而是返回一个新的 String 对象 。
String original = "Hello" ;
String modified = original.concat(" World" );
System.out.println(original);
System.out.println(modified);
String upper = original.toUpperCase();
System.out.println(original);
System.out.println(upper);
正是由于上述特性,在循环中使用 + 进行字符串拼接会带来严重的性能问题。
String result = "" ;
for (int i = 0 ; i < 1000 ; i++) {
result = result + i;
}
在 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,但它是理解这两个类的关键。
abstract class AbstractStringBuilder implements Appendable , CharSequence {
char [] value;
int count;
public void ensureCapacity (int minimumCapacity) {
if (minimumCapacity > value.length) {
expandCapacity(minimumCapacity);
}
}
void expandCapacity (int minimumCapacity) {
int newCapacity = value.length * 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。
@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)查找子串位置。 转 String toString()返回此序列中数据的字符串表示形式。
性能实测 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 拼接耗时:1500 ms
StringBuffer 拼接耗时:3 ms
StringBuilder 拼接耗时:1 ms
String 的 + 操作在大量拼接时性能最差,差了几个数量级。
StringBuilder 通常比 StringBuffer 快(本例中快 2-3 倍),因为省去了同步开销。
横向对比与选型指南 为了让你一目了然,这里将四个核心概念进行横向对比:
特性 char String StringBuilder StringBuffer 类型 原始数据类型 类 类 类 不可变性 / 不可变 (Immutable)可变 (Mutable)可变 (Mutable)线程安全 / 线程安全 (通过不可变性)非线程安全 线程安全 (通过 synchronized)底层存储 16 位 Unicode 值 byte[] (JDK 9+)byte[] (JDK 9+)byte[] (JDK 9+)性能(修改操作) N/A 极差(创建大量对象) 最高 中等(有同步开销) 适用场景 存储单个字符 操作少的字符串、常量、作为键的 HashMap 单线程 下大量字符串操作(如循环拼接)多线程 下共享的字符串缓冲区
选型建议
处理单个字符 :毫无疑问,使用 char。
操作少量、不变的字符串 :使用 String。例如配置项、常量、不经常变化的文本。
单线程环境下,需要大量操作字符串内容(拼接、删除、修改) :首选 StringBuilder 。例如,在方法内部构建复杂的 SQL 语句、处理 JSON 字符串、日志组装等。这是最常见的场景。
多线程环境下,多个线程需要操作同一个字符串缓冲区 :必须使用 StringBuffer 来保证数据同步和安全。例如,一个全局的日志缓冲区,多个线程都要向其追加内容。
字符串作为 HashMap 的键 :必须使用 String,因为其不可变性和正确的 hashCode() 实现。
常见面试题深度剖析
1. 谈谈你对 String 的理解,它为什么是不可变的?
类本身被 final 修饰 ,不可继承,防止子类破坏。
底层存储字符的 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 中字符与字符串的奥秘。
相关免费在线工具 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