Java零基础入门:字符串深度掌握——从基础API到StringBuilder性能优化

Java零基础入门:字符串深度掌握------从基础API到StringBuilder性能优化

如果说Java程序是一个人体,那么字符串(String)​ 就是流淌在血管里的血液------无处不在,不可或缺。你写的每一个系统,从用户登录、数据采集到文件解析,全都在和字符串打交道。

然而,字符串也是Java中最容易"踩坑"的地方:

为什么两个内容一样的字符串,== 比较却是 false?

为什么在循环里拼接了几万次字符串,程序就卡成PPT?

为什么 split(".") 分割不出任何东西?

StringBuilder 为什么那么快?它底层是怎么扩容的?

这一章,我将带你从JVM内存底层到API的每一个参数,把字符串彻底讲透。这是一篇适合收藏、反复阅读的"保姆级"教程。一、String的不可变性

一、String的本质:不可变性和常量池

1.1什么是String的不可变性呢?

在JAVA中,String对象一旦被创建,它的值就不能被改变,这并不是它已经锁死了,你不能用它,而是说内存中原来的那个字符串对象本身纹丝不动。。这是因为String类被final修饰,不能被继承,同时其内部的字符数组(JDK 8及之前是 char\[\] value,JDK 9之后是 byte\[\] value),且类中没有提供任何修改该数组内容的方法。此外,构造方法会对传入的字符数组进行防御性拷贝,避免外部修改影响内部数据。

不可变性的优势:⑴安全性 常用于网络地址、文件路径、数据库连接等敏感信息,防止被恶意修改。⑵线程安全 多线程可安全共享同一String对象,无需同步锁。⑶哈希码缓存 哈希值只需计算一次并缓存,提升HashMap、HashSet等集合性能。⑷字符串常量池优化 不可变性保证了常量池中对象可安全复用,减少内存开销。⑸类加载安全 类名、方法名等关键标识使用String存储,防止被篡改。

核心要点:任何对 String 的"修改"操作(拼接、替换、截取),都会产生全新的 String 对象。

例如:

Public class StringImmutability{

Public static void main(String\[\] args){

String name="张三";

System.out.println("修改前:"+name);

Name="李四";

System.out.println("修改后:"+name);}}

你真的以为name从 张三变成李四了吗?大错特错,在内存中先创建了张三对象,name指向的是 张三,但是执行李四时,JVM在内存中新创建了李四对象,name的引用指向了新对象,原来的张三对象依然还是完好无损的躺在内存中,等待垃圾回收处理器处理。

1.2 字符串常量池(String Pool)

其实常量池的本质是一个哈希表(hashTable),字符串常量池的底层实现是一个固定大小的哈希表,默认容量为1009(可通过 JVM 参数-XX:StringTableSize调整,JDK1.8 后默认是 60013)。哈希表的key:字符串的内容(字符序列),哈希表的value:字符串对象的引用(JDK1.7+)或字符串对象本身(JDK1.6 及以前),哈希表的作用:快速查找是否存在相同内容的字符串,时间复杂度接近 O (1)。

为了节省捏村,JVM在方法区(或堆中的一块特殊区域)维护了一个字符串常量池。

String a ="java";//先去常量池找,没有就创建一个,a指向它

String b ="java";//去常量池找,找到了,b也指向同一个对象

String c = new String("java");//强制在堆中创建一个新对象,不经过常量池

System.out.println(a == b); // true(指向同一个常量池对象)

System.out.println(a == c); // false(a在常量池,c在堆中,地址不同)

System.out.println(a.equals(c)); // true(内容相同)

栈内存:

a ──────┐

b ──────┼───→ 方法区/常量池:"Java"

c ──────┴───→ 堆内存:new String("Java") ───→ 常量池:"Java"

日常开发优先使用 String s = "xxx" 而不是 new String("xxx"),可以复用常量池对象,节省内存。

二、字符串比较:永远不要用==

比较方式 比较内容 适用场景
== 比较内存地址(是不是同一个对象) 判断是否为同一个引用
equals() 比较字符串内容​ 99%的场景都应该用这个​
equalsIgnoreCase() 忽略大小写比较内容 验证码校验、用户名比较

