首页 / Java 入门教程 / 字符串 String:不可变的文字

Java 入门教程

字符串 String:不可变的文字

本教程共 100 篇 · 第 55 篇 · 更新于 2026-08-05 · 约 7 分钟阅读

JavaJava 入门教程String字符串常量池

55. 字符串 String

本节目标:弄懂 String 为什么一旦创建就不能改、常量池如何省内存,以及判断相等和截取替换该怎么用。

字符串大概是 Java 里出场最多的类型。你写的每一个 "hello",背后都是一个 String 对象。它最反直觉的特点是:不可变。

这三个字很多人一带而过,结果在比较相等、循环拼接、拿字符串当缓存键这些地方接连翻车。这一章把它讲透。

String 为什么不可变

不可变到底指什么

字符串对象一旦创建,它内部保存的那串字符就锁死了。你调用的 toUpperCase()replace() 看着像在改,其实都是造了一个新字符串还给你,原来那个一动没动。

String s = "hello";
String big = s.toUpperCase();
System.out.println(s);    // hello,原串没变
System.out.println(big);  // HELLO,这是新串
```bash

2 行的返回值必须接住。不少新手写成孤零零一句 `s.toUpperCase();`,然后奇怪为什么打印出来还是小写——结果被丢掉了。String 所有「修改类」方法都要接返回值,这是铁律。

### 锁死之后换来了什么

设计者不是为了给你添堵。内容固定下来,好处一串:

- **能放心共享**。多个变量指向同一个字符串对象也不怕,谁都改不了它,天生线程安全,不用加锁。
- **哈希值可以缓存**。字符串最常见的用途之一是当 Map 的键。内容不变,哈希码只算一次就存起来,之后查表直接复用,快很多。
- **系统更安全**。类名、文件路径、网络地址、数据库连接串在 Java 里都是字符串。如果这些值能在校验通过之后被中途篡改,就是实打实的漏洞。

> [!NOTE]
> 不可变不等于「引用不能变」。写 `s = "world";` 完全合法,那只是让变量改指向另一个对象,原来那个 "hello" 对象本身依旧没变。这两件事经常被混为一谈。

## 常量池:同一个字面量只存一份

### 字面量走池子

源码里直接写出来的字符串字面量,会被放进一块叫「字符串常量池」的内存区。内容相同的字面量只存一份,后来的直接复用。

```java
String a = "java";
String b = "java";
System.out.println(a == b); // true,指向池中同一个对象

一个中等规模的项目里,“utf-8”、“GET”、“id” 这类短串可能出现上千次。要是每次都造对象,内存全被同样的几个字符撑满了。池子就是为了省这份钱。

new 出来的不走池子

String c = new String("java");
System.out.println(a == c);          // false,c 是堆里另开的新对象
System.out.println(a == c.intern()); // true,intern 换回池中那一份
```bash

`new String("java")` 其实弄出了两样东西:池里那份 "java",再加堆里一个把它复制一遍的新对象。纯属浪费。`intern()` 的作用是去池子里查一下,有就返回池中那份。

> [!TIP]
> 日常直接写字面量就好,别用 `new String(...)`。`intern()` 也别乱用,它只在需要把海量重复的运行时字符串合并成一份时才有价值,滥用反而拖慢速度。

## == 还是 equals,这是个问题

### 两个反例摆在一起看

`==` 比的是「是不是同一个对象」,不是内容。同样是拼出 "java",结果可能天差地别。

```java
String x = "ja" + "va";          // 两个字面量相加,编译期就合并好了
String y = "java";
System.out.println(x == y);      // true

String p = "ja";
String q = p + "va";             // 变量参与拼接,运行时才算得出来
System.out.println(q == y);      // false!内容一样,对象不同
System.out.println(q.equals(y)); // true,比内容永远靠谱

差别在编译器身上。第 1 行两边都是常量,编译器直接算好写死成 “java”,于是复用了池中那份。第 6 行左边是变量,只能等运行时拼,产物是堆里一个崭新对象,== 自然为 false。

结论很干脆:判断字符串内容,永远用 equals== 有时对有时错,比一直错更可怕。

Warning

判等要防空指针。写成 str.equals("yes") 时,一旦 str 是 null 就抛 NullPointerException。反过来写成 "yes".equals(str) 永远安全,因为字面量绝不为 null。这个技巧叫「常量在前」。

常用方法速查表

先给一张表压压惊,用法下面展开。

方法作用示例结果
length()字符个数”hello” → 5
charAt(i)取下标 i 处的字符下标 1 → ‘e’
isEmpty()长度是否为 0"" → true
isBlank()是否全是空白(Java 11)” ” → true
contains(s)是否包含子串true / false
indexOf(s)首次出现下标,找不到返回 -1”hello” 找 “l” → 2
substring(a,b)截取下标 a 到 b(不含 b)“hello” 取 1~4 → “ell”
replace(a,b)按普通文本全部替换”a,b” → “a-b”
split(re)按正则拆成数组”a,b,c” → 三个元素
strip()去掉首尾空白(Java 11)” hi ” → “hi”
repeat(n)重复 n 次(Java 11)“ab” 重复 2 次 → “abab”
equalsIgnoreCase(s)忽略大小写比内容true / false

表里标了版本号的三个是 Java 11 才有的,老项目跑在 Java 8 上会编译不过。本教程基线是 Java 25,放心用。

查找与判断

判断是否包含、以什么开头结尾,一行就搞定:

