一、String 类型的核心本质
String 是 Java 中最常用的引用数据类型,用于表示字符串常量,其核心特性需重点掌握:
- 不可变性(Immutable):String 对象一旦创建,其底层字符数组(char [],JDK9 后改为 byte [])的内容无法修改。所有看似修改的操作(如substring、replace)都会创建新的 String 对象,原对象保持不变。
String str = "abc";
str = str + "d"; // 原"abc"对象未改变,创建新对象"abcd"并让str指向它
- 常量池优化:Java 为 String 提供字符串常量池(String Pool),位于方法区(JDK8 后是元空间)。当使用字面量创建 String(如String s = "abc")时,JVM 会先检查常量池是否存在该字符串,若存在则直接引用,否则创建新对象存入常量池,避免重复创建相同字符串,节省内存。
- 实现接口:String 实现了Serializable(支持序列化)、Comparable)、CharSequence(字符序列接口,提供charAt、length` 等方法)三个核心接口,这也是其具备诸多实用功能的基础。
二、String 的创建方式与内存差异
String 有两种常见创建方式,其内存分配和引用逻辑截然不同,是面试高频考点:
1. 字面量创建(String s = "abc")
- 执行流程:JVM 先查询字符串常量池,若存在 "abc" 则直接将引用赋值给 s;若不存在,在常量池创建 "abc" 对象,再将引用赋值给 s。
- 内存位置:对象存储在常量池,s 指向常量池中的对象。
2. 构造方法创建(String s = new String("abc"))
- 执行流程:无论常量池是否存在 "abc",都会先在堆内存中创建一个 String 对象,再检查常量池:若常量池无 "abc",则在常量池也创建一个 "abc" 对象;最终 s 指向堆内存中的对象。
- 代码验证:
String s1 = "abc";
String s2 = new String("abc");
System.out.println(s1 == s2); // false(==比较引用地址,s1指向常量池,s2指向堆)
System.out.println(s1.equals(s2)); // true(equals比较字符串内容)
3. intern () 方法的作用
intern()方法用于手动将 String 对象存入常量池(JDK7 后优化,常量池移至堆中):
- 若常量池已存在该字符串,返回常量池中的引用;
- 若不存在,将当前对象的引用存入常量池(而非创建新对象),返回该引用。
- 代码示例:
String s3 = new String("xyz").intern();
String s4 = "xyz";
System.out.println(s3 == s4); // true(s3通过intern()指向常量池,s4直接指向常量池)
三、String 的常用方法(实战必备)
String 类提供了大量实用方法,以下是开发中高频使用的核心方法,结合场景说明:
|
方法签名 |
功能描述 |
示例 |
|
int length() |
获取字符串长度 |
"abc".length() → 3 |
|
char charAt(int index) |
获取指定索引的字符(索引从 0 开始) |
"abc".charAt(1) → 'b' |
|
boolean equals(Object obj) |
比较字符串内容(区分大小写) |
"Abc".equals("abc") → false |
|
boolean equalsIgnoreCase(String str) |
忽略大小写比较内容 |
"Abc".equalsIgnoreCase("abc") → true |
|
String concat(String str) |
拼接字符串(等价于+,但效率更高) |
"a".concat("b").concat("c") → "abc" |
|
String substring(int beginIndex) |
从指定索引截取子串(含 beginIndex) |
"abcd".substring(1) → "bcd" |
|
String substring(int beginIndex, int endIndex) |
截取 [beginIndex, endIndex) 区间的子串 |
"abcd".substring(1,3) → "bc" |
|
boolean contains(CharSequence s) |
判断是否包含指定字符序列 |
"abcde".contains("bc") → true |
|
boolean startsWith(String prefix) |
判断是否以指定前缀开头 |
"http://www".startsWith("http") → true |
|
boolean endsWith(String suffix) |
判断是否以指定后缀结尾 |
"test.txt".endsWith(".txt") → true |
|
String replace(char oldChar, char newChar) |
替换所有指定字符 |
"abac".replace('a','x') → "xbxc" |
|
String replace(CharSequence target, CharSequence replacement) |
替换指定字符序列 |
"a-b-c".replace("-","_") → "a_b_c" |
|
String trim() |
去除首尾空白字符(空格、制表符等) |
" abc ".trim() → "abc"(JDK11 后可用strip(),支持更多空白字符) |
|
String[] split(String regex) |
按指定正则表达式分割字符串 |
"a,b,c".split(",") → ["a","b","c"] |
|
static String valueOf(Object obj) |
将任意类型转为字符串(静态方法) |
String.valueOf(123) → "123" |
|
int compareTo(String str) |
按字典序比较(返回差值,0 表示相等) |
"a".compareTo("b") → -1,"c".compareTo("a") → 2 |
关键注意点:
- split(regex)中若分割符是正则特殊字符(如.、|),需转义(如split("\\."));
- substring在 JDK6 中会创建新字符数组,JDK7 后直接引用原数组的指定区间,避免内存浪费;
- 字符串拼接优先使用StringBuilder/StringBuffer(尤其是循环拼接),效率远高于+和concat(+会频繁创建新 String 对象)。
四、String、StringBuilder、StringBuffer 的区别(面试必问)
开发中字符串拼接场景频繁,三者的选择直接影响性能,核心区别如下:
|
特性 |
String |
StringBuilder |
StringBuffer |
|
可变性 |
不可变(修改创建新对象) |
可变(直接操作底层数组) |
可变(直接操作底层数组) |
|
线程安全 |
线程安全(无修改操作) |
线程不安全(效率高) |
线程安全(方法加synchronized,效率低) |
|
性能 |
最低(频繁修改时创建大量对象) |
最高(单线程场景首选) |
中等(多线程场景使用) |
|
适用场景 |
字符串不频繁修改(如常量定义) |
单线程下字符串频繁拼接、修改 |
多线程下字符串频繁拼接、修改 |
性能测试示例(循环拼接 10000 次):
long start = System.currentTimeMillis();
// String拼接(最慢)
String s = "";
for (int i = 0; i 10000; i++) {
s += i;
}
System.out.println("String耗时:" + (System.currentTimeMillis() - start) + "ms");
// StringBuilder拼接(最快)
start = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i 0000; i++) {
sb.append(i);
}
System.out.println("StringBuilder耗时:" + (System.currentTimeMillis() - start) + "ms");
// StringBuffer拼接(中等)
start = System.currentTimeMillis();
StringBuffer sbf = new StringBuffer();
for (int i = 0; i 0; i++) {
sbf.append(i);
}
System.out.println("StringBuffer耗时:" + (System.currentTimeMillis() - start) + "ms");
输出结果(参考):String 耗时约 500ms,StringBuilder 耗时约 1ms,StringBuffer 耗时约 3ms,差异显著。
五、String 的常见误区与避坑指南
- 误区 1:认为String s = null和String s = ""等价
-
- 区别:s = null表示 s 未指向任何对象(引用为空);s = ""表示 s 指向一个长度为 0 的字符串对象(引用非空)。
-
- 避坑:调用null的 String 方法会抛出NullPointerException,需先判空:
// 错误写法(若s为null会报错)
if (s.equals("abc")) { ... }
// 正确写法(先判非空,或用常量在前)
if ("abc".equals(s)) { ... } // 即使s为null,也不会报错(返回false)
- 误区 2:循环中使用+拼接字符串
-
- 问题:每次+都会创建新的 String 对象,循环 10000 次会创建大量对象,导致内存浪费和性能下降。
-
- 解决:使用StringBuilder(单线程)或StringBuffer(多线程):
- 误区 3:String.intern()一定能节省内存
-
- 注意:JDK7 后intern()会将对象引用存入常量池,但若字符串是动态生成的(如用户输入),且重复率低,使用intern()反而会占用常量池内存,适得其反。
-
- 适用场景:字符串重复率高(如配置项、固定枚举值)时,使用intern()可复用对象,节省内存。
- 误区 4:忽略 String 的空字符串判断
-
- 判空推荐写法(兼顾null和空字符串):
// 方法1:使用StringUtils(Apache Commons Lang工具类,推荐)
if (StringUtils.isNotBlank(s)) { ... } // 排除null、""、" "等情况
// 方法2:原生写法
if (s != null && !s.trim().isEmpty()) { ... }
六、总结
String 作为 Java 中最核心的类型之一,其不可变性和常量池优化是理解的基础,而创建方式、内存分配、常用方法及与 StringBuilder/StringBuffer 的区别,是开发和面试中的重点。掌握以下核心要点,即可应对大部分场景:
- 字面量创建走常量池,new创建走堆,intern()可手动入池;
- 不可变性导致修改操作创建新对象,频繁修改优先用StringBuilder;
- 线程安全用StringBuffer,单线程用StringBuilder,字符串常量用String;
- 判空、拼接、截取时注意避坑,优先使用工具类和高效方法。
如果需要进一步深入 String 的底层源码(如hashCode实现、replace方法原理)或更多实战场景案例,可以留言补充说明!
&spm=1001.2101.3001.5002&articleId=156097918&d=1&t=3&u=29a8e2855efb4ac5b8a06ff0d7bbf581)
3909

被折叠的 条评论
为什么被折叠?



