什么是字符串拼接?
在Java代码中,字符串拼接是我们经常需要做的事情,即将多个字符串连接在一起后使用。
例如,你从A获取到"Hello"
,从B获取到"World"
,最终你得到的输出是"Hello World"
。
然而,我们都知道String
是Java中的一个不可变类,其内部实现是一个字符数组,并且被final
关键字修饰,表示一旦实例化就无法修改。
不可变类的实例一旦创建,其成员变量的值就无法更改。这种设计有很多好处,比如可以缓存hashcode
、使用更方便、更安全等。
既然字符串是不可变的,那么字符串拼接又是怎么回事呢?🤔
实际上,所谓的字符串拼接其实是生成了一个新的字符串。以下是一段字符串拼接的代码:
public class StringJointDemo {
public static void main(String[] args) {
// 使用 + 的示例
String s1 = "Hello ";
String s2 = "World";
s2 = s1 + s2;
System.out.println(s2);
}
}
最终我们得到的s2
已经是一个新的字符串。如下图所示:
s2
存储的是新创建的String
对象的引用。
那么,在Java中应该如何进行字符串拼接呢?字符串拼接的方式有很多,以下是几种常用的方式。
几种字符串拼接方式
1. 使用 +
运算符
在Java中,最简单的字符串拼接方式就是直接使用+
符号。
值得注意的是,有些人将Java中使用+
拼接字符串的功能理解为运算符重载。实际上并非如此,Java并不支持运算符重载。这其实是Java提供的一种语法糖。
原理
仍然是之前的代码:
String s1 = "Hello ";
String s2 = "World";
s1 = s1 + s2;
System.out.println(s1);
我们将这段代码生成的字节码反编译,看看结果。
反编译后对应的Java代码是:
new StringBuffer().append(s1).append(s2).toString();
通过反编译结果可以看到,当我们使用+
运算符拼接字符串时,实际上会创建一个StringBuilder
对象,然后调用其append(String str)
方法来实现字符串的拼接。
2. 使用 concat
方法
仍然以之前的例子为例,你可以使用String
类中提供的concat(String str)
方法来拼接字符串,如下所示:
public class StringJointDemo {
public static void main(String[] args) {
// 使用 concat 的示例
String s1 = "Hello ";
String s2 = "World";
String s3 = s1.concat(s2);
System.out.println(s3);
}
}
原理
我们来看看concat
方法的源码,看看这个方法是如何实现的。
这段代码首先创建了一个长度等于现有字符串和待拼接字符串长度之和的字符数组,然后将两个字符串的值复制到新的字符数组中,并使用这个字符数组创建一个新的String
对象并返回。
因此,经过concat
方法处理后,实际上也是创建了一个新的String
,这也对应了我们前面提到的字符串的不可变性。
3. 使用 StringBuffer
类或 StringBuilder
类
我们刚刚知道,使用+
运算符拼接字符串的原理其实是使用了StringBuilder
类的能力。那么当然,你也可以直接使用StringBuilder
来拼接字符串。
public class StringJointDemo {
public static void main(String[] args) {
// 使用 StringBuilder 的示例
String s1 = "Hello ";
String s2 = "World";
String s3 = new StringBuilder().append(s1).append(s2).toString();
System.out.println(s3);
}
}
当然,对于如此简单的字符串拼接场景,如果你使用IDEA编写代码,它会提醒你使用+
运算符来简化代码。
StringBuffer
也有同样的方法来实现相同的效果。
s3 = new StringBuffer().append(s1).append(s2).toString();
那么,这两者有什么区别呢?❓
在解释它们的区别之前,我们先来看看StringBuffer
和StringBuilder
的实现原理。
与String
类类似,StringBuilder
类也封装了一个字符数组,定义在其父类AbstractStringBuilder
中,如下所示:
与String
不同的是,它不是final
的,因此可以被修改。
此外,字符数组中的所有位置并不一定都被使用,因为它支持delete
功能来删除指定范围内的字符。它有一个实例变量count
,表示数组中已经使用的字符数量。
我们来看看append
方法的源码:
内部逻辑不多,主要实现逻辑在父类 AbstractStringBuilder
中;
append
方法会直接将字符复制到内部的字符数组中,如果字符数组的长度不够,则会进行扩容。
StringBuffer
与StringBuilder
类似,最大的区别在于StringBuffer
是线程安全的。我们来看看StringBuffer
的append
方法。
这个方法的定义中包含了synchronized
关键字,表明这是一个线程安全的方法。
4. 使用 StringJoiner
类
StringJoiner
是java.util
包中的一个类,也可以用来拼接字符串。它允许你指定一个连接符(可选),并且可以以提供的前缀开头和以提供的后缀结尾。
在以下代码中,它展示了使用StringJoiner
进行字符串拼接的用法。
public class StringJointDemo {
public static void main(String[] args) {
StringJoiner sj = new StringJoiner(" ");
sj.add("Hello ").add("World");
System.out.println(sj);
sj = new StringJoiner(" ","","!");
sj.add("Hello ").add("World");
System.out.println(sj);
}
}
输出:
Hello World
Hello World!
值得注意的是,当我们使用StringJoiner(CharSequence delimiter)
初始化一个StringJoiner
时,这个delimiter
其实是分隔符,而不是变量string
的初始值,并且必须传入。如果你不需要分隔符,可以传入空字符串""
。
StringJoiner
类还有一个三参数的构造函数,支持定义最终字符串的前缀和后缀,即StringJoiner(CharSequence delimiter, CharSequence prefix, CharSequence suffix)
。
原理
在介绍完简单的用法后,我们来看看这个StringJoiner
的原理,看看它是如何实现的。我们主要看add
方法:
我们看到一个熟悉的家伙——StringBuilder
。没错,StringJoiner
其实是依赖于StringBuilder
来实现的。
既然我们已经有了StringBuilder
,为什么还需要StringJoiner
呢?
试想一下,如果我们有这样一个学生列表:
List<String> students = Arrays.asList("Tom", "Bob", "Victor");
如果我们想将其拼接成如下形式的字符串,你会怎么做?
Tom,Bob,Victor
如果你使用StringBuilder
进行拼接,代码如下:
public class StringJointDemo {
public static void main(String[] args) {
List<String> students = Arrays.asList("Tom", "Bob", "Victor");
StringBuilder stringBuilder = new StringBuilder();
for (String student : students) {
stringBuilder.append(student).append(",");
}
stringBuilder.deleteCharAt(stringBuilder.length() - 1);
System.out.println(stringBuilder);
}
}
实际上,如果你熟悉Stream
中的collect
操作,你可以用一行代码完成上述功能:
String studentJoint = students.stream().collect(Collectors.joining(","));
⚠️:如果你对Stream
和collect
操作不熟悉,可以参考之前的内容:Java Lambda And Stream
在上面使用的表达式中,Collectors.joining
的源码如下:
其实现原理依赖于StringJoiner
。或许你还想为最终的字符串添加前缀和后缀,返回类似以下的内容:
[Tom,Bob,Victor]
那只需要传入prefix
和suffix
即可实现效果。
String studentJoint = students.stream().collect(Collectors.joining(",", "[", "]"));
是的,我注意到了🤔。在String
类内部,有一个名为join
的方法,支持传入数组或列表作为第二个参数。
join(CharSequence delimiter, CharSequence... elements);
join(CharSequence delimiter, Iterable<? extends CharSequence> elements);
因此,你可以使用它来拼接字符串数组。对于List<String>
的拼接,如果你不需要进行任何额外操作,只想将各个元素拼接起来,那么你其实不需要使用Collectors.joining
,建议直接使用String.join
。实际上,IDEA也会提示你这样做。
Collectors.joining
通常用于对集合进行某些处理后再进行字符串拼接的场景,比如在对集合进行过滤等操作后直接进行字符串处理。
如何选择合适的拼接方式?
既然字符串拼接的方式有这么多,我们应该如何选择呢?🤔 这需要根据不同的场景进行选择。
1. 如果只是简单的字符串拼接,可以直接使用
+
。
这里的简单场景指的是我们前面提到的例子,比如"Hello World"
,字符串只是简单地拼接后直接使用,通常是在监控和跟踪的上下文中。那么为什么不使用concat
方法呢?从源码可以看出,当与空字符串""
拼接时,concat
不会生成新的对象,而+
仍然会产生新的对象,这样效率会更高。在空字符串较多的场景中,使用concat
会更合适。然而,+
运算符可以直接与字符串、数字等基本类型数据拼接,而concat
只能接收字符串。因此,+
运算符更加灵活,在实际开发中也更实用。2. 如果在循环中进行字符串拼接,可以考虑使用
StringBuilder
和StringBuffer
。
为什么不建议在循环中使用+
进行拼接呢?我们来看看这个案例:
public class StringJointDemo {
public static void main(String[] args) {
String str = "Test loop:";
for (int i = 0; i < 100; i++) {
String s = String.valueOf(i);
str += s;
}
System.out.println(str);
}
}
这样的代码在反编译后实际上等同于:
public class StringJointDemo {
public static void main(String[] args) {
String str = "Test loop:";
for (int i = 0; i < 100; i++) {
String s = String.valueOf(i);
str = (new StringBuilder()).append(str).append(s).toString();
}
System.out.println(str);
}
}
我们可以看到,在反编译后的代码中,在for
循环中,每次append
之前都会创建一个StringBuilder
。
频繁创建新对象不仅耗时,还会造成内存资源的浪费。因此,对于循环内的字符串拼接,建议直接使用StringBuilder
的append
方法,而不是+
操作。
3. 如果是对
List
进行字符串拼接,可以考虑使用StringJoiner
或其衍生的方法。
这样做的目的前面已经提到过,主要是为了简化代码。