目录
[一、为什么 Java 要单独设计 String 类?](#一、为什么 Java 要单独设计 String 类?)
[1. 对比 C 语言:面向对象的必然选择](#1. 对比 C 语言:面向对象的必然选择)
[2. Java String 的设计目标](#2. Java String 的设计目标)
[二、String 的 4 种创建方式(附内存分析)](#二、String 的 4 种创建方式(附内存分析))
[1. 4 种核心创建方式(完整可运行代码)](#1. 4 种核心创建方式(完整可运行代码))
[2. 底层存储说明(JDK 9 + 重要优化)](#2. 底层存储说明(JDK 9 + 重要优化))
[三、字符串常量池:JVM 的 "字符串缓存神器"](#三、字符串常量池:JVM 的 “字符串缓存神器”)
[1. 什么是字符串常量池?](#1. 什么是字符串常量池?)
[2. 不同创建方式的内存差异(面试核心)](#2. 不同创建方式的内存差异(面试核心))
[3. intern () 方法:手动将字符串入池](#3. intern () 方法:手动将字符串入池)
[四、字符串的 4 种比较方式(开发必避坑)](#四、字符串的 4 种比较方式(开发必避坑))
[1. 4 种比较方式对比](#1. 4 种比较方式对比)
[2. 实战代码示例](#2. 实战代码示例)
[3. 避坑提醒](#3. 避坑提醒)
[五、String 高频操作大全(可直接复制使用)](#五、String 高频操作大全(可直接复制使用))
[1. 字符串查找(定位字符 / 子串)](#1. 字符串查找(定位字符 / 子串))
[2. 类型转换(字符串↔基本类型)](#2. 类型转换(字符串↔基本类型))
[3. 替换、拆分、截取、去空格](#3. 替换、拆分、截取、去空格)
[✅ 核心提醒](#✅ 核心提醒)
[六、String 的不可变性:为什么不能改?](#六、String 的不可变性:为什么不能改?)
[1. 不可变性的底层实现](#1. 不可变性的底层实现)
[2. 不可变性的 3 个核心原因](#2. 不可变性的 3 个核心原因)
[3. 不可变性的 4 个核心好处](#3. 不可变性的 4 个核心好处)
[七、字符串拼接:StringBuilder vs StringBuffer](#七、字符串拼接:StringBuilder vs StringBuffer)
[1. 拼接性能问题演示](#1. 拼接性能问题演示)
[2. 高效拼接:StringBuilder(推荐)](#2. 高效拼接:StringBuilder(推荐))
[3. StringBuilder vs StringBuffer(核心区别)](#3. StringBuilder vs StringBuffer(核心区别))
[八、3 道经典 String 面试题(附详细解析)](#八、3 道经典 String 面试题(附详细解析))
[1. 字符串中第一个唯一字符](#1. 字符串中第一个唯一字符)
[2. 验证回文串](#2. 验证回文串)
[3. 字符串拼接性能对比(面试口述题)](#3. 字符串拼接性能对比(面试口述题))
在 Java 开发中,String 是当之无愧的 "顶流" 类 ------ 日常业务中 80% 以上的代码都会接触字符串操作,面试中更是绕不开常量池、不可变性等核心考点。但看似简单的 String,却藏着无数容易踩坑的细节:为什么==和equals()结果不一样?循环拼接字符串为什么性能差?常量池到底是怎么优化内存的?
本文从设计初衷→核心特性→实战用法→面试真题,层层拆解 Java 字符串的所有关键知识点,既有底层原理剖析,又有可直接复用的代码示例,帮你彻底搞懂 String,告别踩坑。
一、为什么 Java 要单独设计 String 类?
在理解 String 的特性前,我们先搞懂一个基础问题:为什么 Java 要为字符串专门设计一个类?
1. 对比 C 语言:面向对象的必然选择
C 语言中并没有专门的字符串类型,只能通过字符数组 + 库函数实现字符串功能:
cpp
// C语言字符串:数据(字符数组)和操作(库函数)分离
char str[] = "hello";
// 调用库函数完成拼接,需要手动管理内存
strcat(str, " world");
这种方式存在三大问题:
- 数据和操作分离,完全违背面向对象(OOP)的封装思想;
- 需要手动管理内存,容易出现数组越界、内存泄漏等问题;
- 字符串操作分散在各种库函数中,使用不统一、不直观。
2. Java String 的设计目标
由于字符串是开发中使用最频繁的数据类型 ,Java 专门设计java.lang.String类,核心目标是:
- ✅ 封装:把字符串的数据(字符 / 字节) 和操作(拼接、替换、查找) 封装在类中,符合 OOP 思想;
- ✅ 安全:避免手动操作内存,减少空指针、数组越界等异常;
- ✅ 易用:提供丰富的 API,一站式解决所有字符串操作需求;
- ✅ 高效:通过常量池、不可变性等设计优化性能和内存占用。
一句话总结:Java 中所有文本内容(如字面量、用户输入、文件内容),最终都会被封装成 String 对象。
二、String 的 4 种创建方式(附内存分析)
创建字符串远不止""这一种写法,不同创建方式对应不同的内存布局,也是面试高频考点。
1. 4 种核心创建方式(完整可运行代码)
java
public class StringCreateDemo {
public static void main(String[] args) {
// 方式1:字符串字面量(最常用,推荐)
String s1 = "Java String";
// 方式2:new String()构造器(不推荐,会创建多余对象)
String s2 = new String("Java String");
// 方式3:字符数组构造(适合从字符序列构建字符串)
char[] charArray = {'J', 'a', 'v', 'a'};
String s3 = new String(charArray);
// 方式4:字节数组构造(适合网络/文件IO场景)
byte[] byteArray = {97, 98, 99, 100}; // 对应ASCII码:a,b,c,d
String s4 = new String(byteArray);
// 输出结果验证
System.out.println("s1 = " + s1); // s1 = Java String
System.out.println("s2 = " + s2); // s2 = Java String
System.out.println("s3 = " + s3); // s3 = Java
System.out.println("s4 = " + s4); // s4 = abcd
}
}
2. 底层存储说明(JDK 9 + 重要优化)
- JDK 8 及之前:String 底层用
char[] value存储(char占 2 字节); - JDK 9 及之后:优化为
byte[] value + coder存储(byte占 1 字节),根据字符串编码(UTF-8/ISO-8859-1)自动选择存储方式,内存占用减少 50%; - 无论哪种版本,
value数组都被private final修饰,这是 String 不可变的核心基础。
三、字符串常量池:JVM 的 "字符串缓存神器"
常量池是 Java 对 String 的关键优化,也是面试必问的核心知识点,我们从 "是什么→为什么→怎么用" 三层拆解。
1. 什么是字符串常量池?
字符串常量池(StringTable)是 JVM 方法区中一个特殊的HashTable 结构 ,核心作用是:缓存字符串字面量,避免重复创建相同内容的 String 对象,节省内存 + 提升性能。
这是编程中经典的 "池化思想"(类似线程池、连接池):把常用的资源提前缓存,重复使用时直接取,不用重新创建。
2. 不同创建方式的内存差异(面试核心)
java
public class StringPoolDemo {
public static void main(String[] args) {
// 场景1:字面量创建(复用常量池)
String str1 = "hello";
String str2 = "hello";
// == 比较的是对象的内存地址
System.out.println(str1 == str2); // true(指向常量池同一个对象)
// 场景2:new创建(强制新建对象)
String str3 = new String("hello");
String str4 = new String("hello");
System.out.println(str3 == str4); // false(堆上两个不同对象)
// 场景3:字面量 vs new
System.out.println(str1 == str3); // false(常量池 vs 堆)
}
}
核心结论(务必记牢)
| 创建方式 | 内存位置 | 是否复用常量池 | 性能 / 内存 |
|---|---|---|---|
String s = "abc" |
常量池(无则创建,有则复用) | 是 | 最优 |
new String("abc") |
堆(新对象)+ 常量池(若不存在) | 否(堆对象唯一) | 较差(冗余对象) |
关键提醒 :new String("abc")会创建1~2 个对象:
- 如果常量池已存在 "abc":仅在堆创建 1 个新对象;
- 如果常量池无 "abc":先在常量池创建 1 个,再在堆创建 1 个,共 2 个。
3. intern () 方法:手动将字符串入池
intern()是 String 的 native 方法(底层 C++ 实现),作用是:将当前字符串对象的内容放入常量池,并返回常量池中的引用。
java
public class StringInternDemo {
public static void main(String[] args) {
// 场景1:char数组构造的字符串(初始不在常量池)
char[] arr = {'m', 'a', 'i', 'n'};
String s1 = new String(arr); // 堆对象,常量池无"main"
s1.intern(); // 手动将s1的内容"main"放入常量池
String s2 = "main"; // 复用常量池中的"main"
System.out.println(s1 == s2); // JDK 1.7+ 返回true
// 场景2:先有字面量,后intern(复用已有常量)
String s3 = new String("test"); // 常量池已创建"test"
String s4 = s3.intern(); // 返回常量池的"test"引用
String s5 = "test";
System.out.println(s4 == s5); // true
System.out.println(s3 == s5); // false(s3是堆对象)
}
}
实用场景
当你需要大量复用相同内容的字符串(如解析日志、处理海量文本),用intern()可以大幅减少内存占用(但注意:常量池空间有限,过度使用可能导致 OOM)。
四、字符串的 4 种比较方式(开发必避坑)
字符串比较是最容易出错的场景之一,核心原则:比较内容用 equals (),比较地址用 ==,我们用表格 + 代码讲清楚:
1. 4 种比较方式对比
| 方法 | 比较目标 | 返回值 | 适用场景 |
|---|---|---|---|
== |
对象的内存地址 | boolean | 判断是否为同一个对象 |
equals() |
字符串内容(区分大小写) | boolean | 常规内容比较(开发最常用) |
compareTo() |
字典序(ASCII 码差值) | int(正 / 负 / 0) | 排序、比较大小 |
compareToIgnoreCase() |
字典序(忽略大小写) | int(正 / 负 / 0) | 不区分大小写的排序 / 比较 |
2. 实战代码示例
java
public class StringCompareDemo {
public static void main(String[] args) {
String a = "Java";
String b = "java";
String c = new String("Java");
// 1. ==:比较地址(易错点)
System.out.println(a == c); // false(a在常量池,c在堆)
System.out.println(a == b); // false(内容不同,地址不同)
// 2. equals():比较内容(推荐)
System.out.println(a.equals(c)); // true(内容相同)
System.out.println(a.equals(b)); // false(区分大小写)
// 空指针安全写法(推荐):常量在前
System.out.println("Java".equals(a)); // true
// 3. compareTo():字典序比较
// 'J'的ASCII码是74,'j'是106,74-106=-32
System.out.println(a.compareTo(b)); // -32
System.out.println(b.compareTo(a)); // 32
System.out.println(a.compareTo(c)); // 0(内容相同)
// 4. compareToIgnoreCase():忽略大小写
System.out.println(a.compareToIgnoreCase(b)); // 0
}
}
3. 避坑提醒
- ❌ 错误:
if (str == "abc")(即使内容相同,地址可能不同); - ✅ 正确:
if ("abc".equals(str))(常量在前,避免 str 为 null 时抛空指针); - ✅ 进阶:JDK 1.7 + 可用
Objects.equals(str, "abc")(自动处理 null)。
五、String 高频操作大全(可直接复制使用)
String 提供了丰富的 API,以下是开发中最常用的操作,附完整示例和注意事项。
1. 字符串查找(定位字符 / 子串)
java
public class StringFindDemo {
public static void main(String[] args) {
String s = "hello-java-string";
// 1. 获取指定下标字符(下标从0开始)
char ch = s.charAt(6);
System.out.println("下标6的字符:" + ch); // j
// 2. 查找字符首次出现的下标(无则返回-1)
int firstJ = s.indexOf('j');
System.out.println("j首次出现下标:" + firstJ); // 6
// 3. 查找字符最后出现的下标
int lastT = s.lastIndexOf('t');
System.out.println("t最后出现下标:" + lastT); // 12
// 4. 查找子串首次出现的下标
int javaIndex = s.indexOf("java");
System.out.println("java子串下标:" + javaIndex); // 6
}
}
2. 类型转换(字符串↔基本类型)
java
public class StringConvertDemo {
public static void main(String[] args) {
// 场景1:基本类型 → 字符串(推荐用String.valueOf,避免null)
String numStr = String.valueOf(1024); // "1024"
String boolStr = String.valueOf(true); // "true"
// 场景2:字符串 → 基本类型
int num = Integer.parseInt("1024"); // 1024
double d = Double.parseDouble("3.14"); // 3.14
boolean bool = Boolean.parseBoolean("true"); // true
// 场景3:大小写转换(常用作统一格式)
String upper = "java".toUpperCase(); // JAVA
String lower = "JAVA".toLowerCase(); // java
System.out.println(upper + " → " + lower); // JAVA → java
// 注意:转换失败会抛NumberFormatException,需捕获
try {
Integer.parseInt("abc");
} catch (NumberFormatException e) {
System.out.println("字符串无法转换为数字");
}
}
}
3. 替换、拆分、截取、去空格
java
public class StringOptDemo {
public static void main(String[] args) {
String str = " hello-world-java ";
// 1. 替换(支持正则表达式)
String replaceAll = str.replaceAll("-", "/"); // " hello/world/java "
String replaceFirst = str.replaceFirst("-", "/"); // " hello/world-java "
// 2. 拆分(特殊字符需转义,如.、|、\)
String ip = "192.168.1.1";
String[] ipArr = ip.split("\\."); // 转义.,拆分结果:["192","168","1","1"]
System.out.println("IP拆分后第1段:" + ipArr[0]); // 192
// 3. 截取(左闭右开区间 [start, end))
String sub1 = str.substring(2, 7); // 从下标2到6:hello
String sub2 = str.substring(7); // 从下标7到末尾:-world-java
// 4. 去空格(开发常用:处理用户输入、接口参数)
String trimStr = str.trim(); // 去首尾空格:hello-world-java
// JDK 11+新增:去所有空格
// String stripAll = str.replaceAll("\\s", ""); // hello-world-java
// 输出验证
System.out.println("替换全部:" + replaceAll);
System.out.println("截取:" + sub1);
System.out.println("去空格:" + trimStr);
}
}
✅ 核心提醒
String 是不可变对象 :所有修改操作(替换、截取、拼接)都不会改变原字符串,而是返回一个新的 String 对象。
java
// 示例:不可变性验证
String original = "hello";
String modified = original.concat(" world"); // 拼接
System.out.println(original); // hello(原字符串不变)
System.out.println(modified); // hello world(新字符串)
六、String 的不可变性:为什么不能改?
很多初学者疑惑:明明可以写str = "a" + "b",为什么说 String 是不可变的?我们从底层原理→不可变原因→核心好处三层讲透。
1. 不可变性的底层实现
String 的不可变性,本质是由两个关键设计保证的:
java
// JDK 8 String核心源码(关键部分)
public final class String {
// 存储字符串的数组,private + final修饰
private final char value[];
// 哈希值缓存(不可变所以哈希值固定)
private int hash;
// 构造器(外部无法直接修改value)
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
// 所有修改方法都返回新对象(如concat、replace)
public String concat(String str) {
// 省略实现:本质是创建新的char数组,复制原内容+新内容
return new String(buf, true);
}
}
2. 不可变性的 3 个核心原因
| 设计点 | 作用 |
|---|---|
String类被final修饰 |
禁止继承,避免子类修改核心逻辑 |
value数组private final |
外部无法访问,且引用不可变(数组本身不能被替换) |
| 修改方法返回新对象 | 所有 "修改" 都是创建新对象,原对象不变 |
3. 不可变性的 4 个核心好处
- ✅ 线程安全:多线程并发访问时,无需加锁,不会出现数据不一致;
- ✅ 哈希值稳定:hashCode () 计算一次后缓存,适合做 HashMap 的 Key(HashMap 要求 Key 的哈希值不变);
- ✅ 常量池复用:不可变保证常量池中的字符串不会被篡改,可安全复用;
- ✅ 安全性:避免字符串被恶意修改(如文件路径、数据库连接字符串)。
七、字符串拼接:StringBuilder vs StringBuffer
直接用+拼接字符串,在循环中会产生大量临时对象,性能极差!我们先看问题,再讲解决方案。
1. 拼接性能问题演示
java
// 反例:循环中用+拼接,性能极差
public class StringConcatBadDemo {
public static void main(String[] args) {
long start = System.currentTimeMillis();
String s = "";
for (int i = 0; i < 100000; i++) {
s += i; // 每次拼接都创建新String对象
}
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start) + "ms"); // 约5000ms(视机器而定)
}
}
2. 高效拼接:StringBuilder(推荐)
java
// 正例:用StringBuilder拼接,性能提升100倍+
public class StringBuilderDemo {
public static void main(String[] args) {
long start = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
sb.append(i); // 直接追加到内部数组,不创建新对象
}
String result = sb.toString(); // 最终转为String
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start) + "ms"); // 约50ms
// 常用扩展方法
sb.reverse(); // 反转字符串
sb.insert(0, "prefix-"); // 在指定位置插入
sb.delete(0, 7); // 删除指定区间字符
}
}
3. StringBuilder vs StringBuffer(核心区别)
| 特性 | StringBuilder | StringBuffer |
|---|---|---|
| 线程安全 | 不安全(无锁) | 安全(方法加 synchronized) |
| 性能 | 极高(推荐) | 稍低(锁开销) |
| 适用场景 | 单线程(90% 场景) | 多线程(如并发拼接) |
| 核心方法 | append()/insert()/reverse() | 同左 |
最佳实践
- 90% 的业务场景(单线程):优先用
StringBuilder; - 多线程并发拼接(如日志收集):用
StringBuffer; - 少量固定拼接(如
"hello" + "world"):直接用+(编译器会优化为 StringBuilder)。
八、3 道经典 String 面试题(附详细解析)
面试中 String 的题目常结合原理和实战,以下 3 道是高频题,附完整代码和思路解析。
1. 字符串中第一个唯一字符
题目要求
给定一个字符串,找到它的第一个不重复的字符,并返回其索引。如果不存在,则返回 -1。
解题思路
- 第一步:遍历字符串,用数组统计每个字符出现的次数(ASCII 码范围 0-255,数组效率高于 HashMap);
- 第二步:再次遍历字符串,找到第一个次数为 1 的字符,返回其索引。
完整代码
java
public class FirstUniqCharDemo {
public static int firstUniqChar(String s) {
// 边界判断
if (s == null || s.length() == 0) {
return -1;
}
// 统计字符出现次数(ASCII码覆盖所有字符)
int[] count = new int[256];
for (int i = 0; i < s.length(); i++) {
count[s.charAt(i)]++;
}
// 找第一个出现次数为1的字符
for (int i = 0; i < s.length(); i++) {
if (count[s.charAt(i)] == 1) {
return i;
}
}
return -1;
}
public static void main(String[] args) {
System.out.println(firstUniqChar("leetcode")); // 0('l'是第一个唯一字符)
System.out.println(firstUniqChar("loveleetcode")); // 2('v')
System.out.println(firstUniqChar("aabb")); // -1(无唯一字符)
}
}
2. 验证回文串
题目要求
给定一个字符串,验证它是否是回文串(正读和反读一样),只考虑字母和数字,忽略大小写。
解题思路
- 双指针法:左指针从头部、右指针从尾部向中间移动;
- 跳过非字母 / 数字的字符,比较对应位置的字符(忽略大小写);
- 若所有对应字符都相等,则是回文串。
完整代码
javascript
public class IsPalindromeDemo {
public static boolean isPalindrome(String s) {
// 边界判断
if (s == null || s.length() == 0) {
return true;
}
// 统一转为小写,简化比较
s = s.toLowerCase();
int left = 0;
int right = s.length() - 1;
while (left < right) {
// 左指针跳过非字母/数字
if (!Character.isLetterOrDigit(s.charAt(left))) {
left++;
}
// 右指针跳过非字母/数字
else if (!Character.isLetterOrDigit(s.charAt(right))) {
right--;
}
// 字符不相等,直接返回false
else if (s.charAt(left++) != s.charAt(right--)) {
return false;
}
}
return true;
}
public static void main(String[] args) {
System.out.println(isPalindrome("A man, a plan, a canal: Panama")); // true
System.out.println(isPalindrome("race a car")); // false
System.out.println(isPalindrome("12321")); // true
}
}
3. 字符串拼接性能对比(面试口述题)
问题
为什么循环中用+拼接字符串性能差?编译器对String s = "a" + "b" + "c"有优化吗?
标准答案
- 循环中
+拼接:每次拼接都会创建新的 StringBuilder 和 String 对象,大量临时对象导致 GC 频繁,性能差; - 固定拼接
"a" + "b" + "c":编译器会优化为new StringBuilder().append("a").append("b").append("c").toString(),只创建一个 StringBuilder 和一个 String 对象,性能和手动写 StringBuilder 一致; - 最佳实践:循环拼接用 StringBuilder,固定拼接可直接用
+。
九、核心知识点总结(速记版)
- 创建方式 :优先用字面量
""(复用常量池),避免new String()(冗余对象); - 比较规则 :比较内容用
equals()(常量在前防 NPE),比较地址用==,排序用compareTo(); - 核心特性:String 不可变(final 类 + final 数组),所有修改返回新对象;
- 性能优化 :频繁拼接用
StringBuilder(单线程)/StringBuffer(多线程); - 面试核心:常量池原理、intern () 方法、不可变性原因、拼接性能对比。
掌握以上知识点,你不仅能解决日常开发中的所有 String 问题,还能轻松应对面试中的 String 考点。如果觉得本文有用,欢迎收藏转发,后续会持续更新 Java 核心知识点!