哈喽,各位 Java 学习者!欢迎来到《Java 学习日记》的第十二篇内容~ 前面十一天我们搭建了 Java 基础和面向对象的核心体系,今天要攻克 Java 中最常用的类 ------String(字符串类)。字符串是开发中无处不在的元素,而String的不可变性、常用方法、拼接优化更是面试和实战的高频考点。本文会从String的底层原理、不可变性,到常用方法、拼接优化、常量池,结合实战案例帮你彻底掌握String类!
一、String 类的核心本质:不可变的字符序列
1. 什么是 String 的不可变性?
String对象一旦创建,其内部的字符序列(内容)就无法被修改 。看似能 "修改" 字符串的操作(如substring、replace),本质都是创建了新的 String 对象,原对象内容始终不变。
2. 底层实现(Java 9+)
Java 9 之前,String底层用char[]存储字符;Java 9 之后优化为byte[](节省内存,因为大部分字符串是 ASCII 字符,占 1 字节而非 2 字节),核心源码简化如下:
java
运行
java
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// 存储字符的数组(final修饰,引用不可变)
private final byte[] value;
// 编码标识(LATIN1/UTF16)
private final byte coder;
// 构造方法:初始化value数组
public String(String original) {
this.value = original.value;
this.coder = original.coder;
}
}
3. 不可变性的核心原因
从源码能看出,String的不可变性由 3 个关键点保证:
- 类用 final 修饰 :
String不能被继承,避免子类修改其核心逻辑; - 存储字符的数组用 final 修饰 :
value数组的引用不可变(不能指向新数组); - 无 setter 方法 :
value数组是私有且无修改方法,外部无法直接操作数组内容。
实战 1:验证 String 的不可变性
java
运行
java
public class StringImmutableDemo {
public static void main(String[] args) {
// 1. 创建String对象s1,指向常量池中的"Java"
String s1 = "Java";
// 2. 看似修改,实际创建新对象,s1仍指向原"Java"
String s2 = s1.concat("学习"); // 新对象"Java学习"
String s3 = s1.replace("J", "j"); // 新对象"java"
// 输出原对象内容,仍为"Java"
System.out.println(s1); // Java
System.out.println(s2); // Java学习
System.out.println(s3); // java
// 3. 引用可改,但对象内容不可改
s1 = "Python"; // s1指向新对象"Python",原"Java"对象仍存在
System.out.println(s1); // Python
}
}
4. 不可变性的优势
- 线程安全:多线程环境下,无需同步即可安全访问字符串;
- 可缓存:字符串常量池(String Pool)的基础(下文详解);
- 安全性高:作为参数传递时(如数据库用户名 / 密码),内容不会被篡改。
二、String 的常用方法(实战高频)
String提供了大量操作字符串的方法,以下是开发中最常用的核心方法,按场景分类讲解:
1. 字符串创建与比较
| 方法 | 作用 | 示例 |
|---|---|---|
String(String original) |
构造方法创建 | new String("Java") |
equals(Object obj) |
比较字符串内容(区分大小写) | "abc".equals("ABC") → false |
equalsIgnoreCase(String str) |
比较内容(不区分大小写) | "abc".equalsIgnoreCase("ABC") → true |
== |
比较对象引用(是否指向同一内存地址) | "abc" == new String("abc") → false |
compareTo(String str) |
按字典序比较(返回 int,0 = 相等,<0 = 小于,>0 = 大于) | "a".compareTo("b") → -1 |
实战 2:字符串比较核心坑点
java
运行
java
public class StringCompareDemo {
public static void main(String[] args) {
// 1. 常量池:直接赋值的字符串指向常量池同一对象
String s1 = "Java";
String s2 = "Java";
System.out.println(s1 == s2); // true(引用相同)
System.out.println(s1.equals(s2)); // true(内容相同)
// 2. new创建:每次new都生成新对象(堆内存)
String s3 = new String("Java");
System.out.println(s1 == s3); // false(引用不同)
System.out.println(s1.equals(s3)); // true(内容相同)
// 3. 空字符串比较:避免空指针,用常量.equals(变量)
String s4 = null;
// System.out.println(s4.equals("Java")); // 空指针异常
System.out.println("Java".equals(s4)); // false(安全)
}
}
2. 字符串查找与判断
| 方法 | 作用 | 示例 |
|---|---|---|
contains(CharSequence s) |
是否包含指定子串 | "Java学习".contains("Java") → true |
startsWith(String prefix) |
是否以指定前缀开头 | "Java".startsWith("Ja") → true |
endsWith(String suffix) |
是否以指定后缀结尾 | "test.txt".endsWith(".txt") → true |
indexOf(String str) |
查找子串首次出现的索引(无则返回 - 1) | "abcabc".indexOf("bc") → 1 |
lastIndexOf(String str) |
查找子串最后出现的索引 | "abcabc".lastIndexOf("bc") → 4 |
isEmpty() |
是否为空字符串(长度为 0) | "".isEmpty() → true |
isBlank()(Java 11+) |
是否为空或全空格 | " ".isBlank() → true |
实战 3:查找与判断场景
java
运行
java
public class StringFindDemo {
public static void main(String[] args) {
String str = "Java学习日记-2025";
// 1. 包含判断
System.out.println(str.contains("学习")); // true
// 2. 前缀/后缀判断
System.out.println(str.startsWith("Java")); // true
System.out.println(str.endsWith("2025")); // true
// 3. 索引查找
System.out.println(str.indexOf("日记")); // 4
System.out.println(str.lastIndexOf("a")); // 3
// 4. 空判断
String emptyStr = " ";
System.out.println(emptyStr.isEmpty()); // false(有空格)
System.out.println(emptyStr.isBlank()); // true(Java 11+)
}
}
3. 字符串截取与替换
| 方法 | 作用 | 示例 |
|---|---|---|
substring(int beginIndex) |
从指定索引截取到末尾 | "Java学习".substring(4) → "学习" |
substring(int begin, int end) |
截取 [begin, end) 区间(左闭右开) | "Java学习".substring(0,4) → "Java" |
replace(CharSequence old, CharSequence new) |
替换所有匹配子串 | "aabbaa".replace("aa", "bb") → "bbbbbb" |
replaceFirst(String regex, String replacement) |
替换第一个匹配子串 | "aabbaa".replaceFirst("aa", "bb") → "bbaaa" |
trim() |
去除首尾空格(不处理中间) | " Java 学习 ".trim() → "Java 学习" |
strip()(Java 11+) |
去除首尾所有空白(包括全角空格) | " Java ".strip() → "Java" |
实战 4:截取与替换
java
运行
java
public class StringCutReplaceDemo {
public static void main(String[] args) {
String str = " 2025-Java-学习日记 ";
// 1. 截取
String sub1 = str.substring(3); // "2025-Java-学习日记 "
String sub2 = str.substring(3, 7); // "2025"
System.out.println(sub2); // 2025
// 2. 替换
String replace1 = str.replace("-", "|"); // " 2025|Java|学习日记 "
String replace2 = str.replaceFirst("-", "|"); // " 2025|Java-学习日记 "
System.out.println(replace1);
// 3. 去空格
String trimStr = str.trim(); // "2025-Java-学习日记"
System.out.println(trimStr);
}
}
4. 字符串拆分与转换
| 方法 | 作用 | 示例 |
|---|---|---|
split(String regex) |
按正则拆分字符串为数组 | "a,b,c".split(",") → ["a","b","c"] |
toCharArray() |
转换为 char 数组 | "Java".toCharArray() → ['J','a','v','a'] |
toUpperCase() |
转大写 | "java".toUpperCase() → "JAVA" |
toLowerCase() |
转小写 | "JAVA".toLowerCase() → "java" |
valueOf(Object obj) |
静态方法:将任意类型转为字符串 | String.valueOf(123) → "123" |
实战 5:拆分与转换
java
运行
java
public class StringSplitConvertDemo {
public static void main(String[] args) {
// 1. 拆分(注意:. | * 等正则符号需转义)
String str = "2025.12.23";
String[] arr = str.split("\\."); // 转义.,否则按任意字符拆分
for (String s : arr) {
System.out.print(s + " "); // 2025 12 23
}
// 2. 转换
char[] chars = "Java".toCharArray();
System.out.println(chars[0]); // J
// 3. 大小写转换
System.out.println("Java".toUpperCase()); // JAVA
// 4. 任意类型转字符串(避免空指针)
Integer num = null;
System.out.println(String.valueOf(num)); // "null"(安全)
// System.out.println(num.toString()); // 空指针异常
}
}
三、字符串拼接:效率优化核心(面试高频)
字符串拼接是开发中高频操作,但不同拼接方式效率差异极大,核心原因是String的不可变性 ------ 每次+拼接都会创建新对象,频繁拼接会产生大量临时对象,消耗内存且效率低。
1. 三种拼接方式对比
| 拼接方式 | 底层实现 | 效率 | 适用场景 |
|---|---|---|---|
+ 运算符 |
编译后自动转为StringBuilder(单次拼接) |
中等 | 少量、固定次数拼接(如 2-3 次) |
StringBuilder(非线程安全) |
可变字符序列,直接修改内部数组 | 高 | 单线程、大量拼接(如循环拼接) |
StringBuffer(线程安全) |
与StringBuilder类似,方法加synchronized |
中高 | 多线程、大量拼接 |
实战 6:拼接效率对比(循环 10000 次)
java
运行
java
public class StringConcatDemo {
public static void main(String[] args) {
// 1. + 拼接(效率低)
long start1 = System.currentTimeMillis();
String s1 = "";
for (int i = 0; i < 10000; i++) {
s1 += i; // 每次创建新String对象
}
long end1 = System.currentTimeMillis();
System.out.println("+拼接耗时:" + (end1 - start1) + "ms"); // 约100ms+
// 2. StringBuilder(效率高)
long start2 = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i); // 直接修改内部数组,无新对象
}
String s2 = sb.toString();
long end2 = System.currentTimeMillis();
System.out.println("StringBuilder耗时:" + (end2 - start2) + "ms"); // 约1ms
// 3. StringBuffer(线程安全,略慢于StringBuilder)
long start3 = System.currentTimeMillis();
StringBuffer sbf = new StringBuffer();
for (int i = 0; i < 10000; i++) {
sbf.append(i);
}
String s3 = sbf.toString();
long end3 = System.currentTimeMillis();
System.out.println("StringBuffer耗时:" + (end3 - start3) + "ms"); // 约2ms
}
}
2. 拼接优化最佳实践
-
单线程大量拼接 :优先用
StringBuilder,且指定初始容量(避免扩容):java
运行
java// 指定初始容量(预估长度),减少数组扩容次数 StringBuilder sb = new StringBuilder(10000); -
多线程拼接 :用
StringBuffer(或手动加锁的StringBuilder); -
少量固定拼接 :直接用
+(代码简洁,编译后会优化为StringBuilder); -
集合拼接为字符串 (Java 8+):用
String.join():java
运行
javaList<String> list = Arrays.asList("Java", "学习", "日记"); String str = String.join("-", list); // "Java-学习-日记"
四、字符串常量池(String Pool):底层优化核心
字符串常量池是 JVM 为优化字符串创建而设计的内存区域(方法区 / 元空间),核心作用是:缓存字符串常量,避免重复创建相同内容的字符串对象,节省内存。
1. 常量池的核心规则
-
直接赋值 :
String s = "Java"→ 先检查常量池是否有 "Java",有则直接指向,无则创建后指向; -
new 创建 :
String s = new String("Java")→ 先在常量池创建 "Java",再在堆内存创建新对象指向常量池的 "Java"(共 2 个对象,除非常量池已有); -
intern () 方法 :将堆中的字符串对象加入常量池(若不存在),返回常量池引用:
java
运行
javaString s1 = new String("Java"); String s2 = s1.intern(); // s2指向常量池的"Java" System.out.println(s2 == "Java"); // true
实战 7:常量池核心案例
java
运行
java
public class StringPoolDemo {
public static void main(String[] args) {
// 1. 直接赋值:指向常量池同一对象
String s1 = "Java";
String s2 = "Java";
System.out.println(s1 == s2); // true
// 2. new创建:堆对象 vs 常量池对象
String s3 = new String("Java");
System.out.println(s1 == s3); // false
// 3. intern():堆对象入池
String s4 = s3.intern();
System.out.println(s1 == s4); // true
// 4. 拼接常量:编译期优化,直接指向常量池
String s5 = "Ja" + "va";
System.out.println(s1 == s5); // true
// 5. 拼接变量:运行期拼接,创建新对象
String a = "Ja";
String s6 = a + "va";
System.out.println(s1 == s6); // false
}
}
五、综合实战:String 类高频业务场景
场景 1:用户输入校验(空判断、格式校验)
java
运行
java
public class StringValidateDemo {
// 校验手机号格式(简单版)
public static boolean isValidPhone(String phone) {
// 1. 空判断(安全)
if (phone == null || phone.isBlank()) {
System.out.println("手机号不能为空");
return false;
}
// 2. 长度判断
if (phone.length() != 11) {
System.out.println("手机号必须为11位");
return false;
}
// 3. 开头判断
if (!phone.startsWith("1")) {
System.out.println("手机号必须以1开头");
return false;
}
// 4. 是否全为数字(简化版,实际用正则)
for (char c : phone.toCharArray()) {
if (!Character.isDigit(c)) {
System.out.println("手机号只能包含数字");
return false;
}
}
return true;
}
public static void main(String[] args) {
System.out.println(isValidPhone("13800138000")); // true
System.out.println(isValidPhone("123456")); // false(长度不足)
}
}
场景 2:字符串反转(面试高频)
java
运行
java
public class StringReverseDemo {
// 方法1:用StringBuilder
public static String reverse1(String str) {
if (str == null || str.isEmpty()) {
return str;
}
return new StringBuilder(str).reverse().toString();
}
// 方法2:手动反转(char数组)
public static String reverse2(String str) {
if (str == null || str.isEmpty()) {
return str;
}
char[] chars = str.toCharArray();
int left = 0;
int right = chars.length - 1;
while (left < right) {
// 交换字符
char temp = chars[left];
chars[left] = chars[right];
chars[right] = temp;
left++;
right--;
}
return new String(chars);
}
public static void main(String[] args) {
String str = "Java学习";
System.out.println(reverse1(str)); // 习学avaJ
System.out.println(reverse2(str)); // 习学avaJ
}
}
六、高频避坑指南(新手必背)
- 空指针异常 :比较字符串时,始终用常量.equals (变量)(如
"Java".equals(s)); - == 与 equals 混淆 :
==比引用,equals比内容,切勿用==比较字符串内容; - split 方法转义 :拆分
.、|、*等正则特殊字符时,需用\\转义; - 大量拼接用 + :循环中用
+拼接字符串,效率极低,务必用StringBuilder; - new String 的滥用 :直接赋值(
String s = "Java")比new String("Java")更节省内存; - trim 与 isBlank 混淆 :
trim()仅去除半角空格,isBlank()(Java 11+)可处理全角空格和空字符串。
总结
今天我们系统掌握了String类的核心知识点:
- 不可变性 :由
final类、final数组、无 setter 方法保证,是常量池和线程安全的基础; - 常用方法:按 "比较、查找、截取、替换、拆分、转换" 分类记忆,覆盖 80% 业务场景;
- 拼接优化 :单线程大量拼接用
StringBuilder(指定初始容量),多线程用StringBuffer; - 常量池 :直接赋值复用常量池对象,
new String创建堆对象,intern()可将堆对象入池。
String是 Java 中最基础也最易踩坑的类,掌握以上核心点,能解决开发中 90% 的字符串相关问题。下一篇【Day13】,我们会讲解 "集合框架(二):Map 的核心用法与场景",帮你掌握键值对存储的核心容器!如果今天的内容对你有帮助,欢迎点赞 + 收藏 + 关注,有任何问题都可以在评论区留言,咱们一起讨论~ 明天见!🚀