Java中字符串拼接推荐使用+运算符处理少量拼接,大量拼接时优先选用StringBuilder提升性能,多线程环境下可选StringBuffer保证线程安全;截取主要通过substring(int beginIndex)或substring(int beginIndex, int endIndex)实现,需注意索引范围避免越界,实际开发中应结合length、indexOf等方法进行边界判断与安全处理;此外,String类还提供equals、contains、replace、split等丰富方法,用于比较、查找、替换和分割字符串,满足日常文本处理多样化需求。
Java中对字符串进行拼接和截取是日常开发里最基础也最频繁的操作之一。简单来说,拼接就是把多个字符串连成一个,而截取则是从一个现有字符串中提取出它的一部分。理解并熟练运用这些方法,是处理文本数据的基石。
在Java里,字符串的拼接和截取都有多种实现方式,各有特点和适用场景。
字符串拼接
最直观的拼接方式莫过于使用加号
+运算符。它用起来确实方便,比如:
String part1 = "Hello"; String part2 = "World"; String combined = part1 + " " + part2 + "!"; // 结果: "Hello World!" System.out.println(combined);
这种方式在多数情况下表现良好,尤其是在拼接次数不多的时候。然而,需要注意的是,Java中的
String对象是不可变的。这意味着每次使用
+拼接时,都会创建一个新的
String对象来存储结果,这在循环中进行大量拼接时可能会导致性能问题,因为会产生很多临时的、很快就会被垃圾回收的对象。
为了解决这个问题,Java提供了
StringBuilder和
StringBuffer类。它们是可变的字符序列,更适合进行多次字符串修改操作。
StringBuilder非线程安全,性能更高;
StringBuffer线程安全,但性能略低。在单线程环境下,我个人更倾向于使用
StringBuilder。
// 使用 StringBuilder 进行拼接
StringBuilder sb = new StringBuilder();
sb.append("这是").append("一个").append("StringBuilder").append("的例子。");
String result = sb.toString();
System.out.println(result); // 结果: "这是一个StringBuilder的例子。"
// 如果在多线程环境,或者需要线程安全,可以使用 StringBuffer
StringBuffer sbuf = new StringBuffer();
sbuf.append("线程安全的").append("拼接。");
String safeResult = sbuf.toString();
System.out.println(safeResult); // 结果: "线程安全的拼接。"String类自身也提供了一个
concat()方法用于拼接,但它和
+运算符一样,每次调用都会创建新的
String对象,所以性能上没有本质区别,不建议在循环中大量使用。
String s1 = "Java"; String s2 = "编程"; String s3 = s1.concat(s2); System.out.println(s3); // 结果: "Java编程"
字符串截取
截取字符串主要依赖
String类的
substring()方法。它有两个重载版本:
substring(int beginIndex): 从指定索引
beginIndex(包含)开始,截取到字符串的末尾。索引是从0开始计数的。
String original = "HelloJavaWorld"; String sub1 = original.substring(5); // 从索引5开始截取 System.out.println(sub1); // 结果: "JavaWorld"
substring(int beginIndex, int endIndex): 从指定索引
beginIndex(包含)开始,截取到
endIndex(不包含)为止。
String original = "HelloJavaWorld"; String sub2 = original.substring(0, 5); // 从索引0到索引5(不包含5) System.out.println(sub2); // 结果: "Hello" String sub3 = original.substring(5, 9); // 从索引5到索引9(不包含9) System.out.println(sub3); // 结果: "Java"
理解索引的“包含”和“不包含”非常关键,尤其是在处理边界情况时。
在Java中,字符串拼接的性能差异是一个老生常谈的话题,但它确实值得深入理解。这背后主要是
String对象的不可变性在起作用。当我们用
+运算符拼接字符串时,比如
String s = "a" + "b" + "c";,在编译阶段,如果所有操作数都是字面量,Java编译器可能会进行优化,直接生成
String s = "abc";。但如果操作数中包含变量,例如
String s = str1 + str2 + str3;,那么在运行时,每次
+操作都会创建一个新的
String对象。想象一下在一个循环里拼接几千几万次,这会产生大量的中间
String对象,给垃圾回收器带来不小的压力,从而影响程序性能。
concat()方法也面临同样的问题,它的底层实现和
+运算符在变量拼接时是类似的,每次调用都会返回一个新的
String对象。
而
StringBuilder和
StringBuffer的设计初衷就是为了解决这个问题。它们内部维护一个可变的字符数组,
append()操作不会每次都创建新对象,而是直接在现有数组上进行修改或扩容。只有当你调用
toString()方法时,才会生成最终的
String对象。
+或
concat()? 当你需要拼接的字符串数量非常少,比如两三个,或者是在字符串字面量之间拼接时,用
+运算符是最简洁方便的。它的可读性很好,对于这种简单场景,性能开销几乎可以忽略不计。
StringBuilder? 在大多数需要进行多次字符串拼接的场景,尤其是循环内部,或者需要构建复杂字符串时,
StringBuilder是首选。它性能最好,因为它没有同步开销。
StringBuffer? 如果你的应用程序是多线程的,并且多个线程可能同时操作同一个字符串构建器实例,那么
StringBuffer是必要的。它的方法都是同步的,可以保证线程安全,但代价是略微的性能损失。当然,在现代Java开发中,我们更多地会考虑使用
ConcurrentHashMap这样的并发集合,或者通过其他方式避免共享可变状态,但
StringBuffer依然是一个有效的选项。
选择哪种方式,归根结底是根据你的具体需求和对性能、线程安全的要求来决定的。没有绝对的“最好”,只有“最合适”。
substring()方法虽然用起来简单,但如果对索引规则理解不透彻,很容易遇到
IndexOutOfBoundsException。这是我在实际开发中遇到过不少次的问题,尤其是在处理用户输入或外部系统传来的不确定长度的字符串时。
substring(beginIndex)和
substring(beginIndex, endIndex)都要求传入的索引是合法的。所谓合法,就是它们必须在
[0, length]这个范围内。
常见的陷阱:
beginIndex
小于 0 或大于 length()
:
String str = "abc"; str.substring(-1);->
IndexOutOfBoundsException
String str = "abc"; str.substring(4);->
IndexOutOfBoundsException
beginIndex必须在
[0, length]之间。
endIndex
小于 beginIndex
或大于 length()
:
String str = "abc"; str.substring(1, 0);->
IndexOutOfBoundsException(因为
endIndex小于
beginIndex)
String str = "abc"; str.substring(0, 4);->
IndexOutOfBoundsException(因为
endIndex大于
length())
endIndex必须在
[beginIndex, length]之间。记住
endIndex是不包含的。
边界处理策略:
预检查字符串长度: 在调用
substring()之前,先检查字符串是否为空 (
null),或者长度是否符合预期。这是最基本的防御性编程。
String text = "Hello";
if (text != null && text.length() >= 5) {
String part = text.substring(0, 5);
System.out.println(part);
} else {
System.out.println("字符串不符合截取条件");
}利用 indexOf()
和 lastIndexOf()
查找子串位置: 如果你需要根据某个分隔符或特定子串来截取,结合
indexOf()和
lastIndexOf()是非常实用的。它们返回子串的起始索引,如果找不到则返回
-1,这可以帮助我们避免硬编码索引。
String path = "/usr/local/bin/java";
int lastSlash = path.lastIndexOf("/");
if (lastSlash != -1) {
String fileName = path.substring(lastSlash + 1);
System.out.println("文件名: " + fileName); // 结果: "java"
}处理空字符串或单字符字符串: 对于这些特殊情况,
substring()依然有效,但你需要确保你的逻辑能正确处理它们。例如,
"".substring(0)会得到空字符串,而
"".substring(0,0)也是空字符串。
使用 String.isEmpty()
或 String.isBlank()
: 在Java 11及更高版本中,
String.isBlank()可以判断字符串是否为空或只包含空白字符,这比
isEmpty()更全面,有助于提前过滤掉不适合截取的字符串。
总的来说,处理字符串截取时,时刻牢记索引的有效范围,并结合条件判断来规避潜在的运行时错误,是编写健壮代码的关键。

