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)、参数名和参数值。