字符串
9/6/26JavaJava基础About 4 min
String 的不可变性
String 在 Java 中用 final 修饰,不可被继承;其内部字符数据在 JDK 9 之前是 private final char[] value;,JDK 9 之后改为 private final byte[] value;(节省内存)。因为数组是 final 且对外不提供修改方法,所以 String 对象一旦创建就不可修改——任何看似“修改”的操作(拼接、替换、截取)都会产生新的 String 对象。
不可变带来的好处:
- 字符串常量池(String Pool) 可以安全地复用相同字面量,节省内存。
- 线程安全:不可变对象天然线程安全,可自由共享。
- 哈希缓存:
hashCode()只需计算一次并缓存,因此 String 非常适合做HashMap的 key。 - 安全:作为参数(如网络地址、文件路径、类加载)传递时不会被篡改。
String、StringBuilder、StringBuffer
| 类 | 可变性 | 线程安全 | 适用场景 |
|---|---|---|---|
String | 不可变 | 安全(不可变) | 少量字符串、常量 |
StringBuilder | 可变 | 不安全(无锁) | 单线程下大量拼接(效率高) |
StringBuffer | 可变 | 安全(方法 synchronized) | 多线程下大量拼接(效率略低) |
String s = "";
for (int i = 0; i < 10000; i++) {
s += i; // 每次都 new StringBuilder 再 toString,产生大量临时对象,性能差
}
// 正确姿势:循环外使用一个 StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i);
}
String result = sb.toString();编译器对 + 的优化
对于编译期可确定的字面量拼接,如 String s = "a" + "b" + "c";,编译器会直接优化为 "abc",不会创建 StringBuilder。但若拼接中包含变量("a" + i),编译器会在循环体内生成 new StringBuilder().append().toString(),所以循环内用 + 拼接仍然低效。
字符串拼接方式对比
"a" + "b":字面量 → 编译期常量折叠,最省。StringBuilder.append():运行时高效,推荐在循环中使用。String.concat():内部创建新 char 数组复制,单次拼接还行,循环中不推荐。String.join()/StringJoiner:按分隔符拼接多个元素很方便。String.format():格式化输出方便,但性能较低,不宜在热点路径频繁调用。
字符串常量池与 intern()
JVM 为提升性能、减少重复对象,维护了一块字符串常量池(String Pool)。
- 字面量方式
String s = "abc";:JVM 先去常量池查找,存在则直接返回引用,否则创建并放入池中。 new String("abc"):会在堆上新建一个对象,同时常量池中若没有"abc"也会创建字面量(即可能创建 1~2 个对象)。
intern() 方法:如果常量池已包含相等的字符串,返回池中的引用;否则把当前字符串加入池中并返回其引用。
String a = new String("hello");
String b = "hello";
System.out.println(a == b); // false(一个堆对象,一个池引用)
System.out.println(a.intern() == b); // true(intern 返回池引用)
String c = new String("he") + new String("llo"); // 拼接产生的新对象在堆上,常量池此时还没有 "hello"
String d = c.intern();
System.out.println(c == d); // true(c 被加入并返回自身引用)常用方法
String s = " Hello World ";
s.length(); // 14
s.trim(); // 去首尾空白 → "Hello World"
s.substring(2, 7); // "Hello"(左闭右开)
s.charAt(2); // 'H'
s.indexOf("World"); // 6
s.toLowerCase(); // " hello world "
s.replace("l", "L"); // " HeLLo WorLd "
s.split(" "); // 按空格拆分(注意首尾空白会产生空串)
s.equals("Hello World"); // 比较内容
s.equalsIgnoreCase("hello world");比较:== vs equals
==:比较引用(内存地址)。对字符串字面量由于常量池复用可能相等,但不能用于判断内容是否相同。equals:比较字符串内容,安全且推荐。- 判断是否为空/空白推荐:
str != null && !str.isEmpty(),或用StringUtils.isBlank(str)(Apache Commons / Spring 工具类)。
字符串与编码(JDK 7+)
// 显式指定字符集,避免平台默认编码导致的乱码
byte[] bytes = "中文".getBytes(StandardCharsets.UTF_8);
String s = new String(bytes, StandardCharsets.UTF_8);易错点
new String("a") + new String("b")这种拼接不会自动把结果放入字符串常量池(除非调用intern())。substring:JDK 6 及之前共享原 char 数组可能导致内存泄漏;JDK 7+ 已改为复制新数组,该问题不再存在。