1. Java字符串处理基础
在Java编程中,字符串处理是最基础也是最重要的技能之一。String类作为Java语言中最常用的类,几乎出现在每一个Java程序中。理解String类的特性和使用方法,对于编写高效、健壮的Java代码至关重要。
1.1 String类的基本特性
String类位于java.lang包中,是Java语言的核心类之一。它代表不可变的字符序列,具有以下几个重要特点:
-
不可变性:String对象一旦创建,其内容就不能被修改。任何看似修改字符串的操作,实际上都是创建了一个新的String对象。
-
字符串池:Java使用字符串池来存储字符串字面量,相同的字符串字面量会指向池中的同一个对象,这样可以节省内存空间。
-
底层实现:在JDK8及之前,String内部使用char数组存储字符;从JDK9开始,改为使用byte数组存储,并添加了一个编码标识字段,这样可以更节省内存空间。
java复制// JDK8及以前的String实现
public final class String {
private final char[] value;
}
// JDK9及以后的String实现
public final class String {
private final byte[] value;
private final byte coder; // 编码标识(0表示LATIN1,1表示UTF16)
}
1.2 字符串的创建方式
Java中创建字符串主要有两种方式:
- 直接赋值:使用双引号创建字符串字面量,这种方式创建的字符串会被放入字符串池中。
java复制String s1 = "Hello"; // 字符串字面量,存储在字符串池中
String s2 = "Hello"; // 会复用字符串池中的"Hello"
System.out.println(s1 == s2); // true,因为指向同一个对象
- 使用new关键字:通过构造函数创建String对象,这种方式每次都会在堆内存中创建一个新的String对象。
java复制String s3 = new String("Hello"); // 在堆中创建新对象
String s4 = new String("Hello"); // 再次创建新对象
System.out.println(s3 == s4); // false,两个不同的对象
最佳实践:在大多数情况下,推荐使用直接赋值的方式创建字符串,因为它可以利用字符串池的特性,节省内存空间。只有在需要将字符数组或字节数组转换为字符串时,才需要使用new关键字。
1.3 字符串的内存分析
理解字符串在内存中的存储方式对于编写高效代码非常重要。下面是一个内存分析的示例:
java复制String s1 = "Hello"; // 字符串池
String s2 = "Hello"; // 复用字符串池中的"Hello"
String s3 = new String("Hello"); // 堆内存中的新对象
String s4 = s3.intern(); // 返回字符串池中的"Hello"
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // false
System.out.println(s1 == s4); // true
内存结构示意图:
code复制字符串池
┌──────────────┐
│ "Hello" │ ← s1, s2, s4
│ [0x100] │
└──────────────┘
↑
│
s3 → ┌─────────────┐
│ String对象 │
│ value→0x100 │
└─────────────┘
intern()方法可以将字符串对象添加到字符串池中(如果池中还没有该字符串),并返回池中的引用。这是一个非常有用的方法,特别是在处理大量可能重复的字符串时。
2. 字符串的比较操作
字符串比较是编程中最常见的操作之一,但也是最容易出错的地方之一。理解各种比较方式的区别对于编写正确的代码至关重要。
2.1 ==与equals的区别
- ==操作符:比较的是两个对象的引用(内存地址)是否相同。
java复制String s1 = "Hello";
String s2 = "Hello";
String s3 = new String("Hello");
System.out.println(s1 == s2); // true,指向字符串池中的同一个对象
System.out.println(s1 == s3); // false,s3是堆中的新对象
- equals方法:比较的是两个字符串的内容是否相同。
java复制System.out.println(s1.equals(s2)); // true,内容相同
System.out.println(s1.equals(s3)); // true,内容相同
常见错误:初学者经常错误地使用==来比较字符串内容,这会导致逻辑错误。记住,比较字符串内容时总是使用equals方法。
2.2 equals方法的正确使用
equals方法虽然简单,但在使用时也有一些需要注意的地方:
java复制String str = null;
// 错误写法:可能抛出NullPointerException
if (str.equals("Hello")) {
// ...
}
// 正确写法1:把已知字符串放在前面
if ("Hello".equals(str)) {
// ...
}
// 正确写法2:使用Objects.equals(JDK7+)
if (Objects.equals(str, "Hello")) {
// ...
}
2.3 忽略大小写的比较
在某些场景下,我们需要忽略大小写来比较字符串,这时可以使用equalsIgnoreCase方法:
java复制String s1 = "Hello";
String s2 = "hello";
System.out.println(s1.equals(s2)); // false
System.out.println(s1.equalsIgnoreCase(s2)); // true
2.4 compareTo方法
compareTo方法用于按字典顺序比较两个字符串,返回一个整数表示比较结果:
- 返回0表示两个字符串相等
- 返回负数表示当前字符串在字典顺序中位于参数字符串之前
- 返回正数表示当前字符串在字典顺序中位于参数字符串之后
java复制System.out.println("apple".compareTo("banana")); // 负数('a'<'b')
System.out.println("apple".compareTo("apple")); // 0
System.out.println("banana".compareTo("apple")); // 正数('b'>'a')
这个方法在排序字符串集合时非常有用。
3. 字符串的常用操作方法
String类提供了丰富的方法来操作字符串,掌握这些方法可以大大提高编程效率。下面我们分类介绍这些常用方法。
3.1 获取字符串信息
- length():获取字符串的长度(字符数)
java复制String str = "Hello";
System.out.println(str.length()); // 5
- charAt(int index):获取指定索引处的字符
java复制System.out.println(str.charAt(0)); // 'H'
System.out.println(str.charAt(4)); // 'o'
- toCharArray():将字符串转换为字符数组
java复制char[] chars = str.toCharArray();
for (char c : chars) {
System.out.print(c + " "); // H e l l o
}
- substring(int beginIndex):从指定位置开始截取子串
java复制System.out.println("HelloWorld".substring(5)); // "World"
- substring(int beginIndex, int endIndex):截取指定范围的子串
java复制System.out.println("HelloWorld".substring(0, 5)); // "Hello"
注意:substring方法的endIndex参数是"独占"的,即不包含该索引位置的字符。
3.2 字符串判断方法
- isEmpty():判断字符串是否为空(长度为0)
java复制System.out.println("".isEmpty()); // true
System.out.println("Hello".isEmpty()); // false
- contains(CharSequence s):判断是否包含指定字符序列
java复制System.out.println("Hello".contains("ell")); // true
System.out.println("Hello".contains("E")); // false
- startsWith(String prefix):判断是否以指定前缀开头
java复制System.out.println("Hello".startsWith("He")); // true
System.out.println("Hello".startsWith("he")); // false
- endsWith(String suffix):判断是否以指定后缀结尾
java复制System.out.println("Hello.java".endsWith(".java")); // true
System.out.println("Hello.java".endsWith(".class")); // false
3.3 字符串转换方法
- toUpperCase() / toLowerCase():大小写转换
java复制System.out.println("Hello".toUpperCase()); // "HELLO"
System.out.println("Hello".toLowerCase()); // "hello"
- replace(char oldChar, char newChar):字符替换
java复制System.out.println("Hello".replace('l', 'L')); // "HeLLo"
- replace(CharSequence target, CharSequence replacement):字符串替换
java复制System.out.println("Hello World".replace("World", "Java")); // "Hello Java"
- trim():去除首尾空白字符
java复制System.out.println(" Hello ".trim()); // "Hello"
- concat(String str):字符串连接
java复制System.out.println("Hello".concat(" World")); // "Hello World"
3.4 字符串分割方法
split方法用于将字符串按照指定的正则表达式分割成字符串数组:
java复制String str = "张三,李四,王五";
String[] names = str.split(",");
for (String name : names) {
System.out.println(name);
}
// 输出:
// 张三
// 李四
// 王五
当分割符是正则表达式中的特殊字符时,需要进行转义:
java复制// 分割IP地址(.是正则表达式特殊字符)
String ip = "192.168.1.1";
String[] parts = ip.split("\\.");
// 分割文件路径(\需要双重转义)
String path = "D:\\Java\\project\\demo.java";
String[] dirs = path.split("\\\\");
3.5 字符串查找方法
- indexOf(String str):查找子串第一次出现的位置
java复制String str = "Hello World Hello Java";
System.out.println(str.indexOf("Hello")); // 0
System.out.println(str.indexOf("World")); // 6
System.out.println(str.indexOf("Python")); // -1(未找到)
- indexOf(String str, int fromIndex):从指定位置开始查找
java复制System.out.println(str.indexOf("Hello", 1)); // 12
- lastIndexOf(String str):查找子串最后一次出现的位置
java复制System.out.println(str.lastIndexOf("Hello")); // 12
这些查找方法在解析文本内容时非常有用,比如从URL中提取参数,从文件路径中提取文件名等。
4. 字符串的不可变性原理
字符串的不可变性是Java语言设计中一个非常重要的特性,理解这一特性对于编写高效、安全的代码至关重要。
4.1 不可变性的体现
java复制String s1 = "Hello";
System.out.println(s1); // Hello
s1 = "World";
System.out.println(s1); // World
// 问:字符串内容改变了吗?
// 答:没有!"Hello"依然在串池中,s1只是指向了新的"World"
内存变化示意图:
code复制串池
┌─────────────┐
│ "Hello" │ ← 原对象,没有改变
│ [0x100] │
├─────────────┤
│ "World" │ ← 新对象
│ [0x200] │
└─────────────┘
↑
│
s1 (重新指向)
4.2 字符串拼接的内存问题
java复制String s = "a";
s += "b"; // s = s + "b"
s += "c";
System.out.println(s); // abc
// 问:创建了几个对象?
// 答:5个!
// "a" → "b" → "ab" → "c" → "abc"
内存分析:
code复制串池
┌────────┐
│ "a" │ ← s最初指向
├────────┤
│ "b" │ ← 第二个对象
├────────┤
│ "ab" │ ← s拼接后指向
├────────┤
│ "c" │ ← 第三个拼接的字符串
├────────┤
│ "abc" │ ← s最终指向
└────────┘
每次拼接都产生新对象,旧对象变成垃圾
4.3 不可变性的实现原理
String类的不可变性是通过以下设计实现的:
java复制public final class String {
// 1. 类被final修饰,不能被继承
// 2. 成员变量被final修饰,一旦赋值不能改变
private final byte[] value;
// 3. 没有提供修改字符串内容的方法
}
4.4 不可变性的优势
- 安全性:字符串常量池可以共享,节省内存
- 线程安全:多线程环境下不需要同步
- 哈希值缓存:String的hashCode()只需计算一次
- 作为集合键值:因为不可变,适合作为HashMap的key
4.5 不可变性的应用场景
java复制// 场景1:方法参数传递,不会影响原字符串
String str = "Hello";
changeString(str);
System.out.println(str); // Hello(未改变)
// 场景2:字符串常量池的复用
String s1 = "abc";
String s2 = "abc";
String s3 = "abc";
// s1, s2, s3指向同一个对象,节省内存
public static void changeString(String s) {
s = "World"; // 不会影响外部的str
}
性能考虑:虽然字符串不可变带来了很多好处,但在需要频繁修改字符串的场景(如循环拼接),这种特性会导致性能问题。这时应该使用StringBuilder或StringBuffer。
5. 字符串的高效拼接与StringBuilder
由于String的不可变性,在需要频繁拼接字符串的场景下,使用String会导致性能问题。这时,StringBuilder就成为了更好的选择。
5.1 字符串拼接的性能问题
java复制// 问题:循环中使用+拼接,效率极低
String str = "";
for (int i = 1; i <= 10000; i++) {
str += i; // 每次拼接都创建新对象
}
System.out.println(str);
// 创建了约10000个对象!内存浪费,效率低
性能对比测试:
java复制// 测试1:使用+拼接
long start1 = System.currentTimeMillis();
String s1 = "";
for (int i = 0; i < 10000; i++) {
s1 += i;
}
long end1 = System.currentTimeMillis();
System.out.println("String拼接耗时:" + (end1 - start1) + "ms");
// 约200-500ms
// 测试2:使用StringBuilder
long start2 = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i);
}
String s2 = sb.toString();
long end2 = System.currentTimeMillis();
System.out.println("StringBuilder拼接耗时:" + (end2 - start2) + "ms");
// 约1-3ms
5.2 StringBuilder的基本使用
StringBuilder是一个可变的字符序列,它提供了多种方法来操作字符串:
java复制// 1. 创建StringBuilder
StringBuilder sb = new StringBuilder();
// 2. 追加内容
sb.append("Hello");
sb.append(" ");
sb.append("World");
sb.append(123);
sb.append(true);
System.out.println(sb); // Hello World123true
// 3. 反转字符串
sb.reverse();
System.out.println(sb); // eurt321dlroW olleH
// 4. 转换为String
String result = sb.toString();
5.3 StringBuilder的链式调用
StringBuilder的方法大多返回this,因此支持链式调用:
java复制StringBuilder sb = new StringBuilder();
sb.append("Hello")
.append(" ")
.append("World")
.append("!")
.reverse();
System.out.println(sb); // !dlroW olleH
5.4 StringBuilder的其他常用方法
- insert(int offset, String str):在指定位置插入字符串
java复制StringBuilder sb = new StringBuilder("Hello World");
sb.insert(5, " Java");
System.out.println(sb); // Hello Java World
- delete(int start, int end):删除指定范围的字符
java复制sb.delete(5, 10);
System.out.println(sb); // Hello World
- deleteCharAt(int index):删除指定位置的字符
java复制sb.deleteCharAt(5);
System.out.println(sb); // HelloWorld
- replace(int start, int end, String str):替换指定范围的字符
java复制sb.replace(0, 5, "Hi");
System.out.println(sb); // HiWorld
- substring(int start):获取子串(不影响原对象)
java复制String sub = sb.substring(0, 2);
System.out.println(sub); // Hi
System.out.println(sb); // HiWorld(未改变)
5.5 StringBuilder的容量管理
StringBuilder内部使用一个字符数组来存储数据,这个数组的大小就是容量:
java复制// 默认容量为16
StringBuilder sb1 = new StringBuilder();
System.out.println(sb1.capacity()); // 16
// 指定初始容量
StringBuilder sb2 = new StringBuilder(32);
System.out.println(sb2.capacity()); // 32
// 根据字符串初始化
StringBuilder sb3 = new StringBuilder("Hello");
System.out.println(sb3.capacity()); // 21 (5 + 16)
当添加的字符超过当前容量时,StringBuilder会自动扩容(通常是当前容量的2倍加2),但频繁扩容会影响性能,因此在知道大概长度的情况下,最好初始化时指定足够的容量。
5.6 StringBuilder的应用场景
- 循环拼接字符串
java复制public static String joinNumbers(int n) {
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= n; i++) {
sb.append(i);
}
return sb.toString();
}
- 字符串反转
java复制public static String reverse(String str) {
return new StringBuilder(str).reverse().toString();
}
- 过滤特定字符
java复制public static String removeChar(String str, char ch) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) != ch) {
sb.append(str.charAt(i));
}
}
return sb.toString();
}
6. StringBuffer与多线程安全
在Java中,除了StringBuilder外,还有一个类似的类StringBuffer,它提供了线程安全的字符串操作功能。
6.1 StringBuffer的基本使用
StringBuffer的API与StringBuilder几乎完全相同:
java复制StringBuffer sb = new StringBuffer();
sb.append("Hello")
.append(" ")
.append("World");
System.out.println(sb); // Hello World
sb.reverse();
System.out.println(sb); // dlroW olleH
6.2 StringBuffer与StringBuilder的区别
主要区别在于线程安全性:
-
StringBuilder:
- 非线程安全
- 性能更高
- 适用于单线程环境
-
StringBuffer:
- 线程安全(方法使用synchronized修饰)
- 性能略低
- 适用于多线程环境
java复制// StringBuffer的方法声明
public synchronized StringBuffer append(String str) {
// ...
}
6.3 性能比较
java复制// 测试StringBuilder性能
long start1 = System.currentTimeMillis();
StringBuilder sb1 = new StringBuilder();
for (int i = 0; i < 100000; i++) {
sb1.append(i);
}
long end1 = System.currentTimeMillis();
// 测试StringBuffer性能
long start2 = System.currentTimeMillis();
StringBuffer sb2 = new StringBuffer();
for (int i = 0; i < 100000; i++) {
sb2.append(i);
}
long end2 = System.currentTimeMillis();
System.out.println("StringBuilder耗时: " + (end1 - start1) + "ms");
System.out.println("StringBuffer耗时: " + (end2 - start2) + "ms");
// 通常StringBuilder比StringBuffer快20%-30%
6.4 如何选择
-
String:
- 少量字符串拼接
- 字符串内容不需要改变
-
StringBuilder:
- 单线程环境(最常用)
- 大量字符串拼接
- 字符串内容需要频繁修改
-
StringBuffer:
- 多线程环境
- 大量字符串拼接
- 需要线程安全
现代实践:随着Java并发工具包(如java.util.concurrent)的发展,大多数情况下我们可以在多线程环境中使用StringBuilder配合适当的同步机制,而不是直接使用StringBuffer,这样可以获得更好的性能。
7. 字符串操作实战案例
理论知识需要通过实践来巩固。下面我们通过几个实际案例来展示字符串操作的综合应用。
7.1 手机号脱敏处理
java复制/**
* 将手机号的中间4位替换为****
* 例如:13812345678 → 138****5678
*/
public static String maskPhone(String phone) {
if (phone == null || phone.length() != 11) {
return phone;
}
return phone.substring(0, 3) + "****" + phone.substring(7);
}
// 测试
System.out.println(maskPhone("13812345678")); // 138****5678
7.2 身份证信息提取
java复制/**
* 从18位身份证号中提取信息:
* 1. 提取出生年月日
* 2. 判断性别(倒数第二位:奇数男,偶数女)
*
* 例如:110101199001011234
* 出生日期:1990年01月01日
* 性别:男
*/
public static void extractIdInfo(String idCard) {
if (idCard == null || idCard.length() != 18) {
System.out.println("无效的身份证号");
return;
}
// 提取出生日期
String birthDate = idCard.substring(6, 14);
String formattedDate = birthDate.substring(0, 4) + "年" +
birthDate.substring(4, 6) + "月" +
birthDate.substring(6) + "日";
// 判断性别
char genderCode = idCard.charAt(16);
String gender = (genderCode % 2 == 1) ? "男" : "女";
System.out.println("出生日期:" + formattedDate);
System.out.println("性别:" + gender);
}
// 测试
extractIdInfo("110101199001011234");
// 输出:
// 出生日期:1990年01月01日
// 性别:男
7.3 字符串压缩
java复制/**
* 将字符串中连续重复的字符压缩
* 例如:
* "aaabbbccc" → "a3b3c3"
* "aabcccccaaa" → "a2b1c5a3"
*/
public static String compress(String str) {
if (str == null || str.isEmpty()) {
return str;
}
StringBuilder sb = new StringBuilder();
int count = 1;
char current = str.charAt(0);
for (int i = 1; i < str.length(); i++) {
if (str.charAt(i) == current) {
count++;
} else {
sb.append(current).append(count);
current = str.charAt(i);
count = 1;
}
}
sb.append(current).append(count);
return sb.toString();
}
// 测试
System.out.println(compress("aaabbbccc")); // a3b3c3
System.out.println(compress("aabcccccaaa")); // a2b1c5a3
7.4 回文字符串判断
java复制/**
* 判断一个字符串是否为回文(正读和反读相同)
* 例如:
* "aba" → true
* "abcba" → true
* "hello" → false
*/
public static boolean isPalindrome(String str) {
if (str == null) {
return false;
}
int left = 0;
int right = str.length() - 1;
while (left < right) {
if (str.charAt(left) != str.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
// 测试
System.out.println(isPalindrome("aba")); // true
System.out.println(isPalindrome("abcba")); // true
System.out.println(isPalindrome("hello")); // false
7.5 敏感词过滤
java复制/**
* 将字符串中的敏感词替换为***
* 敏感词列表:["垃圾", "傻瓜", "笨蛋"]
*
* 例如:
* "你是个大笨蛋" → "你是个大***"
*/
public static String filterSensitiveWords(String text) {
if (text == null || text.isEmpty()) {
return text;
}
String[] sensitiveWords = {"垃圾", "傻瓜", "笨蛋"};
for (String word : sensitiveWords) {
if (text.contains(word)) {
text = text.replace(word, "***");
}
}
return text;
}
// 测试
System.out.println(filterSensitiveWords("你是个大笨蛋")); // 你是个大***
System.out.println(filterSensitiveWords("这个产品是垃圾")); // 这个产品是***
7.6 字符串切割与重组
java复制/**
* 输入一个逗号分隔的字符串,输出用-连接的字符串
* 例如:
* 输入:"张三,李四,王五"
* 输出:"张三-李四-王五"
*
* 不能使用String的replace方法
*/
public static String convertSeparator(String str) {
if (str == null || str.isEmpty()) {
return str;
}
String[] parts = str.split(",");
StringBuilder sb = new StringBuilder();
for (int i = 0; i < parts.length; i++) {
sb.append(parts[i]);
if (i < parts.length - 1) {
sb.append("-");
}
}
return sb.toString();
}
// 测试
System.out.println(convertSeparator("张三,李四,王五")); // 张三-李四-王五
这些案例涵盖了字符串处理的常见场景,包括信息提取、格式转换、内容过滤等。掌握这些基本模式,可以应对大多数字符串处理需求。