“+”号操作符

  1. String chenmo = "沉默";
  2. String wanger = "王二";
  3. System.out.println(chenmo + wanger);

反编译一下:

  1. String chenmo = "\u6C89\u9ED8"; // 沉默
  2. String wanger = "\u738B\u4E8C"; // 王二
  3. System.out.println((new StringBuilder(String.valueOf(chenmo))).append(wanger).toString());

原来编译的时候把“+”号操作符替换成了 StringBuilder 的 append 方法。也就是说,“+”号操作符在拼接字符串的时候只是一种形式主义,让开发者使用起来比较简便,代码看起来比较简洁,读起来比较顺畅。
每次使用“+”号的时候都会新建一个StringBuilder对象,还需要调用toString()方法,所以性能堪忧。

StringBuilder

先来看一下 StringBuilder 类的 append 方法的源码:

  1. public StringBuilder append(String str) {
  2. super.append(str);
  3. return this;
  4. }

这 3 行代码没啥可看的,可看的是父类 AbstractStringBuilder 的 append 方法:

  1. public AbstractStringBuilder append(String str) {
  2. if (str == null)return appendNull();
  3. int len = str.length();
  4. ensureCapacityInternal(count + len);
  5. str.getChars(0, len, value, count);
  6. count += len;
  7. return this;
  8. }

1)判断拼接的字符串是不是 null,如果是,当做字符串“null”来处理。appendNull 方法的源码如下:

  1. private AbstractStringBuilder appendNull() {
  2. int c = count;
  3. ensureCapacityInternal(c + 4);
  4. final char[] value = this.value;
  5. value[c++] = 'n';
  6. value[c++] = 'u';
  7. value[c++] = 'l';
  8. value[c++] = 'l';
  9. count = c;
  10. return this;
  11. }

2)拼接后的字符数组长度是否超过当前值,如果超过,进行扩容并复制。ensureCapacityInternal 方法的源码如下:

  1. private void ensureCapacityInternal(int minimumCapacity) {
  2. // overflow-conscious code
  3. if (minimumCapacity - value.length > 0) {
  4. value = Arrays.copyOf(value,
  5. newCapacity(minimumCapacity));
  6. }
  7. }

3)将拼接的字符串 str 复制到目标数组 value 中。

  1. str.getChars(0, len, value, count)

StringBuffer

  1. public synchronized StringBuffer append(String str) {
  2. toStringCache = null;
  3. super.append(str);
  4. return this;
  5. }

StringBuffer 类的 append 方法比 StringBuilder 多了一个关键字 synchronized

String 类的 concat 方法

  1. public String concat(String str) {
  2. int otherLen = str.length();
  3. if (otherLen == 0) {
  4. return this;
  5. }
  6. int len = value.length;
  7. char buf[] = Arrays.copyOf(value, len + otherLen);
  8. str.getChars(buf, len);
  9. return new String(buf, true);
  10. }

1)如果拼接的字符串的长度为 0,那么返回拼接前的字符串。

  1. if (otherLen == 0) {
  2. return this;
  3. }

2)将原字符串的字符数组 value 复制到变量 buf 数组中。

  1. char buf[] = Arrays.copyOf(value, len + otherLen);

3)把拼接的字符串 str 复制到字符数组 buf 中,并返回新的字符串对象。

  1. str.getChars(buf, len);
  2. return new String(buf, true);

通过源码分析我们大致可以得出以下结论:
1)如果拼接的字符串是 null,concat 时候就会抛出 NullPointerException,“+”号操作符会当做是“null”字符串来处理。
2)如果拼接的字符串是一个空字符串(””),那么 concat 的效率要更高一点。毕竟不需要 new StringBuilder 对象。
3)如果拼接的字符串非常多,concat 的效率就会下降,因为创建的字符串对象越多,开销就越大。

String 类的 join 方法

  1. String chenmo = "沉默";
  2. String wanger = "王二";
  3. String cmower = String.join("", chenmo, wanger);
  4. System.out.println(cmower);
  5. //第一个参数为字符串连接符,比如说:
  6. String message = String.join("-", "王二", "太特么", "有趣了");
  7. //输出结果为:王二-太特么-有趣了

我们来看一下 join 方法的源码:

  1. public static String join(CharSequence delimiter, CharSequence... elements) {
  2. Objects.requireNonNull(delimiter);
  3. Objects.requireNonNull(elements);
  4. // Number of elements not likely worth Arrays.stream overhead.
  5. StringJoiner joiner = new StringJoiner(delimiter);
  6. for (CharSequence cs: elements) {
  7. joiner.add(cs);
  8. }
  9. return joiner.toString();
  10. }

发现了一个新类 StringJoiner,类名看起来很 6,读起来也很顺口。StringJoiner 是 java.util 包中的一个类,用于构造一个由分隔符重新连接的字符序列。

StringUtils.join

  1. String chenmo = "沉默";
  2. String wanger = "王二";
  3. StringUtils.join(chenmo, wanger);

通过查看源码我们可以发现,其内部使用的仍然是 StringBuilder。

  1. public static String join(final Object[] array, String separator, final int startIndex, final int endIndex) {
  2. if (array == null) {
  3. return null;
  4. }
  5. if (separator == null) {
  6. separator = EMPTY;
  7. }
  8. final StringBuilder buf = new StringBuilder(noOfItems * 16);
  9. for (int i = startIndex; i < endIndex; i++) {
  10. if (i > startIndex) {
  11. buf.append(separator);
  12. }
  13. if (array[i] != null) {
  14. buf.append(array[i]);
  15. }
  16. }
  17. return buf.toString();
  18. }

总结

  1. 由于String对象时不可变对象,因此在需要对字符串进行修改操作时(如字符串连接和替换),String对象总是会生成新的对象,所以其性能相对较差。
  2. String变量的累加操作:底层使用了StringBuilder的功能。
  3. StringBuffer和StringBuilder的扩容策略:当字符串缓冲区容量不足时,原有容量将会加倍,以新的容量来申请内存空间,建立新的char数组,然后将原数组中的内容复制到这个新的数组当中。因此,对于大对象的扩容会涉及大量的内存复制操作。所以,如果能够预先评估StringBuilder或StringBuffer的大小,将能够有效的节省这些操作,从而提高系统的性能。
  4. StringBuilder性能比StringBuffer要好点。在1千万的循环下, StringBuilder大约在500-600毫秒,而StringBuffer大约在700-800毫秒;
  5. 如果我们不想创建StringBuffer或StringBuilder实例使,我们也因该使用concat。但是对于大量的字符串拼接操作,就不应该使用concat,因为concat会降低你程序的性能,消耗你的cpu。因此,在不考虑线程安全和同步的情况下,为了获得最高的性能,我们应尽量使用StringBuilder。
  6. 每次调用contact()方法就是一次数组的拷贝,虽然在内存中是处理都是原子性操作,速度非常快,但是,最后的return语句会创建一个新String对象,限制了concat方法的速度
  7. StringBuffer和StringBuilder的append方法都继承自AbstractStringBuilder,整个逻辑都只做字符数组的加长,拷贝,到最后也不会创建新的String对象,所以速度很快,完成拼接处理后在程序中用strBuffer.toString()来得到最终的字符串。
  8. 执行一次字符串“+”,相当于 str = new StringBuilder(str).append(“a”).toString()。