在Java编程中,String 类是使用最频繁的类之一,也是面试中的常客。深刻理解其设计原理和内存机制,是编写高效、健壮代码的基础。本文将从底层特性出发,系统解析String的不可变性、实例化方式、内存分配及常用方法。
String类概述与不可变性
String 类被声明为 final,意味着它不可被继承。这种设计是保证其不可变性的基石。不可变性是指一个 String 对象一旦被创建,其内容(即内部的字符序列 value)就不可被改变。
其内部使用 final char[] value(在Java 9及之后,为 byte[])来存储字符串数据,这也从实现层面确保了数据的不可变。这种设计带来了诸多好处,如线程安全、作为哈希表键值的可靠性(哈希值不变)以及字符串常量池的实现基础。
不可变性的具体体现:
- 重新赋值时,并非修改原值,而是将引用指向新创建的对象。
- 字符串连接操作时(如
+或concat),同样会生成新的对象。
- 调用
replace() 等方法修改字符时,也会返回一个新的字符串对象。
@Test
public void testImmutability(){
String s1 = "abc"; // 字面量定义,值在常量池
String s2 = "abc";
s1 = "hello"; // s1引用指向新的常量池地址
System.out.println(s1 == s2); // false,比较地址
System.out.println(s1); // hello
System.out.println(s2); // abc
String s3 = "abc";
s3 += "def"; // 连接操作,生成新对象“abcdef”,s3指向它
System.out.println(s3); // abcdef
System.out.println(s2); // abc
String s4 = "abc";
String s5 = s4.replace('a', 'm'); // 替换操作,生成新对象“mbc”
System.out.println(s4); // abc
System.out.println(s5); // mbc
}

两种实例化方式与内存分配
理解 String 对象在内存中的创建方式是掌握其本质的关键。
-
字面量定义:如 String s1 = "abc";
这种方式会首先检查字符串常量池中是否存在内容为 "abc" 的字符串。如果存在,则直接返回该字符串的引用;如果不存在,则在常量池中创建该字符串,然后返回引用。因此,相同字面量的变量指向同一内存地址。
-
new + 构造器:如 String s2 = new String("abc");
这种方式会先在堆空间中创建一个新的 String 对象。构造器中的参数字符串 "abc" 本身会遵循常量池的规则(如果常量池没有,则先创建)。因此,s2 指向的是堆中新对象的地址,而非常量池中的地址。
经典面试题:String s = new String("abc"); 创建了几个对象?
答案:1个或2个。
- 如果字符串常量池中已存在
"abc",则只在堆中创建1个新对象。
- 如果常量池中不存在
"abc",则会先在常量池创建该字符串对象(第1个),然后在堆中创建新的 String 对象(第2个)。
@Test
public void testInstantiation(){
// 字面量定义:s1和s2指向常量池中的同一个“javaEE”
String s1 = "javaEE";
String s2 = "javaEE";
// new+构造器:s3和s4分别指向堆中两个不同的对象,但这两个对象内部的value都指向常量池的“javaEE”
String s3 = new String("javaEE");
String s4 = new String("javaEE");
System.out.println(s1 == s2); // true,地址相同
System.out.println(s1 == s3); // false,一个在池,一个在堆
System.out.println(s1 == s4); // false
System.out.println(s3 == s4); // false,堆中两个不同对象
}

字符串拼接的底层规则
字符串的 + 连接操作在编译期和运行期有不同的处理逻辑,是另一个考察重点。
结论:
- 常量与常量的拼接结果在编译期就会被确定,并直接放入字符串常量池。
- 只要拼接的操作数中有一个是变量(非常量),结果就会在堆中(
new)创建。
- 可以调用
intern() 方法,主动将堆中字符串对象的引用放入常量池(如果池中已有则直接返回池中引用),并返回该常量池引用。
@Test
public void testConcatenation(){
String s1 = "javaEEhadoop";
final String s4 = "javaEE"; // s4被final修饰,是编译期常量
String s5 = s4 + "hadoop"; // 常量+常量,结果在常量池
System.out.println(s1 == s5); // true
String s2 = "javaEE";
String s3 = s2 + "hadoop"; // 变量(s2) + 常量,结果在堆中
System.out.println(s1 == s3); // false
// intern()方法示例
String s6 = s3.intern(); // 将s3对应的字符串尝试放入常量池,并返回池中引用
System.out.println(s1 == s6); // true
}

