在 Java 编程世界中,String 类无疑是使用频率最高的类之一。无论是日常开发中的数据处理,还是校招笔试中的算法题,亦或是面试时被频繁问到的底层原理,String 类都占据着重要地位。与 C 语言中用字符数组或指针处理字符串的方式不同,Java 的 String 类将数据与操作方法封装在一起,完美契合面向对象的设计思想。今天,我们就从 String 类的基础认知出发,逐步深入其核心用法、底层特性,并对比相关的 StringBuffer 和 StringBuilder 类,帮助大家全面掌握这一核心知识点。
一、为什么 String 类如此重要?
在 C 语言中,字符串的处理依赖于字符数组和标准库函数(如strlen、strcpy),这种 “数据与操作分离” 的模式不仅不够直观,还容易出现内存越界等问题。而 Java 作为面向对象语言,专门设计了 String 类来封装字符串相关的属性和方法,让字符串操作更安全、更便捷。
从实际应用场景来看,String 类的重要性体现在三个方面:
- 开发高频性:字符串处理是开发中的基础需求,比如用户输入验证、数据格式转换(如字符串转整数)、日志输出等,都离不开 String 类的支持;
- 笔试常客:校招笔试中,字符串相关题目占比极高,例如 “字符串相加”“第一个只出现一次的字符”“最后一个单词的长度” 等,都是经典考点;
- 面试热点:面试官常围绕 String 类的底层原理提问,比如 “String 为什么不可变?”“String、StringBuffer、StringBuilder 的区别是什么?”,这些问题直接考察对 Java 底层设计的理解。
二、String 类的基础用法
String 类提供了丰富的方法来满足字符串操作需求,我们从最核心的 “构造、比较、查找、转化、替换、拆分、截取” 七个维度展开讲解。
2.1 字符串构造:三种常用方式
String 类的构造方式有很多,日常开发中最常用的是以下三种:
public class StringDemo {
public static void main(String[] args) {
//1. 直接使用常量串构造(推荐,效率更高)
String s1 = "hello world";
//2. 通过new关键字创建String对象
String s2 = new String("hello world");
//3. 利用字符数组构造
char[] charArray = {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'};
String s3 = new String(charArray);
System.out.println(s1); //输出:hello world
System.out.println(s2); //输出:hello world
System.out.println(s3); //输出:hello world
}
}
注意:String 是引用类型,其内部并不直接存储字符串,而是通过一个private final char value[]数组(JDK1.8 及之前)来保存字符数据。这一点对理解 String 的 “不可变性” 至关重要。
2.2 字符串比较:四种核心方式
字符串比较是判断两个字符串 “是否相等” 或 “大小关系” 的操作,Java 提供了四种常用方式,需注意区分适用场景:
| 比较方式 | 核心逻辑 | 适用场景 |
|---|---|---|
== | 对基本类型比较 “值”,对引用类型比较 “地址”(是否指向同一个对象) | 判断两个引用是否指向同一对象 |
equals(Object obj) | 重写自 Object 类,按 “字典序” 逐个比较字符,完全一致则返回 true | 判断字符串内容是否相等 |
compareTo(String s) | 按字典序比较,返回差值(正数:当前串大;负数:参数串大;0:相等) | 字符串排序(如 Collections.sort) |
compareToIgnoreCase | 与 compareTo 逻辑一致,但忽略大小写(如 'A' 和 'a' 视为相等) | 不区分大小写的比较 |
代码示例:
public class StringCompare {
public static void main(String[] args) {
String s1 = new String("hello");
String s2 = new String("hello");
String s3 = new String("Hello");
//1. == 比较地址
System.out.println(s1 == s2); //false(两个不同对象,地址不同)
//2. equals 比较内容
System.out.println(s1.equals(s2)); //true(内容完全一致)
System.out.println(s1.equals(s3)); //false(大小写不同)
//3. compareTo 比较大小
System.out.println(s1.compareTo(s2)); //0(相等)
System.out.println(s1.compareTo(s3)); //32('h'的ASCII码比'H'大32)
//4. compareToIgnoreCase 忽略大小写比较
System.out.println(s1.compareToIgnoreCase(s3)); //0(忽略大小写后相等)
}
}
2.3 字符串查找:定位字符或子串
当需要在字符串中查找某个字符或子串的位置时,可使用 String 类提供的charAt、indexOf、lastIndexOf等方法,常用方法如下:
| 方法 | 功能描述 |
|---|---|
charAt(int index) | 返回指定索引处的字符,索引越界会抛出IndexOutOfBoundsException |
indexOf(int ch) | 从左到右查找字符 ch,返回第一次出现的索引,未找到返回 - 1 |
indexOf(String str, int from) | 从 from 索引开始,查找子串 str 第一次出现的位置,未找到返回 - 1 |
lastIndexOf(int ch) | 从右到左查找字符 ch,返回第一次出现的索引,未找到返回 - 1 |
代码示例:
public class StringSearch {
public static void main(String[] args) {
String s = "aaabbbcccaaabbbccc";
System.out.println(s.charAt(3)); //输出:b(索引3处的字符)
System.out.println(s.indexOf('c')); //输出:6('c'第一次出现的位置)
System.out.println(s.lastIndexOf("bbb")); //输出:12("bbb"最后一次出现的位置)
}
}
2.4 字符串转化:格式与类型的转换
字符串转化是开发中常见的需求,主要包括 “数值与字符串互转”“大小写转换”“字符串与数组互转” 和 “格式化” 四种场景:
(1)数值与字符串互转
- 数值转字符串:使用
String.valueOf(数值),支持 int、double、boolean 等类型; - 字符串转数值:使用包装类的
parseXxx方法(如Integer.parseInt、Double.parseDouble)。
//数值转字符串
String numStr1 = String.valueOf(123); // "123"
String numStr2 = String.valueOf(3.14); // "3.14"
//字符串转数值
int num1 = Integer.parseInt("123"); // 123
double num2 = Double.parseDouble("3.14"); // 3.14
(2)大小写转换
通过toUpperCase()(转大写)和toLowerCase()(转小写)实现,仅影响字母,不改变其他字符:
String lowerStr = "hello WORLD";
System.out.println(lowerStr.toUpperCase()); //"HELLO WORLD"
System.out.println(lowerStr.toLowerCase()); //"hello world"
(3)字符串与数组互转
- 字符串转数组:
toCharArray(),返回字符数组; - 数组转字符串:通过
new String(字符数组)实现。
//字符串转数组
char[] chars = "hello".toCharArray(); // ['h','e','l','l','o']
//数组转字符串
String str = new String(chars); // "hello"
(4)格式化
使用String.format(格式, 参数),按指定格式生成字符串(如日期、数字格式):
String dateStr = String.format("%d年%d月%d日", 2024, 5, 20);
System.out.println(dateStr); //输出:2024年5月20日
2.5 字符串替换:替换指定内容
字符串替换用于将字符串中的某个字符或子串替换为新内容,常用replaceAll(替换所有)和replaceFirst(替换第一个):
String str = "helloworld";
//替换所有'l'为'_'
System.out.println(str.replaceAll("l", "_")); //"he__owor_d"
//只替换第一个'l'为'_'
System.out.println(str.replaceFirst("l", "_")); //"he_loworld"
注意:String 是不可变对象,替换操作不会修改原字符串,而是返回一个新的字符串。
2.6 字符串拆分:按分隔符分割
将一个完整字符串按指定分隔符拆分为多个子串,使用split方法,支持 “全部分拆” 和 “指定拆分数”:
//1. 全部分拆(按空格分割)
String str1 = "hello world hello bit";
String[] parts1 = str1.split(" ");
for (String part : parts1) {
System.out.println(part); //依次输出:hello、world、hello、bit
}
//2. 指定拆分数(拆分为2组)
String[] parts2 = str1.split(" ", 2);
for (String part : parts2) {
System.out.println(part); //输出:hello、world hello bit
}
特殊情况:若分隔符是 “|”“*”“.” 等特殊字符,需添加转义符 “\”(如拆分 IP 地址时,分隔符为 “\.”):
String ip = "192.168.1.1";
String[] ipParts = ip.split("\\."); //注意转义
for (String part : ipParts) {
System.out.println(part); //依次输出:192、168、1、1
}
2.7 字符串截取:获取子串
从原字符串中截取部分内容,使用substring方法,支持 “从指定索引截到末尾” 和 “指定起止索引截取”:
String str = "helloworld";
//1. 从索引5截到末尾(索引从0开始)
System.out.println(str.substring(5)); //"world"
//2. 截取[0,5)区间的子串(前闭后开,包含0,不包含5)
System.out.println(str.substring(0, 5)); //"hello"
三、String 的核心特性:不可变性
String 类最核心的特性是 “不可变性”—— 一旦字符串对象被创建,其内容就无法被修改。其原因是:
- String 类被 final 修饰:意味着 String 类不能被继承,避免子类重写方法破坏不可变性;
- value 数组被 final 修饰:仅表示 value 数组的引用不能指向其他数组,但数组内部的字符是可以修改的(只是 String 类没有提供修改数组的方法);
- 无修改方法:String 类没有提供任何可以修改
value数组内容的 public 方法(如 “修改某个索引的字符”“追加字符” 等),所有看似 “修改” 的操作(如replace、substring)都会创建新的 String 对象。
代码验证:
//尝试修改value数组(反射方式,不推荐)
String str = "hello";
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true); //跳过访问权限检查
char[] value = (char[]) valueField.get(str);
value[0] = 'H';
System.out.println(str); //输出:Hello(证明value数组内容可改,但String类未提供公开方法)
为什么要设计成不可变性?
String 的不可变性带来了三个核心优势:
- 支持字符串常量池:常量池可以缓存相同内容的字符串,避免重复创建对象,节省内存(下文详解);
- 线程安全:不可变对象的状态不会被修改,多线程环境下无需同步,避免线程安全问题;
- 高效缓存哈希值:String 的
hashCode会缓存到hash变量中,后续获取时无需重新计算,作为 HashMap 的 key 时效率更高。
四、字符串常量池:优化内存的关键
为了减少重复创建字符串对象带来的内存开销,Java 虚拟机(JVM)设计了 “字符串常量池”—— 一个存储字符串常量的特殊区域(JDK1.8 后位于堆区)。
常量池的工作原理
- 当使用 “常量串构造”(如
String s = "hello")时,JVM 会先检查常量池中是否存在 “hello”:若存在,直接让s指向常量池中的对象;若不存在,先在常量池中创建 “hello” 对象,再让s指向它。 - 当使用
new关键字构造(如String s = new String("hello"))时,JVM 会先在常量池中检查 “hello”:若不存在,先在常量池创建 “hello”,再在堆区创建新的 String 对象(指向常量池的 “hello”);若存在,直接在堆区创建新的 String 对象(指向常量池的 “hello”)。
代码示例:
String s1 = "hello"; //常量池查找,不存在则创建
String s2 = "hello"; //常量池已存在,直接指向
String s3 = new String("hello"); //堆区创建新对象,指向常量池的"hello"
System.out.println(s1 == s2); //true(都指向常量池的对象)
System.out.println(s1 == s3); //false(s1指向常量池,s3指向堆区)
常量池的优化:intern () 方法
intern()方法可以将堆区的字符串对象 “入池”—— 若常量池中已存在相同内容的字符串,返回常量池对象的引用;若不存在,将当前对象加入常量池并返回其引用。
代码示例:
String s1 = new String("hello"); //堆区对象,常量池创建"hello"
String s2 = s1.intern(); //常量池已存在"hello",返回常量池引用
String s3 = "hello"; //指向常量池
System.out.println(s1 == s2); //false(s1指向堆区,s2指向常量池)
System.out.println(s2 == s3); //true(都指向常量池)
五、StringBuffer 与 StringBuilder:可变字符串
由于 String 的不可变性,频繁修改字符串(如循环追加)会创建大量临时对象,导致效率极低。为了解决这个问题,Java 提供了两个可变字符串类:StringBuffer 和 StringBuilder。
5.1 核心方法(以 StringBuilder 为例)
StringBuffer 和 StringBuilder 的方法基本一致,常用方法包括append(追加)、insert(插入)、delete(删除)、replace(替换)、reverse(反转)等:
StringBuilder sb = new StringBuilder("hello");
sb.append(" world"); //追加:hello world
sb.insert(5, ","); //插入:hello, world
sb.setCharAt(0, 'H'); //修改:Hello, world
sb.delete(5, 6); //删除:Hello world
sb.reverse(); //反转:dlrow olleH
System.out.println(sb.toString()); //输出:dlrow olleH
5.2 三者的核心区别
String、StringBuffer、StringBuilder 的区别是面试高频题,核心差异体现在 “可变性” 和 “线程安全性” 上:
| 特性 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 可变性 | 不可变(修改创建新对象) | 可变(直接修改内容) | 可变(直接修改内容) |
| 线程安全性 | 线程安全(不可变) | 线程安全(同步方法) | 线程不安全(无同步) |
| 效率 | 低(频繁修改创建临时对象) | 中(同步开销) | 高(无同步开销) |
| 适用场景 | 字符串不修改(如常量定义) | 多线程修改(如日志输出) | 单线程修改(如字符串拼接) |
性能对比:
通过循环追加 10000 次数字,对比三者的耗时(单位:毫秒):
//String:耗时约100ms(频繁创建对象)
String s = "";
for (int i = 0; i < 10000; i++) s += i;
//StringBuffer:耗时约1ms(同步开销)
StringBuffer sbf = new StringBuffer();
for (int i = 0; i < 10000; i++) sbf.append(i);
//StringBuilder:耗时约0.5ms(无同步开销)
StringBuilder sbd = new StringBuilder();
for (int i = 0; i < 10000; i++) sbd.append(i);
六、总结
String 类是 Java 的核心类之一,掌握其用法和底层原理对开发和面试都至关重要。本文从基础用法出发,深入讲解了 String 的不可变性、字符串常量池,并对比了 StringBuffer 和 StringBuilder 的差异,最后通过面试题实战帮助大家巩固知识点。
核心要点回顾:
- String 不可变,修改操作会创建新对象,频繁修改建议用 StringBuilder(单线程)或 StringBuffer(多线程);
- 字符串比较优先使用
equals,而非==; - 字符串常量池通过缓存相同内容的字符串优化内存,
intern()方法可手动入池; - StringBuffer 线程安全但效率低,StringBuilder 线程不安全但效率高,根据场景选择。
希望本文能帮助大家更加了解 String 类,在开发中写出更高效的代码。
2152

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



