在 Java 的世界里,String 类就像一位无处不在的 "老朋友"—— 从简单的 Hello World 到复杂的业务逻辑,几乎每个程序都离不开它。但你真的了解这位 "老朋友" 吗?为什么同样是字符串,直接赋值和 new 出来的对象会有区别?为什么频繁拼接字符串时推荐用 StringBuilder?今天,我们就来揭开 String 类的神秘面纱,用生动的例子带你吃透它的方方面面。
一、为什么需要 String 类?
在 C 语言中,字符串是用字符数组或指针表示的,比如char[] str = "hello"
。这种方式把数据(字符数组)和操作(字符串函数)分离开来,每次操作都要调用strlen
、strcpy
等函数,不仅麻烦,还容易出现内存越界等问题。
Java 作为面向对象语言,将字符串的数据和操作封装成了 String 类,让字符串处理变得简单、安全。比如判断两个字符串是否相等,C 语言需要strcmp
,而 Java 直接调用equals
方法即可 —— 这就是面向对象的一点好处。
二、创建字符串:不止一种方式
String 类提供了多种构造方式
构造方法多种多样,常用的有以下几种,让我们用代码来直观表示:
public class StringDemo {
public static void main(String[] args) {
// 1. 直接用字符串常量赋值(最常用)
String s1 = "hello bit";
System.out.println(s1); // 输出:hello bit
// 2. new String对象
String s2 = new String("hello bit");
System.out.println(s2); // 输出:hello bit
// 3. 用字符数组构造
char[] chars = {'h','e','l','l','o',' ','b','i','t'};
String s3 = new String(chars);
System.out.println(s3); // 输出:hello bit
// 4. 用字节数组构造(字节对应ASCII码)
byte[] bytes = {97, 98, 99}; // 'a','b','c'的ASCII码
String s4 = new String(bytes);
System.out.println(s4); // 输出:abc
}
}
看起来这几种方式都能得到 "hello bit",但它们在内存中的存储方式大不相同 —— 这就要说到字符串常量池了。
三、字符串常量池:Java 的 "字符串仓库"
你有没有想过:如果程序中多次用到 "hello",Java 会存储多个相同的字符串吗?答案是否定的,因为有字符串常量池(StringTable) 这个 "仓库"。
那么什么是常量池呢?常量池是 JVM 中的一块特殊内存,本质是一个哈希表,用来存储字符串常量。它的核心作用是避免重复存储相同的字符串,提高内存利用率。 可以把它比作家里的 "冰箱":第一次买牛奶(字符串)会放进冰箱(常量池),下次再喝时直接从冰箱拿,不用再买新的 —— 这就是 "池化技术" 的思想(类似线程池、数据库连接池)。
不同 JDK 版本的常量池位置:
直接赋值 vs new 对象:内存差异
看两个例子,你就能明白常量池的作用:
1.直接用常量赋值
String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2); // 输出:true
在栈中str1和str2都指向常量池中的“abc”,常量池中存储着“abc”地址为(0x112233)
str1->0x112233, str2->0x112233,故结果返回true。s1 创建时,"abc" 被放入常量池;s2 创建时,JVM 先检查常量池,发现已有 "abc",直接让 s2 指向它 —— 所以 s1 和 s2 指向同一个对象,==
判断为 true。
2.用new创建对象
String s1 = new String("abc");
String s2 = new String("abc");
System.out.println(s1 == s2); // 输出:false
在栈中str1指向堆中对象A(地址0x998877),str2指向堆中对象B(地址0x556677),对象A和B的value属性都指向常量池中的"abc"(0x112233),在常量池存储"abc"(0x112233)
new
关键字会强制在堆中创建新对象,即使常量池中有 "abc",str1 和 str2 也是两个不同的堆对象,所以==
判断为 false。
四、String 类的常用方法:从比较到操作
String 类提供了大量实用方法,掌握它们能让字符串处理事半功倍。
1. 字符串比较。
代码如下:
public class StringCompare {
public static void main(String[] args) {
String s1 = new String("hello");
String s2 = new String("hello");
String s3 = new String("HELLO");
String s4 = s1;
// ==:比较地址
System.out.println(s1 == s2); // false(不同对象)
System.out.println(s1 == s4); // true(同一对象)
// equals:比较内容
System.out.println(s1.equals(s2)); // true(内容相同)
System.out.println(s1.equals(s3)); // false(大小写不同)
// compareTo:返回字符差值或长度差
System.out.println("abc".compareTo("abd")); // -1('c'比'd'小1)
System.out.println("abc".compareTo("ab")); // 1(前者长1)
// compareToIgnoreCase:忽略大小写
System.out.println("abc".compareToIgnoreCase(s3)); // 0(内容相同)
}
}
判断字符串内容是否相等时,永远用equals
而不是 ==
2. 字符串查找:定位字符或子串
当你需要在字符串中找某个字符或子串时,这些方法很有用:
代码示例:
public class StringSearch {
public static void main(String[] args) {
String s = "aaabbbcccaaabbbccc";
// 获取索引3的字符(注意:索引从0开始)
System.out.println(s.charAt(3)); // 输出:b
// 找'c'第一次出现的位置
System.out.println(s.indexOf('c')); // 输出:6
// 从索引10开始找'c'
System.out.println(s.indexOf('c', 10)); // 输出:15
// 找子串"bbb"最后一次出现的位置
System.out.println(s.lastIndexOf("bbb")); // 输出:12
}
}
3. 字符串转换:灵活处理数据格式
字符串和其他类型的转换是高频操作,比如用户输入的数字是字符串,需要转成 int 才能计算。
3.1 数值 ↔ 字符串
public class StringConvert {
public static void main(String[] args) {
// 数字转字符串
String s1 = String.valueOf(123); // "123"
String s2 = String.valueOf(12.34); // "12.34"
String s3 = String.valueOf(true); // "true"
// 字符串转数字(注意:字符串必须是纯数字,否则抛异常)
int num1 = Integer.parseInt("123"); // 123
double num2 = Double.parseDouble("12.34"); // 12.34
}
}
3.2 大小写转换
String s = "Hello World";
System.out.println(s.toUpperCase()); // 输出:HELLO WORLD(转大写)
System.out.println(s.toLowerCase()); // 输出:hello world(转小写)
3.3 字符串 ↔ 字符数组
// 字符串转字符数组
String s = "hello";
char[] chars = s.toCharArray(); // {'h','e','l','l','o'}
// 字符数组转字符串
String s2 = new String(chars); // "hello"
4. 字符串替换、拆分与截取
这些方法能帮你快速处理字符串的结构:
4.1 替换
String str = "helloworld";
// 替换所有'l'为'_'
System.out.println(str.replaceAll("l", "_")); // he__owor_d
// 只替换第一个'l'
System.out.println(str.replaceFirst("l", "_")); // he_loworld
4.2 拆分(按分隔符分割)
// 按空格拆分字符串
String str = "hello world hello bit";
String[] parts = str.split(" ");
for (String part : parts) {
System.out.println(part); // 依次输出hello、world、hello、bit
}
// 拆分IP地址(注意:.需要转义为\\.)
String ip = "192.168.1.1";
String[] ipParts = ip.split("\\."); // ["192","168","1","1"]
注意:拆分特殊字符(如*
、+
、\
)时需要转义,比如split("\\*")
。
4.3 截取子串
String str = "helloworld";
// 从索引5截取到结尾
System.out.println(str.substring(5)); // world
// 截取[0,5)区间(包含0,不包含5)
System.out.println(str.substring(0, 5)); // hello
注意:
1.索引是从0开始的。
2.截取遵循前闭后开的原则,即第一位包含,第二位数字不包含。
在Java中还有更多的String方法,能够更好的满足你的不同需求,有待你的发现。
五、String 的不可变性:为什么字符串不能被修改?
你可能会疑惑:明明可以写s = s + "world"
,为什么说 String 是不可变的?
1.什么是不可变性?
String 的不可变性是指:字符串一旦创建,其内容(字符序列)就不能被修改。任何看似修改的操作(如拼接、替换),实际上都会创建新的 String 对象。
2.为什么不可变?
看 String 类的源码就知道了:
关键原因有两点:
- 存储字符的
value
数组被private final
修饰,外部无法直接修改,且 String 类没有提供修改数组的方法(如 set 方法)。 - 所有修改操作(如
toUpperCase
、concat
)都会创建新的 String 对象,原对象内容不变。
3.直观感受:字符串拼接的本质
String s = "hello";
s = s + " world"; // 看似修改,实则创建新对象
- 初始
s
指向常量池中的 "hello"。 - 执行
s + " world"
时,JVM 创建新对象 "hello world"。 s
转而指向新对象,原 "hello" 仍在内存中(等待垃圾回收)。
六、StringBuffer 与 StringBuilder:可变字符串的救星
由于 String 的不可变性,频繁修改字符串(如循环拼接)会创建大量临时对象,严重影响效率。这时就需要StringBuffer和StringBuilder。
两者有以下主要区别:
建议:单线程场景用 StringBuilder(效率高),多线程场景用 StringBuffer(安全)。
常用方法(以 StringBuilder 为例)
public class StringBuilderDemo {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("hello");
// 追加内容(尾插)
sb.append(" "); // "hello "
sb.append("world");// "hello world"
sb.append(123); // "hello world123"
// 修改指定位置字符
sb.setCharAt(0, 'H'); // "Hello world123"
// 插入内容
sb.insert(5, "!!!"); // "Hello!!! world123"
// 逆转字符串
sb.reverse(); // "321dlrow !!!olleH"
// 转成String
String result = sb.toString();
System.out.println(result);
}
}
效率对比:String vs StringBuilder
用循环拼接 10000 次数字,看看差距:
public class EfficiencyTest {
public static void main(String[] args) {
// String拼接(效率低)
long start = System.currentTimeMillis();
String s = "";
for (int i = 0; i < 10000; i++) {
s += i;
}
long end = System.currentTimeMillis();
System.out.println("String耗时:" + (end - start) + "ms"); // 约几百ms
// StringBuilder拼接(效率高)
start = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i);
}
end = System.currentTimeMillis();
System.out.println("StringBuilder耗时:" + (end - start) + "ms"); // 约1ms
}
}
结论:频繁修改字符串时,坚决不用 String,改用 StringBuilder!