一道关于值传递的面试题
这道题综合考察了字符串的不可变性和Java的参数传递机制(值传递)。
public class StringTest {
String str = new String("good");
char[] ch = { 't', 'e', 's', 't' };
public void change(String str, char ch[]) {
str = "test ok"; // 修改的是形参str的指向,不影响成员变量str
ch[0] = 'b'; // 直接修改了传入的数组对象的内容
}
public static void main(String[] args) {
StringTest ex = new StringTest();
ex.change(ex.str, ex.ch);
System.out.println(ex.str); // 输出 "good"
System.out.println(ex.ch); // 输出 "best"
}
}

String类的常用方法
String 类提供了丰富的方法,熟练使用它们是进行日常Java字符串处理的基础。
常用方法示例:
public class StringMethodTest {
// 1. 获取与判断
@Test
public void testBasicMethods(){
String s1 = "HelloWorld";
System.out.println(s1.length()); // 10
System.out.println(s1.charAt(4)); // 'o'
System.out.println(s1.isEmpty()); // false
String s2 = s1.toLowerCase();
System.out.println(s1); // HelloWorld (不变)
System.out.println(s2); // helloworld
String s3 = " he llo world ";
System.out.println("---" + s3.trim() + "---"); // ---he llo world---
}
// 2. 比较、截取与连接
@Test
public void testCompareAndSubstring(){
String s1 = "HelloWorld";
String s2 = "helloworld";
System.out.println(s1.equals(s2)); // false
System.out.println(s1.equalsIgnoreCase(s2)); // true
String s3 = "北京尚硅谷教育";
String s4 = s3.substring(2); // "尚硅谷教育"
String s5 = s3.substring(2, 5); // "尚硅谷" (包左不包右)
System.out.println(s4);
System.out.println(s5);
}
// 3. 查找与匹配
@Test
public void testSearch(){
String str1 = "hellowworld";
System.out.println(str1.contains("wor")); // true
System.out.println(str1.indexOf("lo")); // 3
System.out.println(str1.lastIndexOf("or")); // 7
System.out.println(str1.startsWith("He")); // false
System.out.println(str1.endsWith("rld")); // true
}
// 4. 替换、分割与匹配
@Test
public void testReplaceAndSplit(){
String str1 = "北京尚硅谷教育北京";
System.out.println(str1.replace('北', '东')); // 东京尚硅谷教育东京
System.out.println(str1.replace("北京", "上海")); // 上海尚硅谷教育上海
String str2 = "12hello34world5java7891mysql456";
// 将数字替换为逗号,并去掉首尾多余的逗号
String result = str2.replaceAll("\\d+", ",").replaceAll("^,|,$", "");
System.out.println(result); // hello,world,java,mysql
String str3 = "hello|world|java";
String[] strs = str3.split("\\|");
for (String s : strs) {
System.out.print(s + " "); // hello world java
}
}
}

String与其他结构的转换
1. 与基本类型/包装类互转
@Test
public void testParse(){
String str1 = "123";
int num = Integer.parseInt(str1); // String -> int
String str2 = String.valueOf(num); // int -> String (推荐)
String str3 = num + ""; // 会在堆中生成新对象
System.out.println(str1 == str3); // false
}
2. 与字符数组互转
@Test
public void testCharArray(){
String str1 = "abc123";
char[] charArray = str1.toCharArray(); // String -> char[]
for(char c : charArray){
System.out.println(c);
}
char[] arr = {'h','e','l','l','o'};
String str2 = new String(arr); // char[] -> String
System.out.println(str2);
}
3. 与字节数组互转(涉及编码)
编码解码是网络传输和文件处理中的常见操作,理解网络与系统基础中的字符集概念非常重要。
@Test
public void testByteArray() throws UnsupportedEncodingException {
String str1 = "abc123中国";
// 编码(String -> byte[]),使用默认字符集(UTF-8)
byte[] bytes = str1.getBytes();
System.out.println(Arrays.toString(bytes));
// 使用指定字符集(GBK)编码
byte[] gbks = str1.getBytes("gbk");
// 解码(byte[] -> String),必须使用与编码一致的字符集
String str2 = new String(bytes); // 默认UTF-8解码,正常
String str3 = new String(gbks, "gbk"); // 使用GBK解码,正常
String str4 = new String(gbks); // 使用UTF-8解码GBK编码的字节,乱码
System.out.println(str2);
System.out.println(str3);
System.out.println(str4); // 输出乱码
}
