Java字符串常用的5种拼接方法

时间:2025-02-21 08:30:54

String +=

  1. 加号拼接,以a=“a”; str = “a”+"b"为例,将字节码反编译,得到以下效果代码:
    String a = “a”;
    StringBuilder sb = new StringBuilder();
    (a).append(“b”);
    String str = ();
  2. JVM对于“+”处理过程:
    首先创建一个String对象a,并把"a"赋值给a,然后在第2行中,JVM通过创建一个新的StringBuilder对象sb,并在第3行通过sb的append方法将字符串"a"和字符串"b"进行拼接,第4行sb调用toString方法转换成字符串,然后把结果赋给新的String对象str。
  3. 其中,String对象a并没有被修改(String对象一旦创建之后就不可更改),只是会被JVM的垃圾回收机制(GC)给回收掉。字符串拼接的结果是一个新的String对象str,new出了一个新的内存。而且拼接的过程,还new了一个新的StringBuilder对象,调用其toString()方法。
  4. 所以,在循环内部执行+=,每执行一次循环,都会创建一个新的String对象,一个StringBuilder对象,然后GC不断回收,所以时间、空间开销都很大。
  5. 当然,在编译阶段就能够确定的字符串常量,完全没有必要创建String或StringBuffer对象。直接使用字符串常量的"+"连接操作效率最高(如:String str = “a” + “b” + “c”)。此处 “a” , “b” ,“c”常量完全确定,不用通过循环之类的获取。

StringBuilder的apend

StringBuffer 和 StringBuilder 的append方法都继承自AbstractStringBuilder,整个逻辑都只做字符数组的加长,拷贝,不会创建新的String对象,但会创建新的字符数组:扩容数组属于一个新的数组,原来的数组成为垃圾对象并被GC回收。
StringBuilder源码(jdk1.8):
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = ();
ensureCapacityInternal(count + len);
(0, len, value, count);
count += len;
return this;
}
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > ) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
//将拼接的字符串复制到扩容后的数组中
// value:需要拼接的字符串
// srcBegin:需要拼接的字符串的起始位置,等于0
// dst:目标数组,保存有原来的数据
// dstBegin:目标数组开始放置的位置,等于原来数据的长度
// srcEnd – srcBegin:拼接的字符串的长度,拼接到原来数据的后面
(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
private AbstractStringBuilder appendNull() {
int c = count;
ensureCapacityInternal(c + 4);
final char[] value = ;
value[c++] = ‘n’;
value[c++] = ‘u’;
value[c++] = ‘l’;
value[c++] = ‘l’;
count = c;
return this;
}
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - > 0) {
value = (value,
newCapacity(minimumCapacity));
}
}
:new StringBuilder()时容量初始化=16。
新添加的数据长度和已有数据长度的和小于初始化容量时,不扩容,否则扩容。
Arrays的copyOf()方法传回的数组是新的数组对象,新数组含原数组的元素值,且不会影响原来的数组。
copyOf()的第二个自变量指定要建立的新数组长度,如果新数组的长度超过原数组的长度,则保留数组默认值。
综上两点,说明产生了垃圾对象,扩容一次产生一个垃圾对象。
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = ( << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
private int hugeCapacity(int minCapacity) {
if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
throw new OutOfMemoryError();
}
return (minCapacity > MAX_ARRAY_SIZE)
? minCapacity : MAX_ARRAY_SIZE;
}
( << 1) + 2:两倍加2。
扩容:(1)如果minCapacity(新添加的数据长度和已有数据长度的和)大于当前容量Capacity的2倍加2,则添加数据后容量Capacity变为新添加的数据长度和已有数据长度的和;(2)如果minCapacity(新添加的数据长度和已有数据长度的和)小于等于当前容量Capacity的2倍加2,则添加数据后容量Capacity变为上次容量的2倍加2。
所以,如果能够预先评估StringBuilder或StringBuffer的大小,将能够有效的节省这些操作,从而提高系统的性能。

StringBuffer的apend

public synchronized StringBuffer append(String str) {
toStringCache = null;
(str);
return this;
}
StringBuffer和StringBuilder在功能上是完全相同的,唯一的不同点在于StringBuffer是线程安全的,而StringBuilder不是。绝大多数的String连接操作发生在一个方法调用中,也就是说是单一线程的工作环境,因而线程安全在这里是绝对多余的。所以JDK给开发者的建议是当你要做String连接操作时,请使用StringBuffer或StringBuilder,当你确定连接操作只发生在单一线程环境下时,使用StringBuilder而不是StringBuffer。因为加锁和解锁或者等待都消耗了时间。

String的concat

  1. 源码:
    public String concat(String str) {
    int otherLen = ();
    if (otherLen == 0) {
    return this;
    }
    int len = ;
    char buf[] = (value, len + otherLen);
    (buf, len);
    return new String(buf, true);
    }
  2. 分析:
  3. value为String内部维护的一个字符数组,用来存储String的值。
  4. 判断需要拼接的字符串是否长度为0,若为0则返回原字符串。
  5. 使用方法将value复制到buf,且buf长度扩充为原字符串和新字符串长度之和。 此处产生一个垃圾对象,即原数组。
  6. 使用String的getChars方法将str的字符全部复制到buf索引len(即原字符串之后),完成拼接。
  7. 创建一个新String对象,将buf转化返回。
    创建一个新String对象,限制了concat方法的速度,且造成空间浪费。所以对大批量数据做处理时,contact和加号“+”一样绝对不能用,因为时间和空间成本都很高。

StringUtils的join

用于将数组元素用某个字符串拼接起来。
与String +=类似,也是使用了StringBuilder来进行操作,但是其中还有很多其他操作,所以耗时比较长:产生新的String对象,新的StringBuilder对象,还有数组的循环,分隔符判断。
public static String join(final Iterable<?> iterable, final String separator) {
if (iterable == null) {
return null;
}
return join((), separator);
}
public static String join(final Iterator<?> iterator, final String separator) {
// handle null, zero and one elements before building a buffer
if (iterator == null) {
return null;
}
if (!()) {
return EMPTY;
}
final Object first = ();
if (!()) {
return (first, “”);
}
// two or more elements
final StringBuilder buf = new StringBuilder(STRING_BUILDER_SIZE); // Java default is 16, probably too small
if (first != null) {
(first);
}
while (()) {
if (separator != null) {
(separator);
}
final Object obj = ();
if (obj != null) {
(obj);
}
}
return ();
}

性能分析:

的join和用“+”号连接字符串都是最慢的 ,StringUtils的join比"+"还慢。
2.在拼接少数字符串(不超过4个)的时候,concat效率是最高的 。
3.多个字符串拼接的时候,StringBuilder/StringBuffer的效率是碾压的。
4.在不需要考虑线程安全问题的时候,使用StringBuilder的效率比StringBuffer更高。
Concat和StringBuilder/StringBuffer的比较:
(1)Concat产生新对象String,每一次都进行扩容,每一次扩容都产生垃圾对象,即原数组。同时还生成一个新的String对象。
(2)StringBuilder不是每次都需要扩容,但扩容一次也需要产生垃圾对象。StringBuilder不需要产生行的String对象,但有更多的方法调用,因而对应的栈操作有时间上的耗时。