跳到主要内容Java String 字符串核心特性与 API 详解 | 极客日志Javajava
Java String 字符串核心特性与 API 详解
综述由AI生成Java String 的核心特性,包括底层实现差异(JDK 8 vs 9+)、不可变性原理及常量池机制。涵盖常用 API 如拼接、替换、截取、查找、转换等,并提供代码示例。对比了 String、StringBuilder 和 StringBuffer 在可变性、线程安全及效率上的区别,最后总结了常见面试题,帮助开发者深入理解字符串处理。
锁机制28 浏览 一、String 基础定义与底层实现
1.1 核心定义
String 是 Java 中用于表示不可变字符序列的引用类型,位于 java.lang 包下。JVM 会自动加载该包,无需手动导入。它并非 8 种基本数据类型(byte、short、int、long、float、double、char、boolean),但经 JVM 特殊优化,拥有类似基本类型的使用体验,比如可直接赋值。
1.2 底层实现差异(JDK 8 vs JDK 9+)
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 字符(如英文、数字、符号)的场景(绝大多数业务场景) |
示例验证:
- 存储字符串 'abc'(全 ASCII):JDK8 中 char[] 占 3×2=6 字节,JDK9 中 byte[] 占 3×1=3 字节,内存直接减半。
- 存储 '中文 123'(2 个中文+3 个 ASCII):JDK8 占 5×2=10 字节,JDK9 中中文用 UTF-16(2 字节/个)、ASCII 用 1 字节,总占 2×2 + 3×1=7 字节,仍节省内存。
二、String 不可变性(核心特性)
2.1 不可变性定义
String 对象一旦创建,其内部的字符序列(value 数组)就无法被修改。所谓的'修改'操作(如拼接、替换),本质上都是创建新的 String 对象,原对象保持不变。
2.2 不可变性实现原理(JDK 8)
底层通过三大特性保障不可变性,缺一不可:
- private 修饰 value 数组:禁止外部直接访问 value,避免外部修改数组内容。
- final 修饰 value 数组:禁止 value 数组引用重新指向新的数组(但数组内部元素仍可通过反射修改,不推荐)。
- 无 set 方法:String 类未提供修改 value 数组元素的方法,所有操作均返回新对象。
2.3 不可变性的影响(优势 + 局限)
优势:
- 线程安全:不可变对象天生线程安全,无需加锁,可在多线程环境下安全共享。
- 支持字符串常量池:相同内容的字符串可复用常量池中的对象,节省内存(核心优化)。
- 哈希值稳定:String 的 hashCode 基于 value 计算,一旦创建哈希值不变,适合作为 HashMap 等集合的键。
局限:
频繁修改字符串(如循环拼接)会创建大量临时对象,占用内存并增加 GC 压力,此时推荐使用 StringBuilder(非线程安全)或 StringBuffer(线程安全,效率较低)。
2.4 代码示例:不可变性验证
public class StringImmutabilityDemo {
public static void main(String[] args) {
;
s1;
s1 = s1 + ;
System.out.println(s1);
System.out.println(s2);
{
String.class.getDeclaredField();
valueField.setAccessible();
[] value = ([]) valueField.get(s2);
value[] = ;
System.out.println(s2);
} (Exception e) {
e.printStackTrace();
}
}
}
String
s1
=
"abc"
String
s2
=
"d"
try
Field
valueField
=
"value"
true
char
char
0
'x'
catch
三、字符串常量池(String Pool)
3.1 常量池定义与作用
字符串常量池是 JVM 为 String 专门设计的内存区域(属于方法区),用于存储字符串常量,核心作用是复用对象、减少内存占用。当创建字符串时,JVM 会先检查常量池:若存在相同内容的字符串,直接返回其引用;若不存在,创建新字符串存入常量池并返回引用。
3.2 字符串创建的两种方式对比
String 对象有两种创建方式,其在内存中的存储位置、是否复用常量池对象均不同,是面试高频考点。
方式 1:字面量赋值(String s = 'abc')
- JVM 检查字符串常量池,是否存在 'abc'。
- 存在:直接将常量池中 'abc' 的引用赋值给 s。
- 不存在:在常量池创建 'abc' 对象,再将引用赋值给 s。
方式 2:new 关键字创建(String s = new String('abc')
- 在堆内存中创建一个新的 String 对象。
- 检查字符串常量池,是否存在 'abc'。
- 不存在:在常量池创建 'abc' 对象,再让堆对象的 value 引用指向常量池对象。
- 存在:直接让堆对象的 value 引用指向常量池对象。
- 将堆对象的引用赋值给 s。
结论:new String('abc') 至少创建 1 个对象(堆对象),最多创建 2 个对象(堆对象 + 常量池对象,若常量池无对应值)。
3.3 代码示例:两种创建方式对比
public class StringPoolDemo {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
String s3 = new String("abc");
String s4 = new String("abc");
System.out.println(s3 == s4);
System.out.println(s3.equals(s4));
System.out.println(s1 == s3);
}
}
3.4 常量池相关方法:intern()
intern() 方法是 String 类提供的手动关联常量池的方法,作用是:将当前字符串对象的内容存入常量池(若不存在),并返回常量池中该对象的引用。
代码示例:intern() 使用
public class StringInternDemo {
public static void main(String[] args) {
String s1 = new String("abc");
String s2 = s1.intern();
String s3 = "abc";
System.out.println(s1 == s2);
System.out.println(s2 == s3);
String s4 = new String("xy") + new String("z");
String s5 = s4.intern();
String s6 = "xyz";
System.out.println(s4 == s5);
System.out.println(s5 == s6);
}
}
注意:JDK 7 对 intern() 做了优化,若常量池不存在该字符串,会直接存储堆对象的引用(而非复制内容),进一步节省内存。
四、String 核心 API 详解(附示例)
String 类提供了大量操作字符串的 API,按功能分类整理,结合示例说明核心用法,覆盖开发高频场景。
4.1 字符串长度与空判断
int length()
String str = "Java String";
System.out.println(str.length());
boolean isEmpty()
String emptyStr = "";
String nonEmptyStr = "test";
String nullStr = null;
System.out.println(emptyStr.isEmpty());
System.out.println(nonEmptyStr.isEmpty());
System.out.println(nullStr != null && !nullStr.isEmpty());
4.2 字符串拼接与替换
4.2.1 拼接方法
String concat(String str)
String str1 = "Hello";
String str2 = "World";
String result = str1.concat(" ").concat(str2);
System.out.println(result);
System.out.println(str1);
+ 号拼接
String str = "年龄:";
int age = 25;
String result1 = str + age;
String result2 = "姓名:" + "张三" + "," + result1;
String result3 = "null 拼接:" + null;
System.out.println(result1);
System.out.println(result2);
System.out.println(result3);
4.2.2 替换方法
String replace(char oldChar, char newChar)
String str = "abacada";
String result = str.replace('a', 'x');
String noChange = str.replace('z', 'y');
System.out.println(result);
System.out.println(noChange);
String replace(CharSequence target, CharSequence replacement)
替换字符串中所有指定的子串为目标子串,支持 String、StringBuilder 等 CharSequence 类型。
String str = "Hello World, World is beautiful";
String result = str.replace("World", "Java");
System.out.println(result);
String replaceAll(String regex, String replacement)
按正则表达式匹配内容,替换所有符合条件的子串,正则特殊字符需转义。
String str = "abc123def456 789";
String noNum = str.replaceAll("\\d", "");
String noSpace = str.replaceAll("\\s", "-");
System.out.println(noNum);
System.out.println(noSpace);
String replaceFirst(String regex, String replacement)
String str = "abacada";
String result = str.replaceFirst("a", "x");
System.out.println(result);
4.3 字符串截取与分割
4.3.1 截取方法
String substring(int beginIndex)
String str = "abcdefgh";
String result1 = str.substring(3);
String result2 = str.substring(0);
System.out.println(result1);
System.out.println(result2);
String substring(int beginIndex, int endIndex)
遵循'左闭右开'原则,截取从 beginIndex(包含)到 endIndex(不包含)的子串,beginIndex 不能大于 endIndex。
String str = "abcdefgh";
String result = str.substring(2, 6);
System.out.println(result);
4.3.2 分割方法
String[] split(String regex)
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));
System.out.println(Arrays.toString(arr2));
System.out.println(Arrays.toString(arr3));
String[] split(String regex, int limit)
import java.util.Arrays;
String str = "a,b,c,d,e";
String[] arr1 = str.split(",", 3);
String[] arr2 = str.split(",", 0);
String[] arr3 = str.split(",", -2);
System.out.println(Arrays.toString(arr1));
System.out.println(Arrays.toString(arr2));
System.out.println(Arrays.toString(arr3));
4.4 字符串查找与判断
4.4.1 查找方法
int indexOf(int ch)
String str = "Hello World";
int index1 = str.indexOf('o');
int index2 = str.indexOf(111);
int index3 = str.indexOf('z');
System.out.println(index1);
System.out.println(index2);
System.out.println(index3);
int indexOf(int ch, int fromIndex)
String str = "Hello World";
int index = str.indexOf('o', 5);
System.out.println(index);
int indexOf(String str)
String str = "Hello World Java";
int index1 = str.indexOf("World");
int index2 = str.indexOf("");
int index3 = str.indexOf("Python");
System.out.println(index1);
System.out.println(index2);
System.out.println(index3);
int lastIndexOf(int ch)
String str = "abacada";
int index = str.lastIndexOf('a');
System.out.println(index);
char charAt(int index)
String str = "Hello World";
char c1 = str.charAt(3);
char c2 = str.charAt(str.length()-1);
System.out.println(c1);
System.out.println(c2);
4.4.2 判断方法
boolean equals(Object anObject)
String str1 = "Hello";
String str2 = "Hello";
String str3 = "hello";
String str4 = new String("Hello");
System.out.println(str1.equals(str2));
System.out.println(str1.equals(str3));
System.out.println(str1.equals(str4));
System.out.println(str1.equals(null));
boolean equalsIgnoreCase(String anotherString)
String str1 = "Java";
String str2 = "java";
String str3 = "JAVA";
System.out.println(str1.equalsIgnoreCase(str2));
System.out.println(str1.equalsIgnoreCase(str3));
System.out.println(str1.equals(str2));
boolean startsWith(String prefix)
String str = "Hello World Java";
boolean flag1 = str.startsWith("Hello");
boolean flag2 = str.startsWith("World");
boolean flag3 = str.startsWith("");
System.out.println(flag1);
System.out.println(flag2);
System.out.println(flag3);
boolean endsWith(String suffix)
String str = "Hello World Java";
boolean flag1 = str.endsWith("Java");
boolean flag2 = str.endsWith("World");
System.out.println(flag1);
System.out.println(flag2);
boolean contains(CharSequence s)
String str = "Hello World Java";
boolean flag1 = str.contains("World");
boolean flag2 = str.contains("Python");
System.out.println(flag1);
System.out.println(flag2);
4.5 字符串转换(大小写、数组、基本类型)
4.5.1 大小写转换
String toLowerCase()
String str = "Hello World 123!";
String lowerStr = str.toLowerCase();
System.out.println(lowerStr);
System.out.println(str);
String toUpperCase()
String str = "Hello World 123!";
String upperStr = str.toUpperCase();
System.out.println(upperStr);
System.out.println(str);
4.5.2 字符串与数组转换
char[] toCharArray()
import java.util.Arrays;
String str = "Hello";
char[] charArr = str.toCharArray();
System.out.println(Arrays.toString(charArr));
System.out.println(charArr[0]);
static String valueOf(char[] data)
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);
System.out.println(str2);
4.5.3 字符串与基本类型转换
基本类型转 String:可通过 + 号拼接或 String.valueOf() 方法(推荐 valueOf(),效率更高、处理 null 更安全)。
String 转基本类型:通过对应包装类的 parseXxx() 方法(如 Integer.parseInt()),转换失败抛 NumberFormatException。
int num = 123;
double d = 3.14;
boolean b = true;
String strNum = String.valueOf(num);
String strD = String.valueOf(d);
String strB = String.valueOf(b);
String strNum2 = num + "";
System.out.println(strNum);
System.out.println(strD);
System.out.println(strB);
System.out.println(strNum2);
String str1 = "456";
String str2 = "3.1415";
int num2 = Integer.parseInt(str1);
double d2 = Double.parseDouble(str2);
System.out.println(num2 + 100);
System.out.println(d2 + 0.8585);
4.6 去除空格与比较
4.6.1 去除空格方法
- String trim():去除字符串首尾的空白字符(空格、制表符 \t、换行符 \n 等),中间空格保留。
- String strip():JDK 11+ 新增,功能与 trim() 类似,但支持 Unicode 空白字符(推荐使用)。
4.6.2 字符串比较(按字典序)
- 返回 0:内容相同。
- 返回正数:当前字符串大于目标字符串。
- 返回负数:当前字符串小于目标字符串。
int compareToIgnoreCase(String str)
int compareTo(String anotherString)
public class StringTrimCompareDemo {
public static void main(String[] args) {
String s1 = " Hello World \t\n";
System.out.println(s1.trim());
String s2 = "abc";
String s3 = "abd";
String s4 = "ABC";
System.out.println(s2.compareTo(s3));
System.out.println(s3.compareTo(s2));
System.out.println(s2.compareToIgnoreCase(s4));
}
}
五、String、StringBuilder、StringBuffer 对比
开发中频繁修改字符串时,需选择 StringBuilder 或 StringBuffer,三者核心差异集中在可变性、线程安全、效率上,对比如下:
| 特性 | String | StringBuilder | StringBuffer |
|---|
| 可变性 | 不可变(修改创建新对象) | 可变(直接修改底层数组) | 可变(直接修改底层数组) |
| 线程安全 | 安全(不可变) | 不安全(无同步锁) | 安全(方法加 synchronized 锁) |
| 效率 | 最低(频繁修改产生大量临时对象) | 最高(无锁 overhead) | 中等(锁机制消耗性能) |
| 底层实现 | char[](JDK8)/ byte[](JDK9+) | char[](JDK8)/ byte[](JDK9+) | char[](JDK8)/ byte[](JDK9+) |
| 适用场景 | 字符串不频繁修改(如常量、少量拼接) | 单线程环境,频繁修改字符串(如循环拼接) | 多线程环境,频繁修改字符串(如多线程日志拼接) |
5.1 代码示例:效率对比(循环拼接)
public class StringPerformanceDemo {
public static void main(String[] args) {
int loop = 100000;
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");
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");
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,差异显著。
六、常见面试题汇总
- String 为什么是不可变的?
答:底层通过 private final char[] value(JDK8)+ 无修改方法实现,不可变性保障线程安全、支持常量池、哈希值稳定。
- String s = new String('abc') 创建了几个对象?
答:至少 1 个(堆对象),最多 2 个(堆对象 + 常量池对象,若常量池无 'abc')。
- String、StringBuilder、StringBuffer 的区别?
答:核心差异在可变性、线程安全、效率(详见第五章对比表),单线程优先用 StringBuilder,多线程用 StringBuffer,不频繁修改用 String。
- intern() 方法的作用?JDK7 对其做了什么优化?
答:将字符串内容存入常量池并返回常量池引用;JDK7 优化为:常量池不存在时,直接存储堆对象引用(而非复制内容),节省内存。
- JDK8 与 JDK9+ 中 String 底层实现的差异?
答:JDK8 用 char[](2 字节/字符),JDK9+ 用 byte[] + coder(ASCII 占 1 字节,UTF-16 占 2 字节),目的是节省内存。
- String 的 equals() 与 == 的区别?
答:equals() 比较字符串内容(String 重写后),== 比较对象引用地址;基本类型用 == 比较值,引用类型默认 == 比较引用(除 String 常量池复用场景)。
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online