字符串常用操作
这是什么:String(字符串) = Java 里表示一串文字的类型(首字母大写、用双引号),自带一整套好用的方法。
为什么用:注册时校验手机号、搜索框过滤关键词、把一堆数据拼成一条日志——程序世界里一半的日常活在字符串上。不专门学一手,干活时就得反复翻文档。
如何用:本章先讲清 String 为什么「不可变」,再把常用方法一次练全,最后记住别用 == 比较字符串。
String 是"不可变"的
String s = "hello";
s.toUpperCase(); // 白调了!
System.out.println(s); // 还是 hello
s = s.toUpperCase(); // 要用变量"接住"新字符串
System.out.println(s); // HELLO
输出:
hello
HELLO
String 的每个方法都不修改原字符串,而是返回一个新字符串——所以必须接住返回值。这叫"不可变"(immutable),Java 有意为之,更安全。
为什么这么设计?两个理由。其一,安全:字符串常用来存用户名、路径、密码这类关键信息,如果谁拿到手都能改两笔,传递过程中就充满了惊吓;不可变意味着"原件永远锁在保险柜里,给出去的都是复印件"。其二,复用:正因为没人能改它,Java 敢把相同的字面量放进一个公共池子里(字符串常量池),写两次 "hello",内存里其实只有一份,大家共用——省内存,也不会有"改了一个影响另一个"的副作用。
代价则是效率:每次"修改"都造一个新对象。偶尔改改无所谓,循环里拼几百次就很奢侈了——这正是本章后面 StringBuilder 出场的理由。
常用方法一览
public class StringDemo {
public static void main(String[] args) {
String str = "Hello Java";
System.out.println(str.length()); // 长度(带括号,是方法!)
System.out.println(str.charAt(0)); // 取下标 0 的字符
System.out.println(str.substring(6)); // 从下标 6 截到结尾
System.out.println(str.substring(0, 5)); // 截取 [0, 5) 这段
System.out.println(str.indexOf("Java")); // 查位置,找不到返回 -1
}
}
输出:
10
H
Java
Hello
6
| 方法 | 作用 | 示例结果 |
|---|---|---|
length() |
长度 | "abc".length() → 3 |
charAt(i) |
取第 i 个字符 | "abc".charAt(0) → 'a' |
substring(a, b) |
截取 [a, b) 区间 | "hello".substring(1, 3) → "el" |
indexOf(s) |
查位置,没有则 -1 | "hello".indexOf("ll") → 2 |
equals(t) |
比较内容 | "a".equals("a") → true |
这张表是最常用的 5 个,再补充一批高频选手,凑成完整工具箱:
| 方法 | 作用 | 示例结果 |
|---|---|---|
toLowerCase() / toUpperCase() |
转小写 / 大写 | "AbC".toLowerCase() → "abc" |
trim() |
去掉两端的空格 | " hi ".trim() → "hi" |
replace(a, b) |
全部替换 | "aaa".replace("a", "b") → "bbb" |
contains(s) |
是否包含 | "hello".contains("ell") → true |
startsWith(s) |
是否以 s 开头 | "img.png".startsWith("img") → true |
equalsIgnoreCase(t) |
忽略大小写比较 | "Java".equalsIgnoreCase("JAVA") → true |
isEmpty() |
是否为空串 | "".isEmpty() → true |
注意 charAt 返回的是 char(单引号的单字符),而 substring 返回的是 String(双引号的字符串)——一个字符和一串字符在 Java 里是两种类型,这也呼应了变量章的 char 与 String 之别。
下标规则要刻进脑子:字符串下标从 0 开始,substring(0, 5) 取的是下标 0、1、2、3、4——含头不含尾。这样设计有个好处:substring(a, b) 的长度永远等于 b - a,掐指一算就知道。
血泪警告:比较字符串别用 ==
String a = "java";
String b = new String("java");
System.out.println(a == b); // false!== 比的是"是不是同一个对象"
System.out.println(a.equals(b)); // true,比的是"内容一不一样"
== 比较的是两个变量是否指向同一个对象,不是内容!比字符串内容永远用 equals。这个坑坑过无数人,包括工作多年的老手。
展开讲讲原理。变量存的不是字符串本身,而是"对象的地址"(相当于门牌号)。== 比的是两张门牌号是不是同一栋楼;equals 比的是楼里的住户名单是否一模一样。上面 b 是 new 出来的新对象——虽然内容和 a 一模一样,但它是"隔壁新盖的同名楼",门牌号不同,== 判 false。
更迷惑的是,两个字面量比较时 == 有时碰巧是 true:
String x = "java";
String y = "java";
System.out.println(x == y); // true!两个字面量共用常量池里的同一个对象
别被它骗了——这是常量池复用带来的"巧合",一旦哪边换成拼接、new、方法返回值,结果立刻翻脸。结论只有一条:比内容,写 equals,无例外。 顺带一个加分写法:拿"确定的字面量"去调用 equals,比如 "admin".equals(input)——即使 input 是 null 也只会返回 false,不会让程序崩掉。
拼接很多次?用 StringBuilder
+ 拼接在循环里拼几百次时效率低(每次都造新字符串),这时换 StringBuilder:
public class SbDemo {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 3; i++) {
sb.append("第").append(i).append("名 ");
}
System.out.println(sb);
}
}
输出:
第1名 第2名 第3名
为什么快?字符串不可变,用 + 拼一次就要"造新串、抄内容、扔旧串"折腾一轮,循环 1000 次就是 1000 轮搬家。StringBuilder 则是一个可以反复涂改的草稿本:append 直接在原文后面续写,不用搬家,写完再用 toString() 一次性定稿。
StringBuilder 的常用方法:
| 方法 | 作用 |
|---|---|
append(x) |
往末尾追加,什么类型都能接 |
insert(位置, x) |
往中间插入 |
reverse() |
整个翻转 |
length() |
当前长度 |
toString() |
定稿,变成 String |
StringBuilder sb = new StringBuilder("abcdef");
sb.reverse(); // 反转
System.out.println(sb); // fedcba
System.out.println(sb.length()); // 6
选型口诀:拼两三下用 +,直观;循环里拼、拼的次数不定,换 StringBuilder。
格式化输出:printf
拼字符串还有第三种武器——printf 格式化输出,用占位符搭好模板,再把值填进去:
public class FormatDemo {
public static void main(String[] args) {
String name = "呱小吉";
double score = 92.567;
System.out.printf("%s 的成绩是 %.1f 分%n", name, score);
}
}
输出:
呱小吉 的成绩是 92.6 分
| 占位符 | 填什么 | 效果 |
|---|---|---|
%s |
字符串 | 原样填入 |
%d |
整数 | 42 |
%.1f |
小数,保留 1 位(自动四舍五入) | 92.6 |
%n |
换行 | 同 \n |
对比 + 拼接,printf 的模板一目了然,尤其适合"一句话里塞多个值"的场景。综合练习章的 BMI 计算器就用它保留一位小数,提前混个脸熟。
逐字符处理:length + charAt 组合拳
有了 length() 和 charAt(),就能用循环把字符串"拆开逐个看"——统计字母个数、逐位校验格式都靠这招:
public class CountDemo {
public static void main(String[] args) {
String word = "ribbit";
int count = 0;
for (int i = 0; i < word.length(); i++) {
if (word.charAt(i) == 'b') {
count++;
}
}
System.out.println("b 出现了 " + count + " 次");
}
}
输出:
b 出现了 2 次
套路和数组遍历一模一样:下标从 0 走到 length() - 1,用 charAt(i) 取出当前位置的 char。注意比较单个字符用 == 就行(char 是基本类型),别和字符串的 equals 规则搞混——char 用 ==,String 用 equals,这组对比是高频考点。
❓ 常见问题
Q1:数组长度是 length 没括号,字符串长度是 length() 有括号,为什么?
数组把长度当一个"随身属性"直接读;字符串把长度当"问一句答一句"的方法调用。设计历史原因造成的不一致,死记即可:数组不带括号,字符串带括号。面试爱考,日常爱忘,忘了就靠编译器提醒。
Q2:怎么把字符串 "123" 变成能计算的数字 123?
用 Integer.parseInt("123"),返回 int;小数用 Double.parseDouble("1.5")。反过来,任何类型和字符串做 + 拼接都会自动变成字符串。这组转换在"读用户输入再计算"的场景天天用。
Q3:"a,b,c".split(",") 是干什么的?
按逗号切开,返回一个字符串数组:["a", "b", "c"]。配合数组章的遍历就能逐项处理——拆日志、解析 CSV 这类活全靠它。
Q4:indexOf 返回 -1 是什么暗号?
"没找到"的约定返回值。判断"包不包含"既可以用 contains(返回 boolean),也可以用 indexOf(...) != -1。看到 -1,第一反应就是"查找落空"。
Q5:字符串能用 for (char c : s) 这种增强 for 遍历吗?
不能,这是 String 和数组的一个差别:数组支持增强 for,String 不支持。想逐字符遍历,用刚才的组合拳 for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); }。Java 后续版本也提供了更现代的遍历方式,但掌握了下标遍历,走到哪都不慌。
⚠️ 新手翻车现场
翻车一:调用了方法却没接返回值
String name = " frog ";
name.trim(); // 白调了
System.out.println(name.length()); // 6,不是 4
问题:trim() 返回去掉空格的新串,但没人接住,原串纹丝不动——不可变规则的第一次实操翻车。
修正:name = name.trim(); 用变量接住新字符串。
翻车二:substring 下标越界
String s = "hi";
System.out.println(s.substring(1, 5)); // ❌ 运行时炸了
完整报错:
Exception in thread "main" java.lang.StringIndexOutOfBoundsException:
begin 1, end 5, length 2
问题:字符串长度是 2,substring(1, 5) 却想取到下标 4——编译能过,运行时才炸。报错信息把 begin、end、length 三个数都告诉你了,对照着改。
修正:截取前先确认 end <= s.length(),拿不准就用 Math.min(end, s.length()) 兜底。
翻车三:null 上调方法,程序当场去世
String input = null;
System.out.println(input.length()); // ❌ NullPointerException
完整报错:
Exception in thread "main" java.lang.NullPointerException
问题:null 的意思是"这变量没指向任何对象",对空气调用方法自然崩。这是 Java 出勤率最高的运行时异常。
修正:调用前判断 if (input != null);比较内容时用 "确定值".equals(可能为null的变量) 的保险写法。
🎯 什么时候用什么招
| 需求 | 首选招式 |
|---|---|
| 判断相等 | equals / equalsIgnoreCase(永远不用 ==) |
| 找子串位置 / 判断包含 | indexOf / contains |
| 取一段文字 | substring(含头不含尾) |
| 清理用户输入的首尾空格 | trim |
| 循环拼接、反复修改 | StringBuilder |
| 模板化输出(对齐、保留小数) | printf + 占位符 |
📝 本章小结
- String 不可变:所有方法返回新串,必须接住返回值
- 常用方法:
length、charAt、substring(含头不含尾)、indexOf、replace、trim、contains - 比较内容永远用
equals,==比的是对象地址,坑过无数人 - 循环拼接用 StringBuilder:
append续写,toString定稿 printf占位符%s%d%.1f让输出模板化- 记忆口诀:字符串不可变,方法返回新串要接住;比内容用 equals,== 是坑;循环拼接找 StringBuilder。
记忆锚点:String 是"复印店"——原稿锁死不给改,每个方法都给你复印一份改好的;StringBuilder 是你的草稿本,随便涂改最后誊一次;== 查的是两份复印件是不是同一张纸,equals 查的是字迹内容。
动手改
- 判断你的英文名里有没有字母
a(提示:indexOf返回 -1 就是没找到) - 把
"abcdef"截取成"cde"(提示:想想下标) - 对比
a.equals("JAVA")和a.equalsIgnoreCase("JAVA")的区别 - 用
split把"青蛙,游泳,荷叶"切成数组并逐项打印 - 用 StringBuilder 把你的名字反转过来输出,再用
printf打印一行"反转结果:xxx"
下一章:综合练习,把所学串成一个真工具 🐸