String类与字符串操作

String的不可变性

String就像刻在石头上的字,一旦创建就改不了

你以为你改了字符串?其实是Java偷偷创建了一个新的字符串对象,原来那个还在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testImmutable() {
String s = "hello";
System.out.println("原始s的地址:" + System.identityHashCode(s));

s = s + " world"; // 看起来是修改了s,其实是创建了新对象
System.out.println("拼接后s的地址:" + System.identityHashCode(s));
// 两次打印的地址不一样!说明s指向了一个全新的对象

String a = "abc";
String b = a;
a = "xyz";
System.out.println("b = " + b); // 还是"abc",b没有跟着变
}

为什么设计成不可变?

安全:字符串常用于密码、网络连接、文件路径,如果能随意修改就太危险了

性能:不可变才能放进字符串池复用,多个变量可以安全地指向同一个对象

线程安全:不可变天生就是线程安全的,多个线程同时读也不怕

HashMap的key:String经常做HashMap的key,不可变保证hashCode不会变

字符串池(String Pool)

字符串池就像一个公共仓库,相同内容的字符串只存一份,大家共享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testStringPool() {
// 方式1:直接用双引号(会走字符串池)
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // true!指向池中同一个对象

// 方式2:用new(不走字符串池,强制在堆上创建新对象)
String s3 = new String("hello");
String s4 = new String("hello");
System.out.println(s3 == s4); // false!两个不同的对象
System.out.println(s3.equals(s4)); // true!内容是一样的

// intern() 方法:手动放进字符串池
String s5 = s3.intern();
System.out.println(s1 == s5); // true!intern()返回池中的对象
}

面试经典:new String("abc") 创建了几个对象?

1个或2个

如果池中没有”abc”:先在池中创建一个,再在堆上new一个 → 2个

如果池中已有”abc”:只在堆上new一个 → 1个

== vs equals()

这是Java最容易踩的坑之一,参考 运算符 中关系运算符部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testEquality() {
// == 比较的是内存地址(两个引用是不是指向同一个对象)
// equals() 比较的是内容(两个字符串的字符是不是一模一样)

String a = "hello";
String b = "hello";
String c = new String("hello");

System.out.println(a == b); // true(都指向字符串池中的同一个)
System.out.println(a == c); // false(一个在池中,一个在堆上)
System.out.println(a.equals(c)); // true(内容一样)

// ⚠️ 防止空指针的写法
String name = null;
// System.out.println(name.equals("test")); // ❌ NPE!
System.out.println("test".equals(name)); // ✅ 安全!返回false
}

铁律:比较字符串内容永远用 .equals(),别用 ==

常用方法速查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Test
public void testCommonMethods() {
String s = "Hello, World!";

// 长度
System.out.println(s.length()); // 13

// 取单个字符
System.out.println(s.charAt(0)); // 'H'
System.out.println(s.charAt(7)); // 'W'

// 截取子串(含头不含尾)
System.out.println(s.substring(7)); // "World!"
System.out.println(s.substring(0, 5)); // "Hello"

// 查找
System.out.println(s.indexOf("World")); // 7(返回首次出现的下标)
System.out.println(s.indexOf("Java")); // -1(找不到)
System.out.println(s.contains("World")); // true

// 替换
System.out.println(s.replace("World", "Java")); // "Hello, Java!"

// 分割
String csv = "苹果,香蕉,橘子";
String[] fruits = csv.split(",");
System.out.println(fruits.length); // 3
System.out.println(fruits[1]); // "香蕉"

// 去除首尾空格
String messy = " hello ";
System.out.println(messy.trim()); // "hello"

// 大小写转换
System.out.println(s.toUpperCase()); // "HELLO, WORLD!"
System.out.println(s.toLowerCase()); // "hello, world!"

// 判断开头结尾
System.out.println(s.startsWith("Hello")); // true
System.out.println(s.endsWith("!")); // true

// 判断是否为空
System.out.println("".isEmpty()); // true
System.out.println(" ".isEmpty()); // false(有空格不算空)
System.out.println(" ".isBlank()); // true(Java 11+,空格也算空)
}
方法 作用 示例 结果
length() 字符串长度 "abc".length() 3
charAt(i) 取第i个字符 "abc".charAt(1) ‘b’
substring(a,b) 截取[a,b) "hello".substring(1,3) “el”
indexOf(s) 查找位置 "hello".indexOf("ll") 2
contains(s) 是否包含 "hello".contains("ell") true
replace(a,b) 替换 "abc".replace("b","x") “axc”
split(s) 按分隔符拆分 "a,b,c".split(",") [“a”,”b”,”c”]
trim() 去首尾空格 " hi ".trim() “hi”
toUpperCase() 转大写 "abc".toUpperCase() “ABC”
toLowerCase() 转小写 "ABC".toLowerCase() “abc”

