Java中的char、String、StringBuilder与StringBuffer 深度详解

Java中的char、String、StringBuilder与StringBuffer 深度详解
在这里插入图片描述

文章目录

在这里插入图片描述

在Java编程中,char、String、StringBuilder和StringBuffer是处理字符和字符串的四个基石。理解它们的设计哲学、底层实现和性能差异,对于编写高效、健壮的代码至关重要。本文将深入浅出地为你剖析这四大金刚的方方面面。


第一章:一切的基础——char原始类型

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

1.1 定义与本质

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

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

1.2 字符编码的演变:从char到byte

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

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

1.3 char的初始化与赋值

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 ='中';

1.4 char的运算

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

char ch ='A';System.out.println("ch is "+ ch);// 输出: ch is A ch =(char)(ch +1);// 将 'A' 的码点 (65) 加 1,得到 66,再强转为 charSystem.out.println("ch is now "+ ch);// 输出: ch is now Ychar 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

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


第二章:不可变的字符串——String类

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

2.1 类的定义与不可变性

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

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

2.2 不可变性的优势

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

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

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

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

2.3 创建String对象的两种方式

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

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

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

2.4 操作的真相:总是生成新对象

理解了不可变性,就不难明白,对 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);// 输出: HelloSystem.out.println(upper);// 输出: HELLO

2.5 字符串拼接的陷阱与优化

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

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

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


第三章:可变的字符序列——StringBuilder与StringBuffer

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

3.1 AbstractStringBuilder:共同的祖先

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

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

3.2 StringBuilder:非线程安全的“快枪手”

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

3.3 StringBuffer:线程安全的“老大哥”

  • 诞生时间:JDK 1.0就已存在。
  • 适用场景多线程环境下,多个线程可能同时操作同一个 StringBuffer 对象时。在这种场景下,为了保证数据的正确性,必须使用 StringBuffer

核心特点线程安全。它的绝大多数方法都使用了 synchronized 关键字修饰,确保在多线程并发访问时,不会出现数据错乱的问题。

// StringBuffer 的 append 方法@OverridepublicsynchronizedStringBufferappend(String str){ toStringCache =null;super.append(str);returnthis;}

3.4 核心API对比

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

方法分类常用方法描述
构造器StringBuilder() / StringBuffer()创建一个初始容量为16字符的空对象。
StringBuilder(int capacity)指定初始容量。
StringBuilder(String str)根据字符串创建,初始容量为 16 + str.length()
追加append(任意类型 x)将参数的字符串表示形式追加到序列末尾。这是最常用的方法,支持重载。
插入insert(int offset, 任意类型 x)在指定位置插入参数的字符串表示形式。
删除delete(int start, int end)删除从 startend-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()返回此序列中数据的字符串表示形式。

3.5 性能对比

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