除了拼接和截取,
String类提供了一系列丰富的方法,几乎涵盖了日常文本处理的各种需求。这些方法在数据清洗、格式化、校验等场景中扮演着重要角色。
获取信息类:
length(): 返回字符串的长度(字符数)。
charAt(int index): 返回指定索引处的字符。
indexOf(String str)/
indexOf(String str, int fromIndex): 返回指定子字符串第一次出现的索引。找不到则返回 -1。
lastIndexOf(String str)/
lastIndexOf(String str, int fromIndex): 返回指定子字符串最后一次出现的索引。
比较与查找类:
equals(Object anObject): 比较两个字符串的内容是否相等,区分大小写。
equalsIgnoreCase(String anotherString): 比较两个字符串的内容是否相等,不区分大小写。
contains(CharSequence s): 判断字符串是否包含指定的字符序列。
startsWith(String prefix): 判断字符串是否以指定前缀开头。
endsWith(String suffix): 判断字符串是否以指定后缀结尾。
compareTo(String anotherString): 按字典顺序比较两个字符串。
修改与替换类:
replace(char oldChar, char newChar): 将字符串中所有出现的
oldChar替换为
newChar。
replace(CharSequence target, CharSequence replacement): 将所有出现的
target字符序列替换为
replacement。
replaceAll(String regex, String replacement): 使用正则表达式替换所有匹配项。
replaceFirst(String regex, String replacement): 使用正则表达式替换第一个匹配项。
trim(): 移除字符串两端的空白字符。
strip(): (Java 11+)更智能地移除两端的空白字符,包括Unicode空白字符。
stripLeading()/
stripTrailing(): (Java 11+)分别移除前导或尾随空白字符。
toLowerCase(): 将字符串转换为小写。
toUpperCase(): 将字符串转换为大写。
分割类:
split(String regex): 根据给定的正则表达式将字符串分割成子字符串数组。
split(String regex, int limit): 限制分割的次数。
格式化与转换类:
valueOf(各种类型): 将各种数据类型转换为字符串。这是
String.valueOf(123)这样的静态方法。
toCharArray(): 将字符串转换为字符数组。
getBytes(): 将字符串编码为字节数组。
这些方法共同构成了Java字符串处理的强大工具箱。在实际项目中,我们很少只用到拼接和截取,往往需要组合使用这些方法来完成复杂的文本处理任务,比如解析日志、处理CSV文件、构建SQL查询、验证用户输入格式等等。理解并灵活运用它们,能大大提高开发效率和代码质量。