前言
本文参考网上的一些博文,结合自身的开发经验,整理出一些字符串拼接的使用建议。
版本约定
- JDK 版本:1.8.0_231
- Java SE API Documentation:https://docs.oracle.com/javase/8/docs/api/
正文
字符串不变性与字符串拼接
在字符串第一篇中,我们知道字符串是不可变的。所以字符串的拼接,都是重新生成一个新的字符串。比如下面这段字符串拼接代码。
public static void main(String[] args) {
String s = "abcd";
s = s.concat("ef");
}
其实最后我们得到的 s 已经是一个新的字符串了。如下图:
s 中保存的是一个重新创建出来的 String 对象的引用。
符号 ‘+’
在 Java 中,拼接字符串最简单的方式就是直接使用符号 ‘+’ 来拼接。
public static void main(String[] args) {
String message = "User's age: ";
int age = 18;
System.out.println(message + age);
}
运行程序,输出:
User's age: 18
‘+’ 号按照给定的次序将两个字符串拼接起来。我们注意到,当将一个字符串与一个非字符串的值进行拼接时,后者会被转换成字符串。
在 Java 中,任何一个 Java 对象都可以转换成字符串,默认会调用 Java 对象的 toString() 方法生成 Java 对象的字符串。
一些简单的场景,或者说是不涉及到遍历生成字符串的场景都可以使用符号 ‘+’ 来拼接字符串。
实现原理
符号 ‘+’ 是 Java 提供的语法糖,我们不知道它是怎么实现的。只能先把 .java 文件编译成 .class 文件,然后通过反编译工具 Jad 反编译生成的 .class 文件,查看字节码文件中真正的实现。
public static void main(String args[])
{
String message = "User's age: ";
int age = 18;
System.out.println((new StringBuilder()).append(message).append(age).toString());
}
concat
除了使用符号 ‘+’ 拼接字符串之外,还可以使用 String 类中 concat 方法来拼接字符串。
public static void main(String[] args) {
String message = "User's age: ";
int age = 18;
System.out.println(message.concat(age + ""));
}
运行程序,输出:
User's age: 18
实现原理
我们通过 concat 的源码了解一下它的实现原理。
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
首先创建了一个字符数组,长度是已有字符串和待拼接字符串的长度之和,再把两个字符串的值复制到新的字符数组中,并使用这个字符数组创建一个新的 String 对象并返回。
StringBuilder & StringBuffer
使用 StringBuilder 可以方便的对字符串进行拼接。
public static void main(String[] args) {
String message = "User's age: ";
int age = 18;
StringBuilder sb = new StringBuilder();
sb.append(message).append(age);
System.out.println(sb.toString());
}
运行程序,输出:
User's age: 18
StringBuffer 的用法和 StringBuilder 类似,StringBuffer 是线程安全的,性能对比 StringBuilder,要差点,一般我们也不会使用。
实现原理
接下来我们看看 StringBuilder 和 StringBuffer 的实现原理。
StringBuilder 继承了 AbstractStringBuilder,和 String 类类似,AbstractStringBuilder 类也封装了一个字符数组,定义如下:
/**
* The value is used for character storage.
*/
char[] value;
与 String 不同的是,它并不是 final 的,所以它是可以修改的。另外,它还提供了一个实例变量,用来记录数组中已经使用的字符个数。
/**
* The count is the number of characters used.
*/
int count;
StringBuilder 的 append 方法重写了父类的方法,并且调用了父类的 append 方法。
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
AbstractStringBuilder 的 append 方法如下所示:
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
append 方法内部还是通过字符数组拷贝实现的,如果字符数组长度不够,通过 ensureCapacityInternal 方法进行扩容。
StringBuffer 和 StringBuilder 类似,最大的区别就是 StringBuffer 是线程安全的,看一下 StringBuffer 的 append 方法。
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
该方法使用 synchronized 进行声明,说明是一个线程安全的方法。而 StringBuilder 则不是线程安全的。
String.join
Java 8 的 String 类中提供了一个静态方法:join,使用方式如下。
public static void main(String[] args) {
String message = String.join("-", "Java", "is", "cool");
System.out.println(message);
}
运行程序,输出:
Java-is-cool
该方法比较适合用来将字符串数组以固定的拼接符拼接到一起形成新的字符串。
实现原理
通过查看 String.join 方法的源码,我们发现,它是通过 StringJoiner 类实现的。
public static String join(CharSequence delimiter, CharSequence... elements) {
Objects.requireNonNull(delimiter);
Objects.requireNonNull(elements);
// Number of elements not likely worth Arrays.stream overhead.
StringJoiner joiner = new StringJoiner(delimiter);
for (CharSequence cs: elements) {
joiner.add(cs);
}
return joiner.toString();
}
通过查看 StringJoiner 类的源码,它内部也是通过 StringBuilder 实现,受限于文章篇幅,这里就不列出来了。
StringJoiner 只是对 StringBuilder 的进一步封装,适合用来构造一个由固定分隔符分隔的字符序列,并且可以非常方便的添加前缀和后缀。
StringUtils.join
StringUtils 是 Apache Commons 中提供的字符串工具类,其中的静态方法 join 可以用来拼接字符串,用法和 String.join 方法类似。
public static void main(String[] args) {
String[] arr = {"Java", "is", "cool"};
String message = StringUtils.join(arr, "-");
System.out.println(message);
}
运行程序,输出:
Java-is-cool
该方法也适合用来将字符串数组以固定的拼接符拼接到一起形成新的字符串。
实现原理
通过查看 StringUtils.join 的源代码,我们可以发现,其实它也是通过 StringBuilder 实现的。
public static String join(final Object[] array, String separator, final int startIndex, final int endIndex) {
if (array == null) {
return null;
}
if (separator == null) {
separator = EMPTY;
}
// endIndex - startIndex > 0: Len = NofStrings *(len(firstString) + len(separator))
// (Assuming that all Strings are roughly equally long)
final int noOfItems = endIndex - startIndex;
if (noOfItems <= 0) {
return EMPTY;
}
final StringBuilder buf = new StringBuilder(noOfItems * 16);
for (int i = startIndex; i < endIndex; i++) {
if (i > startIndex) {
buf.append(separator);
}
if (array[i] != null) {
buf.append(array[i]);
}
}
return buf.toString();
}
效率比较
既然有这么多种字符串拼接的方法,那么到底哪一种效率最高呢?我们来简单对比一下。
public class Test14 {
public static void main(String[] args) {
testPlus();
testConcat();
testStringBuilder();
testStringBuffer();
testStringUtilsJoin();
}
public static void testPlus() {
long t1 = System.currentTimeMillis();
String str = "start";
for (int i = 0; i < 50000; i++) {
str = str + "hello";
}
long t2 = System.currentTimeMillis();
System.out.println("+: " + (t2 - t1));
}
public static void testConcat() {
long t1 = System.currentTimeMillis();
String str = "start";
for (int i = 0; i < 50000; i++) {
str = str.concat("hello");
}
long t2 = System.currentTimeMillis();
System.out.println("Concat: " + (t2 - t1));
}
public static void testStringBuilder() {
long t1 = System.currentTimeMillis();
StringBuilder sb = new StringBuilder("start");
for (int i = 0; i < 50000; i++) {
sb.append("hello");
}
long t2 = System.currentTimeMillis();
System.out.println("StringBuilder: " + (t2 - t1));
}
public static void testStringBuffer() {
long t1 = System.currentTimeMillis();
StringBuffer sb = new StringBuffer("start");
for (int i = 0; i < 50000; i++) {
sb.append("hello");
}
long t2 = System.currentTimeMillis();
System.out.println("StringBuffer: " + (t2 - t1));
}
public static void testStringUtilsJoin() {
long t1 = System.currentTimeMillis();
String str = "start";
for (int i = 0; i < 50000; i++) {
str = StringUtils.join(str, "hello");
}
long t2 = System.currentTimeMillis();
System.out.println("StringUtilsJoin: " + (t2 - t1));
}
}
运行程序,输出:
+: 5589
Concat: 1634
StringBuilder: 1
StringBuffer: 2
StringUtilsJoin: 5152
从结果可以看出,用时从短到长的对比是:
StringBuilder < StringBuffer < concat < StringUtilsJoin < +
那么问题来了,前面我们分析过,其实使用符号 ‘+’ 拼接字符串的实现原理也是使用的 StringBuilder,那为什么结果相差这么多呢?
我们再把以下代码反编译下:
public static void testPlus() {
long t1 = System.currentTimeMillis();
String str = "start";
for (int i = 0; i < 50000; i++) {
str = str + "hello";
}
long t2 = System.currentTimeMillis();
System.out.println("+: " + (t2 - t1));
}
反编译后代码如下:
public static void testPlus()
{
long t1 = System.currentTimeMillis();
String str = "start";
for(int i = 0; i < 50000; i++)
str = (new StringBuilder()).append(str).append("hello").toString();
long t2 = System.currentTimeMillis();
System.out.println((new StringBuilder()).append("+: ").append(t2 - t1).toString());
}
我们可以看到,反编译后的代码,在 for 循环中,每次都是 new 了一个 StringBuilder,然后再把 String 转成StringBuilder,再进行 append。
而频繁的新建对象当然要耗费很多时间了,不仅仅会耗费时间,频繁的创建对象,还会造成内存资源的浪费。
所以,阿里巴巴 Java 开发手册建议:循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展,而不要使用符号 ‘+’。
总结
本文介绍了什么是字符串拼接,虽然字符串是不可变的,但是还是可以通过新建字符串的方式来进行字符串的拼接。
常用的字符串拼接方式有六种,分别是使用 ‘+’、concat、StringBuilder、StringBuffer、String.join 以及使用 StringUtils.join。
由于字符串拼接过程中会创建新的对象,所以如果要在一个循环体中进行字符串拼接,就要考虑内存问题和效率问题。
因此,经过对比,我们发现,直接使用 StringBuilder 的方式是效率最高的。因为 StringBuilder 天生就是设计来定义可变字符串和字符串的变化操作的。
但是,还要强调的是:
- 如果只是简单的字符串拼接,考虑直接使用 ‘+’ 即可;
- 如果是在 for 循环中进行字符串拼接,考虑使用 StringBuilder;
- 如果是通过一个 List 进行字符串拼接,则考虑使用 StringJoiner or String.join or StringUtils.join;
- 如果在并发场景中进行字符串拼接的话,要使用StringBuffer来代替StringBuilder。
参考
作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/awh99y 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。