publicclassPerformanceTest{privatestaticfinalint TIMES =20000;publicstaticvoidmain(String[] args){testString();testStringBuffer();testStringBuilder();}publicstaticvoidtestString(){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");}publicstaticvoidtestStringBuffer(){long start =System.currentTimeMillis();StringBuffer sb =newStringBuffer();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");}publicstaticvoidtestStringBuilder(){long start =System.currentTimeMillis();StringBuilder sb =newStringBuilder();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单线程下大量字符串操作(如循环拼接)多线程下共享的字符串缓冲区

4.1 选型指南:到底该用谁?

  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 是不可变类,操作后产生新对象。StringBuilderStringBuffer 是可变类,可以在原对象上修改。
  • 线程安全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 类的不可变性及其带来的设计权衡,并由此引出为了解决性能问题而诞生的 StringBuilderStringBuffer。通过对它们底层实现(继承自 AbstractStringBuilder 的可变数组与扩容机制)和线程安全性(synchronized 关键字的有无)的对比,我们明确了各自的优缺点和适用场景。

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

Read more

内存暴涨700%背后的惊天真相:AI正在吞噬一切!能源·隐私·绿色三大维度深度拆解

内存暴涨700%背后的惊天真相:AI正在吞噬一切!能源·隐私·绿色三大维度深度拆解

🔥作者简介: 一个平凡而乐于分享的小比特,中南民族大学通信工程专业研究生,研究方向无线联邦学习 🎬擅长领域:驱动开发,嵌入式软件开发,BSP开发 ❄️作者主页:一个平凡而乐于分享的小比特的个人主页 ✨收录专栏:未来思考,本专栏结合当前国家战略和实时政治,对未来行业发展的思考 欢迎大家点赞 👍 收藏 ⭐ 加关注哦!💖💖 🔥内存暴涨700%背后的惊天真相:AI正在吞噬一切!能源·隐私·绿色三大维度深度拆解 |前言| 最近装机的小伙伴们欲哭无泪:DDR5内存价格一路狂飙,部分DRAM现货价格在过去一年暴涨近700% 。大家习惯性吐槽“厂商放火”、“产能不足”,但很少有人看到,这场涨价风暴的真正推手,是那只名为“AI”的巨兽。 当你还在为多花几百块钱买内存心疼时,国家正在西部荒漠建起一座座数据中心,科技巨头正在为“吃电怪兽”抢购每一颗芯片。2026年,大型科技公司的AI相关投资预计将达到6500亿美元,较去年增长约80% 。 今天,我们从能源供应、隐私安全、绿色AI 三个维度,结合东数西算、算电协同、

By Ne0inhk
C#AI系列:从零开始打造自己的OpenClaw

C#AI系列:从零开始打造自己的OpenClaw

OpenLum.Console 项目说明 这个项目是参考OpenClaw的CSharp版控制台智能体助手,Aot发布后主体程序7mb大小,另外的Skills文件夹目前自带了浏览器操作、office文件读取等基础工具。 用户可自行动态扩展Skills(描述提供地址及操作方式后,即可学会各种技能,比如登录到公司网络报销发票、请假考勤等。注意:部分网站的DOM可能不易交互导致失败) 基于 .NET 的通用智能体 Shell,原生 AOT 发布、零第三方依赖。 面向本地/内网部署,支持 OpenAI API 兼容的各类模型(DeepSeek、Ollama、OpenAI 等)。 浏览器搜索信息获取操作 自带规划拉取信息、创建工具、完成任务(pdf文档生成) 技能的按需加载示例 全部开源免费,新朋友可以关注公众号“萤火初芒”回复"OpenLum"获取仓库地址,有问题可留言或私信作者。让我们一起探索 AI 助手的无限可能!

By Ne0inhk
AI的提示词专栏:Instruction Tuning 与自定义指令集

AI的提示词专栏:Instruction Tuning 与自定义指令集

AI的提示词专栏:Instruction Tuning 与自定义指令集 本文围绕 Instruction Tuning(指令微调)与自定义指令集展开深入解析,先阐释 Instruction Tuning 的定义、与传统 Prompt 调优的区别及核心价值,指出其通过 “指令 - 响应” 对训练让模型从通用文本生成转向精准执行任务,解决传统 Prompt 调优痛点。接着详解自定义指令集的构成要素与设计原则,给出多领域示例。随后介绍 Instruction Tuning 从数据准备、模型选择、微调训练、效果评估到部署应用的完整实施流程,结合电商客服场景实战案例说明落地要点。还针对数据不足、过拟合等常见问题提供解决方案,最后总结核心内容并展望自动指令集生成等未来趋势,为相关实践提供全面指导。 人工智能专栏介绍     人工智能学习合集专栏是 AI 学习者的实用工具。它像一个全面的 AI 知识库,把提示词设计、AI 创作、智能绘图等多个细分领域的知识整合起来。无论你是刚接触

By Ne0inhk
Flutter 组件 google_generative_language_api 适配鸿蒙 HarmonyOS 实战:生成式 AI 集成,构建大语言模型调度与全场景智能推理治理架构

Flutter 组件 google_generative_language_api 适配鸿蒙 HarmonyOS 实战:生成式 AI 集成,构建大语言模型调度与全场景智能推理治理架构

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 google_generative_language_api 适配鸿蒙 HarmonyOS 实战:生成式 AI 集成,构建大语言模型调度与全场景智能推理治理架构 前言 在鸿蒙(OpenHarmony)生态迈向全场景 AI 赋能、涉及高效的语义理解、自动化内容生成及严苛的端云协同智能隐私保护背景下,如何实现一套既能深度对接 Google 生成式语言模型(如 Gemini、PaLM)、又能保障异步请求高响应性且具备多模态输入处理能力的“AI 调度中枢”,已成为决定应用智能化水平与用户体验代差的关键。在鸿蒙设备这类强调分布式协同与端侧算力按需分配的环境下,如果应用依然采用低效的 REST 手写拼接,由于由于 payload 结构复杂性,极易由于由于“协议解析异常”导致鸿蒙应用在大模型推理环节发生由于由于由于由于通讯阻塞。 我们需要一种能够统一模型调用语义、支持流式(Streaming)响应且符合鸿蒙异步异步并发范式的

By Ne0inhk