String s = "hello world";
System.out.println(s.contains("world"));   // true
System.out.println(s.startsWith("hello")); // true
System.out.println(s.endsWith(".txt"));    // false
System.out.println(s.indexOf("o"));        // 4,第一次出现的位置
System.out.println(s.lastIndexOf("o"));    // 7,最后一次出现的位置
System.out.println(s.indexOf("xyz"));      // -1,没找到
```bash

最后一行要记牢:`indexOf` 找不到时返回 -1,不是 0 也不抛异常。所以判断「有没有」常写成 `indexOf(...) >= 0`,不过直接用 `contains` 更直白。想从后往前找就用 `lastIndexOf`,解析文件扩展名、取路径最后一段时特别顺手。

## 判空、去空白与大小写

判空有三个容易混的方法,一次说清:

```java
String s = "   ";
System.out.println(s.isEmpty()); // false,它长度是 3
System.out.println(s.isBlank()); // true,全是空白字符
System.out.println("".isEmpty()); // true

isEmpty() 只看长度是不是 0,一串空格它认为「不空」。真正想表达「没有有效内容」,用 isBlank()

去首尾空白,老方法是 trim(),它只认 ASCII 空格及以下的控制字符,中文全角空格它删不掉。Java 11 起用 strip(),按 Unicode 规则处理,更可靠:

String raw = "\u3000 hi \u3000";  // 前后是全角空格
System.out.println(raw.trim().length());  // 3,全角空格没删掉
System.out.println(raw.strip().length()); // 2,干净了
```java

> [!NOTE]
> `trim()` 属于历史兼容,不推荐在新代码里用。处理用户输入、表单字段一律换成 `strip()`。

大小写转换同样返回新串,忽略大小写比较用 `equalsIgnoreCase`,比对邮箱、验证码时很好用:

```java
System.out.println("Hello".toLowerCase());            // hello
System.out.println("Hello".equalsIgnoreCase("HELLO")); // true

截取、替换与拆分

切一段用 substring,两个参数分别是起点和终点,终点不包含:

String s = "hello";
System.out.println(s.substring(1, 4)); // "ell",取下标 1、2、3
System.out.println(s.substring(2));    // "llo",一个参数表示切到结尾
```java

替换有两套。`replace` 按普通文本替换,`replaceAll` 的第一个参数是正则表达式:

```java
System.out.println("a.b.c".replace(".", "-"));    // a-b-c,点就是点
System.out.println("a.b.c".replaceAll(".", "-")); // -----,点在正则里匹配任意字符

这两行结果完全不同,是新手常见的困惑来源。不涉及正则就用 replace,别图顺手写 replaceAll

拆分用 split,参数也是正则;拼回去用静态方法 String.join

String[] parts = "a,b,c".split(",");            // 三个元素
String joined = String.join("-", "a", "b", "c"); // a-b-c
```bash

> [!WARNING]
> `substring` 的结束下标不包含在内,很多人写 05 以为取到第 5 个字符,其实取的是下标 0~4。下标越界会抛 `StringIndexOutOfBoundsException`,截取前先确认长度。

## 转数字、转字符数组与编码

字符串和数字互转非常常见:

```java
int n = Integer.parseInt("123");   // 字符串 → 数字
String s = String.valueOf(456);    // 数字 → 字符串

parseInt 遇到非数字内容会抛 NumberFormatException,处理用户输入时要么先校验,要么用异常兜住。

要逐个处理字符,转成字符数组:

char[] cs = "hi".toCharArray();
System.out.println(cs[0]); // h
```java

涉及中文等多字节字符时,编码是重灾区。`getBytes()` 不带参数会用平台默认编码,同一份代码在不同机器上跑出不同字节,乱码就是这么来的。显式指定 UTF-8

```java
import java.nio.charset.StandardCharsets;

byte[] bytes = "你好".getBytes(StandardCharsets.UTF_8);
String back = new String(bytes, StandardCharsets.UTF_8);
System.out.println(bytes.length); // 6,一个汉字占 3 字节
System.out.println(back.length()); // 2,字符个数还是 2

最后两行值得对照着看:字节数和字符数不是一回事,length() 数的是字符不是字节。

现代 Java 的便利方法

Java 11 往后,String 又添了几个贴心方法:

System.out.println("ab".repeat(3));        // ababab
System.out.println("a\nb\nc".lines().count()); // 3,按行拆成流
System.out.println("  x  ".stripLeading()); // "x  ",只去左边
```bash

`repeat` 画分隔线特别方便,`lines()` 逐行处理多行文本比自己按换行符切稳妥得多——它同时认 Windows 和 Unix 的换行符。

## 常见疑问

**问:字符串常量池到底放在哪里?**

Java 7 之前在方法区(永久代),Java 7 起搬到了堆里。搬家之后池子能被垃圾回收,也不再受永久代大小限制。日常写代码不用关心这个位置,但面试常问。

**问:既然不可变,为什么感觉字符串拼接也没多慢?**

单行里的 `+` 拼接,编译器会自动优化成 StringBuilder。真正慢的是循环里反复用 `+` 累加,那种场景每转一圈都造新对象。下一章专门讲怎么办。

**问:比较字符串大小该用什么?**

用 `compareTo`,按字典序返回负数、0、正数。忽略大小写排序用 `compareToIgnoreCase`。别拿 `>` 或 `<` 去比字符串,那根本编译不过。

## 小结

String 不可变,所有「修改」方法都返回新串,返回值必须接住。内容相同的字面量共享常量池里同一份对象。`==` 比对象、`equals` 比内容,判等永远用 `equals` 且把常量写在前面。查找、截取、替换、拆分是日常基本功;去空白用 `strip` 不用 `trim`;涉及中文一律显式指定 UTF-8 编码。