String input = "admin";

String config = new String("admin");

// ❌ 错误示范

if (input == config) {

System.out.println("相等"); // 永远不会执行!}

// ✅ 正确示范

if (input.equals(config)) {

System.out.println("内容相等!"); // 正确}

// ✅ 忽略大小写比较(验证码场景)

String code1 = "AbCd";

String code2 = "abcd";

System.out.println(code1.equalsIgnoreCase(code2)); // true

防NullPointerException写法:如果 input 可能为 null,建议写成 "admin".equals(input),这样即使 input 是 null 也不会报错(返回 false),而 input.equals("admin") 会直接崩溃。

三、核心API全家桶(深度详解)

3.1 charAt(int index)------精准定位

获取指定索引处的字符,索引从0开始

String str ="java编程";

System.out.println(str.charAt(0)); // 'J'

System.out.println(str.charAt(4)); // '编'

String text ="订单号:ORD2024-001,金额:99.5元";

Int digitcount =0;

For(int i=0;i<text.length();i++){

Char c =text.charAt(i);

If(c>='0'&&c<='9'){

Digitcount++;}}

System.out.println("数字个数:" + digitCount);

3.2 indexof()/lastIndexof()------字符串的GPS

方法 说明
indexOf(ch) 返回字符第一次出现的索引,找不到返回 -1
indexOf(str) 返回子串第一次出现的索引
indexOf(str, fromIndex) 从指定位置开始找
lastIndexOf(ch) 从后往前找,返回最后一次出现的索引

String url = "https://www.example.com/page?name=java\&id=1";

indexOf(ch):

int index = str.indexOf('x');

System.out.println(index); // 输出:-1(字符串里没有 'x')

indexOf(str):

String text = "Java is fun, Java is powerful";

int index = text.indexOf("Java");

System.out.println(index);

indexOf(str, fromIndex):

不从开头找,而是跳过前面 fromIndex 个字符,从那个位置开始往后找。

String text = "Java is fun, Java is powerful";// 从索引 5 开始找 "Java"

int index = text.indexOf("Java", 5);

System.out.println(index); // 输出:13

lastIndexOf(ch):

从右往左扫描,返回某个字符最后一次出现的索引。找不到同样返回 -1。

String filename = "photo.version.final.jpg";

int lastDot = filename.lastIndexOf('.');

System.out.println(lastDot); // 输出:22(最后一个点号的位置)

3.3 substring------精准截取(左闭右开)

方法 说明
substring(begin) 从begin截到末尾,包含begin
substring(begin, end) 截取 [begin, end) 区间(包含begin,不包含end)

String email = "zhangsan2024@company.com";

String username = email.substring(0, email.indexOf("@"));//提取@之前

System.out.println("用户名:" + username); // zhangsan2024

String domain = email.substring(email.indexOf("@") + 1);//提起域名之后

System.out.println("域名:" + domain); // company.com

String file = "report.v2.1.final.pdf";//从右往左提取文件扩展名

String ext = file.substring(file.lastIndexOf(".") + 1);

System.out.println("扩展名:" + ext); // pdf

String str = "HelloWorld";//左开右闭

System.out.println(str.substring(0, 5)); // "Hello"

3.4 split()------拆分大师(正则大坑预警)

split() 就是按你指定的"分隔符",把一整串字符串切成若干段,装进一个数组里返回。

// 1. 基本用法:按逗号分割

String fruits = "苹果,香蕉,橙子";

String\[\] arr1 = fruits.split(",");

// arr1 = "苹果", "香蕉", "橙子"

// 2. 按多个空格分割

String text = "Java Python C++"; // 多个空格

String\[\] arr2 = text.split("\\s+"); // \\s+ 表示一个或多个空白字符

// arr2 = "Java", "Python", "C++"

// 3. 限制分割次数(limit参数)

String data = "a,b,c,d,e";

String\[\] arr3 = data.split(",", 3); // 最多分成3段

// arr3 = "a", "b", "c,d,e"

// 4. 解析键值对

String query = "name=张三&age=20&city=北京";

String\[\] pairs = query.split("&");

for (String pair : pairs) {

String\[\] keyValue = pair.split("=");

System.out.println(keyValue0 + " : " + keyValue1);}

// 坑1:点号分割(点号在正则中表示"任意字符")

String ip = "192.168.1.1";

String\[\] wrong = ip.split("."); // ❌ 什么都分割不出来!

String\[\] right = ip.split("\\."); // ✅ 正确写法

// 坑2:竖线分割

String cols = "A|B|C";

String\[\] wrong2 = cols.split("|"); // ❌ 错误

String\[\] right2 = cols.split("\\|"); // ✅ 正确

// 坑3:分割后末尾的空字符串会被丢弃

String str = "a,b,c,,,";

String\[\] result = str.split(",");

System.out.println(result.length); // 3,而不是6!

// 如果想保留末尾空串,用 split(",", -1)

String\[\] result2 = str.split(",", -1);

System.out.println(result2.length); // 6

在正则中有特殊含义的字符,用作分隔符时必须加 \\ 转义:. | * + ? { } ( ) ^ $ ``

3.5 其他高频实用API

方法 说明 示例
trim() 去除首尾空格 " hi ".trim() → "hi"
toUpperCase() 转大写 "java".toUpperCase() → "JAVA"
toLowerCase() 转小写 "JAVA".toLowerCase() → "java"
replace(old, new) 替换所有匹配 "a-b-c".replace("-", "+") → "a+b+c"
replaceAll(regex, new) 正则替换 "a1b2c3".replaceAll("\\d", "*") → "a*b*c"
contains(str) 是否包含 "hello".contains("ell") → true
startsWith(prefix) 是否以...开头 "https://".startsWith("https") → true
endsWith(suffix) 是否以...结尾 "file.pdf".endsWith(".pdf") → true
length() 字符串长度 "Java".length() → 4
isEmpty() 是否为空串 "".isEmpty() → true

四、StringBuilder:可变字符串的救星

4.1 为什么String拼接是性能灾难?

String result ="";

For(int i=0;i<10000;i++){

Result +=I; //每次都创建新的String对象}

底层真相:reslut +=i在编译后等价于result = new StringBuilder().append(result).append(i).toString();

每次循环都创建了两个新对象(StringBuilder + String),10000次循环 = 20000个对象!

4.2 StringBuider 底层揭秘

StringBuilder 内部维护了一个可扩容的字符数组(char\[\]),默认初始容量为 16。

当追加的内容超过当前容量时,会创建一个更大的新数组,新容量 = 旧容量 * 2 + 2,然后把旧数据拷贝过去。

如果你知道大概要拼接多长的字符串,可以在构造时指定容量,避免频繁扩容:StringBuilder sb = new StringBuilder(10000); // 直接指定容量

五、StringBuffer

StringBuffer 是一个可变的字符序列,可以在原有对象上直接修改,不会产生大量新对象,并且它是线程安全的。

它和 StringBuilder 的继承关系完全一样:

StringBuilder没有锁:多个人同时冲进去,场面混乱(数据错乱)

StringBuffer有锁:门上有一把锁,一个人进去后锁门,出来才让别人进// StringBuffer 的 append 方法(源码)

public synchronized StringBuffer append(String str) {

super.append(str); // 调用父类的追加逻辑

return this;

}

// StringBuilder 的 append 方法(源码)

public StringBuilder append(String str) {

super.append(str); // 一模一样的逻辑,但没有锁

return this;

}

synchronized 保证了同一时刻只有一个线程能调用 append(),所以不会出乱子。但加锁、解锁需要额外开销,所以性能比 StringBuilder 低。

Object

└── AbstractStringBuilder(父类,提供扩容、追加等核心逻辑)

├── StringBuilder(兄弟A:快,但不安全)

└── StringBuffer(兄弟B:稍慢,但安全)

StringBuffer sb = new StringBuffer();

for (int i = 0; i < 10000; i++) {

sb.append(i); // 在原有对象上追加,不产生新对象}

String result = sb.toString(); // 最后转成 String

方法 说明
append(任意类型) 追加内容
insert(int offset, 内容) 在指定位置插入
delete(int start, int end) 删除指定范围(左闭右开)
reverse() 反转字符串
toString() 转换为 String
length() 当前字符长度
capacity() 内部数组容量
setLength(int newLength) 设置长度(可截断或扩充)
setCharAt(int index, char ch) 修改指定位置的字符

public class StringBufferDemo {

public static void main(String\[\] args) {

// 1. 创建(默认初始容量16,也可以指定)

StringBuffer sb = new StringBuffer(50);

// 2. 追加(链式调用)

sb.append("Hello")

.append(" ")

.append("StringBuffer")

.append("!")

.append(2024);

System.out.println("追加后:" + sb);

// 输出:Hello StringBuffer!2024

// 3. 插入

sb.insert(6, "World ");

System.out.println("插入后:" + sb);

// 输出:Hello World StringBuffer!2024

// 4. 删除

sb.delete(6, 12); // 删除 "World "

System.out.println("删除后:" + sb);

// 输出:Hello StringBuffer!2024

// 5. 修改指定位置字符

sb.setCharAt(0, 'h'); // 把索引0的 'H' 改成 'h'

System.out.println("修改后:" + sb);

// 输出:hello StringBuffer!2024

// 6. 反转

sb.reverse();

System.out.println("反转后:" + sb);

// 输出:4202!refbuferS olleh

// 7. 转回 String

String result = sb.reverse().toString(); // 再反转回来

System.out.println("最终结果:" + result);

// 8. 查看长度和容量

System.out.println("长度:" + sb.length());

System.out.println("容量:" + sb.capacity());}}

特性 String StringBuilder StringBuffer
可变性 ❌ 不可变 ✅ 可变 ✅ 可变
线程安全 ✅ 是(不可变天然安全) ❌ 否 ✅ 是(方法加了synchronized)
性能 低(频繁修改时) 最高​ 中等(有锁开销)
适用场景 字符串不经常改变 单线程、大量拼接/修改​ 多线程、大量拼接/修改
底层 final char\[\] char\[\](可扩容) char\[\](可扩容+同步锁)

六、每日一练

编写方法 public static int countVowels(String str),统计字符串中元音字母(a, e, i, o, u,不区分大小写)的个数。

编写方法 public static String maskPhone(String phone),将手机号中间4位替换为 *,如 13812345678 → 138****5678。

编写方法 public static String reverseWords(String sentence),将句子中的单词顺序反转,如 "Hello Java World" → "World Java Hello"。(提示:用 split(" ") + StringBuilder)

编写方法 public static boolean isPalindrome(String str),判断字符串是否为回文(正读反读一样),如 "上海自来水来自海上"。(提示:用 StringBuilder.reverse() 或双指针)

模拟解析HTTP请求头:给定字符串 "GET /api/user?name=张三&age=20 HTTP/1.1",提取出请求方法(GET)、路径(/api/user)、参数名和参数值。

相关推荐
java资料站5 小时前
十一、评估测试
开发语言·人工智能·python
左手の明天5 小时前
SLIP 协议封装与解析详解:原理、C 代码与实战案例
linux·c语言·开发语言·c++
shuoshuohaohao6 小时前
MyBatis 小知识点
java·spring·mybatis
海上小飞龙6 小时前
【KMP算法-下篇】同一道题:Java 库函数 2600 微秒,手写 KMP 16 微秒
java·开发语言·算法
朝朝辞暮i6 小时前
C++ 第 12 课:局部变量、作用域、变量生命周期
开发语言·c++·算法
狗凯之家源码网6 小时前
淘客口令落地页生成器 PHP 版深度评测
开发语言·php
画绛集美术6 小时前
本地化部署LLM用于美术史问答:一间美术教室的实践记录
开发语言·php
谢亮_vipxieliang7 小时前
泛型擦除、通配符与 PECS 一次讲透
java·面试
八解毒剂7 小时前
【计组】中央处理器CPU
java·开发语言·计算机组成原理