字符串拼接的性能问题

循环里用 + 拼接字符串就像每次搬家都买新房子,老房子直接扔了,太浪费了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void testConcatPerformance() {
// ❌ 错误写法:循环中用 + 拼接
// 每次 + 都会创建一个新的String对象,10000次循环就是10000个垃圾对象
String bad = "";
long start1 = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
bad = bad + i; // 每次都创建新对象!
}
long time1 = System.currentTimeMillis() - start1;
System.out.println("+ 拼接耗时:" + time1 + "ms");

// ✅ 正确写法:用 StringBuilder
StringBuilder good = new StringBuilder();
long start2 = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
good.append(i); // 在同一个对象上追加,不创建新对象
}
String result = good.toString();
long time2 = System.currentTimeMillis() - start2;
System.out.println("StringBuilder 耗时:" + time2 + "ms");

// 差距可能是几十倍甚至上百倍!
}

原理"a" + "b" + "c" 编译器会优化成 StringBuilder,但循环中的 + 优化不了,每次循环都new一个新的StringBuilder

详见 StringBuilder与StringBuffer

⭐ 安全角度:输入验证基础

用户输入的东西永远不可信,就像门口的快递必须先验货再签收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Test
public void testInputValidation() {
// 1. 检查长度:防止超长输入搞崩系统
String username = "admin";
if (username.length() < 3 || username.length() > 20) {
System.out.println("用户名长度必须3~20个字符");
}

// 2. 特殊字符过滤:防止XSS攻击
String input = "<script>alert('hack')</script>";
String safe = input.replace("<", "&lt;").replace(">", "&gt;");
System.out.println("过滤后:" + safe);
// 输出:&lt;script&gt;alert('hack')&lt;/script&gt;

// 3. SQL注入简介:别直接把用户输入拼进SQL!
String userInput = "' OR 1=1 --";
// ❌ 危险写法(拼接SQL)
String badSQL = "SELECT * FROM users WHERE name = '" + userInput + "'";
System.out.println("危险SQL:" + badSQL);
// 结果:SELECT * FROM users WHERE name = '' OR 1=1 --'
// 这会查出所有用户!攻击者就是利用引号闭合来注入恶意SQL

// ✅ 正确写法:使用PreparedStatement(参数化查询)
// PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE name = ?");
// ps.setString(1, userInput); // 自动转义特殊字符,安全!
}

三条基本原则

检查长度:设置合理的最大长度

过滤特殊字符:< > ' " &

参数化查询:永远别把用户输入直接拼进SQL

关于字符编码的更多知识,参考 进制与编码

常见坑总结

错误写法 正确写法 原因
字符串比较 s1 == s2 s1.equals(s2) == 比地址不比内容
空指针 name.equals("test") "test".equals(name) name可能是null
循环拼接 s = s + i StringBuilder.append(i) + 每次创建新对象
substring越界 s.substring(0, 100) 先检查 s.length() 下标超出抛异常
split正则 "1.2.3".split(".") "1.2.3".split("\\.") . 在正则里是通配符

练习题

题1:反转字符串

给定一个字符串,返回它的反转结果(不用StringBuilder.reverse)

1
2
3
4
5
6
7
8
9
10
@Test
public void testReverse() {
String input = "hello";
// 思路:从后往前取每个字符,拼起来
StringBuilder sb = new StringBuilder();
for (int i = input.length() - 1; i >= 0; i--) {
sb.append(input.charAt(i));
}
System.out.println(sb.toString()); // "olleh"
}

题2:统计字符出现次数

统计字符串中每个字符出现的次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void testCharCount() {
String s = "aabbccabc";
// 用数组当计数器(利用char可以当下标)
int[] count = new int[128]; // ASCII码只有128个
for (int i = 0; i < s.length(); i++) {
count[s.charAt(i)]++;
}
// 打印结果
for (int i = 0; i < 128; i++) {
if (count[i] > 0) {
System.out.println((char) i + " 出现了 " + count[i] + " 次");
}
}
}

题3:判断回文字符串

正着读和倒着读一样的就是回文,比如 “abcba”

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testPalindrome() {
String s = "abcba";
boolean isPalindrome = true;
for (int i = 0; i < s.length() / 2; i++) {
if (s.charAt(i) != s.charAt(s.length() - 1 - i)) {
isPalindrome = false;
break;
}
}
System.out.println(s + " 是回文?" + isPalindrome); // true
}

上一章 目录 下一章
JavaBean规范 java基础 StringBuilder与StringBuffer