Java String 字符串核心特性与 API 详解
本文详解 Java String 的核心特性,包括底层实现差异(JDK 8 vs 9+)、不可变性原理及常量池机制。涵盖常用 API 如拼接、替换、截取、查找、转换等,并提供代码示例。对比了 String、StringBuilder 和 StringBuffer 在可变性、线程安全及效率上的区别,最后总结了常见面试题,帮助开发者深入理解字符串处理。

本文详解 Java String 的核心特性,包括底层实现差异(JDK 8 vs 9+)、不可变性原理及常量池机制。涵盖常用 API 如拼接、替换、截取、查找、转换等,并提供代码示例。对比了 String、StringBuilder 和 StringBuffer 在可变性、线程安全及效率上的区别,最后总结了常见面试题,帮助开发者深入理解字符串处理。

String 是 Java 中用于表示不可变字符序列的引用类型,位于 java.lang 包下。JVM 会自动加载该包,无需手动导入。它并非 8 种基本数据类型(byte、short、int、long、float、double、char、boolean),但经 JVM 特殊优化,拥有类似基本类型的使用体验,比如可直接赋值。
String 的底层存储结构在 JDK 9 发生重大变更,核心目的是节省内存空间。具体差异如下:
| JDK 版本 | 底层存储 | 优势 | 适用场景 |
|---|---|---|---|
| JDK 8 及之前 | char[] value(每个 char 占 2 字节,UTF-16 编码) | 实现简单,无需编码判断 | 包含大量非 ASCII 字符(如中文、特殊符号)的场景 |
| JDK 9 及之后 | byte[] value + byte coder(coder 标识编码:0=ISO-8859-1,1=UTF-16) | ASCII 字符(占 1 字节)场景下节省 50% 内存 | 包含大量 ASCII 字符(如英文、数字、符号)的场景(绝大多数业务场景) |
String 对象一旦创建,其内部的字符序列(value 数组)就无法被修改。所谓的'修改'操作(如拼接、替换),本质上都是创建新的 String 对象,原对象保持不变。
底层通过三大特性保障不可变性,缺一不可:
频繁修改字符串(如循环拼接)会创建大量临时对象,占用内存并增加 GC 压力,此时推荐使用 StringBuilder(非线程安全)或 StringBuffer(线程安全,效率较低)。
public class StringImmutabilityDemo {
public static void main(String[] args) {
String s1 = "abc";
String s2 = s1; // s2 与 s1 指向常量池同一对象
s1 = s1 + "d"; // 拼接操作创建新对象 'abcd',s1 指向新对象
System.out.println(s1); // 输出:abcd
System.out.println(s2); // 输出:abc(原对象未变)
// 反射破坏不可变性(仅作演示,禁止生产使用)
try {
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true); // 暴力访问私有字段
char[] value = (char[]) valueField.get(s2);
value[0] = 'x';
System.out.println(s2); // 输出:xbc(原对象被修改)
} catch (Exception e) {
e.printStackTrace();
}
}
}
字符串常量池是 JVM 为 String 专门设计的内存区域(属于方法区),用于存储字符串常量,核心作用是复用对象、减少内存占用。当创建字符串时,JVM 会先检查常量池:若存在相同内容的字符串,直接返回其引用;若不存在,创建新字符串存入常量池并返回引用。
String 对象有两种创建方式,其在内存中的存储位置、是否复用常量池对象均不同,是面试高频考点。
优先使用常量池,流程如下:
优先在堆内存创建对象,流程如下:
结论:new String('abc') 至少创建 1 个对象(堆对象),最多创建 2 个对象(堆对象 + 常量池对象,若常量池无对应值)。
public class StringPoolDemo {
public static void main(String[] args) {
// 方式 1:字面量赋值,复用常量池对象
String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2); // true(引用同一常量池对象)
System.out.println(s1.equals(s2)); // true(内容相同)
// 方式 2:new 创建,堆对象不同
String s3 = new String("abc");
String s4 = new String("abc");
System.out.println(s3 == s4); // false(堆中两个不同对象)
System.out.println(s3.equals(s4)); // true(内容相同)
// 堆对象与常量池对象对比
System.out.println(s1 == s3); // false(分别指向常量池、堆)
}
}
intern() 方法是 String 类提供的手动关联常量池的方法,作用是:将当前字符串对象的内容存入常量池(若不存在),并返回常量池中该对象的引用。
public class StringInternDemo {
public static void main(String[] args) {
String s1 = new String("abc");
String s2 = s1.intern(); // 将 'abc' 存入常量池(已存在),返回常量池引用
String s3 = "abc";
System.out.println(s1 == s2); // false(s1 指向堆,s2 指向常量池)
System.out.println(s2 == s3); // true(均指向常量池)
// 常量池不存在时的场景
String s4 = new String("xy") + new String("z"); // 堆对象 "xyz",常量池无 "xyz"
String s5 = s4.intern(); // 常量池创建 "xyz",返回其引用
String s6 = "xyz";
System.out.println(s4 == s5); // JDK7+ 为 true(常量池存储堆对象引用),JDK6 为 false
System.out.println(s5 == s6); // true
}
}
注意:JDK 7 对 intern() 做了优化,若常量池不存在该字符串,会直接存储堆对象的引用(而非复制内容),进一步节省内存。
String 类提供了大量操作字符串的 API,按功能分类整理,结合示例说明核心用法,覆盖开发高频场景。
// 返回字符串的长度(字符个数),注意与数组长度属性区分(数组用 length,字符串用 length() 方法)
// 示例:获取字符串长度
String str = "Java String";
System.out.println(str.length()); // 输出:11(包含空格,空格算一个字符)
// 判断字符串是否为空(长度为 0),仅当字符串是 "" 时返回 true,无法判断 null(判断 null 需先做非空校验,否则抛空指针异常)
// 示例:判断字符串是否为空
String emptyStr = "";
String nonEmptyStr = "test";
String nullStr = null;
System.out.println(emptyStr.isEmpty()); // 输出:true
System.out.println(nonEmptyStr.isEmpty()); // 输出:false
// System.out.println(nullStr.isEmpty()); // 报错:NullPointerException
// 安全判断写法
System.out.println(nullStr != null && !nullStr.isEmpty()); // 输出:false
// 仅能拼接 String 类型,若传入 null 会抛出 NullPointerException,拼接后原字符串不变,返回新字符串
// 示例:使用 concat 拼接字符串
String str1 = "Hello";
String str2 = "World";
String result = str1.concat(" ").concat(str2); // 支持链式调用
System.out.println(result); // 输出:Hello World
System.out.println(str1); // 输出:Hello(原字符串未变)
// String error = str1.concat(null); // 报错:NullPointerException
// 支持任意数据类型(基本类型、引用类型)拼接,null 会被当作 "null" 处理,编译期优化为 StringBuilder(循环拼接除外,循环中 + 号会重复创建 StringBuilder,效率低)
// 示例:使用 + 号拼接字符串
String str = "年龄:";
int age = 25;
String result1 = str + age; // 拼接基本类型
String result2 = "姓名:" + "张三" + "," + result1; // 链式拼接字符串
String result3 = "null 拼接:" + null; // 处理 null
System.out.println(result1); // 输出:年龄:25
System.out.println(result2); // 输出:姓名:张三,年龄:25
System.out.println(result3); // 输出:null 拼接:null
// 替换字符串中所有指定的旧字符为新字符,若旧字符不存在,返回原字符串
// 示例:替换指定字符
String str = "abacada";
String result = str.replace('a', 'x'); // 替换所有 'a' 为 'x'
String noChange = str.replace('z', 'y'); // 旧字符不存在,返回原字符串
System.out.println(result); // 输出:xbxcdxd
System.out.println(noChange); // 输出:abacada
替换字符串中所有指定的子串为目标子串,支持 String、StringBuilder 等 CharSequence 类型。
// 示例:替换指定子串
String str = "Hello World, World is beautiful";
String result = str.replace("World", "Java"); // 替换所有 "World" 为 "Java"
System.out.println(result); // 输出:Hello Java, Java is beautiful
按正则表达式匹配内容,替换所有符合条件的子串,正则特殊字符需转义。
// 示例:正则替换(去除所有数字、替换空格)
String str = "abc123def456 789";
String noNum = str.replaceAll("\\d", ""); // 匹配所有数字并替换为空
String noSpace = str.replaceAll("\\s", "-"); // 匹配所有空格并替换为 "-"
System.out.println(noNum); // 输出:abcdef
System.out.println(noSpace); // 输出:abc123def456-789
// 按正则表达式匹配内容,仅替换第一个符合条件的子串
// 示例:替换第一个匹配的子串
String str = "abacada";
String result = str.replaceFirst("a", "x"); // 仅替换第一个 'a' 为 'x'
System.out.println(result); // 输出:xbacada
// 从指定索引(包含该索引字符)开始截取,到字符串末尾结束,索引越界会抛出 StringIndexOutOfBoundsException
// 示例:从指定索引截取到末尾
String str = "abcdefgh";
String result1 = str.substring(3); // 从索引 3 开始截取
String result2 = str.substring(0); // 从开头截取,返回原字符串
System.out.println(result1); // 输出:defgh
System.out.println(result2); // 输出:abcdefgh
// String error = str.substring(10); // 报错:StringIndexOutOfBoundsException
遵循'左闭右开'原则,截取从 beginIndex(包含)到 endIndex(不包含)的子串,beginIndex 不能大于 endIndex。
// 示例:指定起始和结束索引截取
String str = "abcdefgh";
String result = str.substring(2, 6); // 截取索引 2、3、4、5 的字符(不包含 6)
System.out.println(result); // 输出:cdef
// String error = str.substring(6, 2); // 报错:StringIndexOutOfBoundsException
// 按正则表达式分割字符串,返回字符串数组,若末尾有匹配项,会忽略空字符串;若无匹配项,返回仅含原字符串的数组
// 示例:按正则分割字符串
import java.util.Arrays;
String str1 = "a,b,c,d,e";
String[] arr1 = str1.split(","); // 按逗号分割
String str2 = "192.168.1.1";
String[] arr2 = str2.split("\\."); // 点是正则特殊字符,需双重转义
String str3 = "abc";
String[] arr3 = str3.split(","); // 无匹配项,返回原字符串数组
System.out.println(Arrays.toString(arr1)); // 输出:[a, b, c, d, e]
System.out.println(Arrays.toString(arr2)); // 输出:[192, 168, 1, 1]
System.out.println(Arrays.toString(arr3)); // 输出:[abc]
// limit 控制分割结果的数组长度,limit>0 时,分割 limit-1 次,返回长度为 limit 的数组;limit=0 时,效果同 split(String regex);limit<0 时,分割所有匹配项,不忽略末尾空字符串
// 示例:指定 limit 分割字符串
import java.util.Arrays;
String str = "a,b,c,d,e";
String[] arr1 = str.split(",", 3); // limit=3,分割 2 次,返回 3 个元素
String[] arr2 = str.split(",", 0); // 等同于无 limit,忽略末尾空串
String[] arr3 = str.split(",", -2); // 分割所有,不忽略末尾空串(此处无空串)
System.out.println(Arrays.toString(arr1)); // 输出:[a, b, c,d,e]
System.out.println(Arrays.toString(arr2)); // 输出:[a, b, c, d, e]
System.out.println(Arrays.toString(arr3)); // 输出:[a, b, c, d, e]
// 返回指定字符(传入字符的 Unicode 编码也可)第一次出现的索引,未找到返回 -1
// 示例:查找字符第一次出现的索引
String str = "Hello World";
int index1 = str.indexOf('o'); // 查找 'o' 的索引
int index2 = str.indexOf(111); // 111 是 'o' 的 Unicode 编码,效果同上
int index3 = str.indexOf('z'); // 未找到,返回 -1
System.out.println(index1); // 输出:4
System.out.println(index2); // 输出:4
System.out.println(index3); // 输出:-1
// 从 fromIndex(包含)开始,查找指定字符第一次出现的索引,fromIndex 超出字符串长度返回 -1
// 示例:从指定索引开始查找字符
String str = "Hello World";
int index = str.indexOf('o', 5); // 从索引 5 开始查找 'o'
System.out.println(index); // 输出:7(索引 5 之后第一个 'o' 在 7 的位置)
// 返回指定子串第一次出现的索引,子串为空时返回 0,未找到返回 -1
// 示例:查找子串第一次出现的索引
String str = "Hello World Java";
int index1 = str.indexOf("World"); // 查找子串 "World"
int index2 = str.indexOf(""); // 空串返回 0
int index3 = str.indexOf("Python"); // 未找到返回 -1
System.out.println(index1); // 输出:6
System.out.println(index2); // 输出:0
System.out.println(index3); // 输出:-1
// 返回指定字符最后一次出现的索引,未找到返回 -1,与 indexOf 方向相反
// 示例:查找字符最后一次出现的索引
String str = "abacada";
int index = str.lastIndexOf('a'); // 查找最后一个 'a'
System.out.println(index); // 输出:6(字符串末尾的 'a' 索引为 6)
// 返回指定索引处的字符,索引范围为 0~length()-1,越界抛出 StringIndexOutOfBoundsException
// 示例:获取指定索引处的字符
String str = "Hello World";
char c1 = str.charAt(3); // 获取索引 3 的字符
char c2 = str.charAt(str.length()-1); // 获取最后一个字符
System.out.println(c1); // 输出:l
System.out.println(c2); // 输出:d
// char error = str.charAt(20); // 报错:StringIndexOutOfBoundsException
// 重写自 Object 类,严格比较字符串内容(区分大小写、字符顺序),仅当参数是 String 且内容完全一致时返回 true
// 示例:比较字符串内容(区分大小写)
String str1 = "Hello";
String str2 = "Hello";
String str3 = "hello";
String str4 = new String("Hello");
System.out.println(str1.equals(str2)); // 输出:true(内容一致)
System.out.println(str1.equals(str3)); // 输出:false(大小写不同)
System.out.println(str1.equals(str4)); // 输出:true(内容一致,引用不同不影响)
System.out.println(str1.equals(null)); // 输出:false(避免空指针)
// 忽略大小写比较字符串内容,仅比较字符本身,不区分大小写(不改变原字符串大小写)
// 示例:忽略大小写比较内容
String str1 = "Java";
String str2 = "java";
String str3 = "JAVA";
System.out.println(str1.equalsIgnoreCase(str2)); // 输出:true
System.out.println(str1.equalsIgnoreCase(str3)); // 输出:true
System.out.println(str1.equals(str2)); // 输出:false(对比 equals 方法)
// 判断字符串是否以指定前缀开头,前缀为空串时返回 true
// 示例:判断是否以指定前缀开头
String str = "Hello World Java";
boolean flag1 = str.startsWith("Hello"); // 以 "Hello" 开头
boolean flag2 = str.startsWith("World"); // 不以 "World" 开头
boolean flag3 = str.startsWith(""); // 空前缀返回 true
System.out.println(flag1); // 输出:true
System.out.println(flag2); // 输出:false
System.out.println(flag3); // 输出:true
// 判断字符串是否以指定后缀结尾,后缀为空串时返回 true
// 示例:判断是否以指定后缀结尾
String str = "Hello World Java";
boolean flag1 = str.endsWith("Java"); // 以 "Java" 结尾
boolean flag2 = str.endsWith("World"); // 不以 "World" 结尾
System.out.println(flag1); // 输出:true
System.out.println(flag2); // 输出:false
// 判断字符串是否包含指定字符序列(子串),子串为空串时返回 true,本质是通过 indexOf 实现(indexOf != -1 则返回 true)
// 示例:判断是否包含指定子串
String str = "Hello World Java";
boolean flag1 = str.contains("World"); // 包含 "World"
boolean flag2 = str.contains("Python"); // 不包含 "Python"
System.out.println(flag1); // 输出:true
System.out.println(flag2); // 输出:false
// 将字符串中所有大写字母转换为小写字母,非字母字符保持不变,返回新字符串(原字符串不变)
// 示例:转换为小写字符串
String str = "Hello World 123!";
String lowerStr = str.toLowerCase();
System.out.println(lowerStr); // 输出:hello world 123!
System.out.println(str); // 输出:Hello World 123!(原字符串不变)
// 将字符串中所有小写字母转换为大写字母,非字母字符保持不变,返回新字符串
// 示例:转换为大写字符串
String str = "Hello World 123!";
String upperStr = str.toUpperCase();
System.out.println(upperStr); // 输出:HELLO WORLD 123!
System.out.println(str); // 输出:Hello World 123!(原字符串不变)
// 将字符串转换为字符数组,数组长度与字符串长度一致,字符顺序完全相同
// 示例:字符串转字符数组
import java.util.Arrays;
String str = "Hello";
char[] charArr = str.toCharArray();
System.out.println(Arrays.toString(charArr)); // 输出:[H, e, l, l, o]
System.out.println(charArr[0]); // 输出:H(可通过数组索引访问字符)
// 静态方法,将字符数组转换为字符串,数组为 null 时返回 "null",而非抛出空指针
// 示例:字符数组转字符串
import java.util.Arrays;
char[] charArr1 = {'H', 'e', 'l', 'l', 'o'};
char[] charArr2 = null;
String str1 = String.valueOf(charArr1);
String str2 = String.valueOf(charArr2);
System.out.println(str1); // 输出:Hello
System.out.println(str2); // 输出:null(处理 null 安全)
基本类型转 String:可通过 + 号拼接或 String.valueOf() 方法(推荐 valueOf(),效率更高、处理 null 更安全)。
String 转基本类型:通过对应包装类的 parseXxx() 方法(如 Integer.parseInt()),转换失败抛 NumberFormatException。
// 示例 1:基本类型转字符串
int num = 123;
double d = 3.14;
boolean b = true;
// 方式 1:valueOf(推荐)
String strNum = String.valueOf(num);
String strD = String.valueOf(d);
String strB = String.valueOf(b);
// 方式 2:+ 号拼接
String strNum2 = num + "";
System.out.println(strNum); // 输出:123
System.out.println(strD); // 输出:3.14
System.out.println(strB); // 输出:true
System.out.println(strNum2); // 输出:123
// 示例 2:字符串转基本类型
String str1 = "456";
String str2 = "3.1415";
int num2 = Integer.parseInt(str1); // 字符串转 int
double d2 = Double.parseDouble(str2); // 字符串转 double
System.out.println(num2 + 100); // 输出:556
System.out.println(d2 + 0.8585); // 输出:4.0
// 转换失败示例(编译通过,运行报错)
// String strError = "abc";
// int numError = Integer.parseInt(strError); // 报错:NumberFormatException
按字典序比较,不区分大小写。
// 按字典序比较两个字符串,返回差值
public class StringTrimCompareDemo {
public static void main(String[] args) {
// 去除空格
String s1 = " Hello World \t\n";
System.out.println(s1.trim()); // Hello World(去除首尾空白)
// System.out.println(s1.strip()); // JDK11+ 可用,效果同上,支持 Unicode 空白
// 字典序比较
String s2 = "abc";
String s3 = "abd";
String s4 = "ABC";
System.out.println(s2.compareTo(s3)); // -1(s2 < s3,差值为 'c'-'d'=-1)
System.out.println(s3.compareTo(s2)); // 1(s3 > s2)
System.out.println(s2.compareToIgnoreCase(s4)); // 0(不区分大小写,内容相同)
}
}
开发中频繁修改字符串时,需选择 StringBuilder 或 StringBuffer,三者核心差异集中在可变性、线程安全、效率上,对比如下:
| 特性 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 可变性 | 不可变(修改创建新对象) | 可变(直接修改底层数组) | 可变(直接修改底层数组) |
| 线程安全 | 安全(不可变) | 不安全(无同步锁) | 安全(方法加 synchronized 锁) |
| 效率 | 最低(频繁修改产生大量临时对象) | 最高(无锁 overhead) | 中等(锁机制消耗性能) |
| 底层实现 | char[](JDK8)/ byte[](JDK9+) | char[](JDK8)/ byte[](JDK9+) | char[](JDK8)/ byte[](JDK9+) |
| 适用场景 | 字符串不频繁修改(如常量、少量拼接) | 单线程环境,频繁修改字符串(如循环拼接) | 多线程环境,频繁修改字符串(如多线程日志拼接) |
public class StringPerformanceDemo {
public static void main(String[] args) {
int loop = 100000; // 循环次数
// String 拼接(效率极低)
long start1 = System.currentTimeMillis();
String s = "";
for (int i = 0; i < loop; i++) {
s += i;
}
long end1 = System.currentTimeMillis();
System.out.println("String 耗时:" + (end1 - start1) + "ms");
// StringBuilder 拼接(效率最高)
long start2 = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < loop; i++) {
sb.append(i);
}
String result2 = sb.toString();
long end2 = System.currentTimeMillis();
System.out.println("StringBuilder 耗时:" + (end2 - start2) + "ms");
// StringBuffer 拼接(效率中等)
long start3 = System.currentTimeMillis();
StringBuffer sbf = new StringBuffer();
for (int i = 0; i < loop; i++) {
sbf.append(i);
}
String result3 = sbf.toString();
long end3 = System.currentTimeMillis();
System.out.println("StringBuffer 耗时:" + (end3 - start3) + "ms");
}
}
运行结果参考:String 耗时 5000+ms,StringBuilder 耗时 1-2ms,StringBuffer 耗时 5-10ms,差异显著。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online