黑马程序员技术交流社区

标题: String、StringBuffer和StringBuilder的连接性能 [打印本页]

作者: 陈泽鑫    时间: 2012-12-8 18:05
标题: String、StringBuffer和StringBuilder的连接性能
本帖最后由 陈泽鑫 于 2012-12-11 16:29 编辑

(由于字数限制,删截很多,个人觉得认识String、StringBuffer和StringBuilder很有帮助)
首先,需要辟谣,有些人说SB(StringBuffer和StringBuilder)总是比String.concat()有更好的性能。这一说法是不准确的!在特定条件下String.concat()要胜过SB。我们来通过一个例子证明这一点。

任务:
      连接两个String,
       String a = "abcdefghijklmnopq"; //length=17
       String b = "abcdefghijklmnopqr"; //length=18

说明:
      我们将要来分析一下不同连接方案的垃圾生产情况。讨论中我们将忽略由输入参数引起的垃圾,因为他们不是由连接代码创建的。另外我们只计算String内部的char[],因为除了这个字符数组String的其它域都非常小,完全可以忽略他们对GC的影响。

方案1:
      使用String.concat()

代码:
       String result = a.concat(b);
      这行代码简单到不能再简单了,不过还是让我们来看看Sun JDK java.lang.String的源代码,搞清楚这个调用究竟是怎样进行的。
Sun JDK java.lang.String的源代码片段:
1     public String concat(String str) {
2         int otherLen = str.length();
3         if (otherLen == 0) {
4             return this;
5         }
6         char buf[] = new char[count + otherLen];
7         getChars(0, count, buf, 0);
8         str.getChars(0, otherLen, buf, count);
9         return new String(0, count + otherLen, buf);
10     }
11
12     String(int offset, int count, char value[]) {
13         this.value = value;
14         this.offset = offset;
15         this.count = count;
16     }
      这段代码首先创建一个新的char[],数组长度为a.length() + b.length(),然后分别将a和b的内容拷贝到新数组中,最后使用这个数组创建一个新的String对象。这里我们要特殊注意一下使用的构造函数, 这个构造函数只有package访问权限,它直接使用传入的char[]作为新生成的String的内部字符数组,而没有做任何拷贝保护。这个构造函数必 须是package级别的访问权限,否则你就能用它创建出一个可变的String对象(在构造完String后修改传入的char[])。JDK在 java.lang中的代码保证不会在调用这一构造函数后再修改传入的数组,加上java的安全机制不允许第三方代码加入java.lang包(你可以尝 试将自己的类放入java.lang包,此类将无法成功加载),所以String的不可变性不会被破坏。

      整个过程我们没有创建任何垃圾对象(我们有言在先,a和b是传入参数,不是连接代码创建的,所以即使他们变成垃圾我们也不去计算),所以一切良好!

方案2:
      使用SB.append(), 这里我使用StringBuilder来进行分析,对于StringBuffer也是完全一样的。

代码:
      String result = new StringBuilder().append(a).append(b).toString();
      这行代码明显比String.concat()方案的代码复杂,但它的性能如何呢?让我们分4步来分析它new StringBuilder(),append(a),append(b)和toString().
      1)new StringBuilder().
      让我们来看看StringBuilder的源代码:
1     public StringBuilder() {
2         super(16);
3     }
4
5     AbstractStringBuilder(int capacity) {
6         value = new char[capacity];
7     }
      它创建了一个大小为16的char[],目前为止还没有创建任何垃圾对象。
      2)append(a).
      继续看源代码:
1     public StringBuilder append(String str) {
2         super.append(str);
3         return this;
4     }
5     public AbstractStringBuilder append(String str) {
6         if (str == null) str = "null";
7         int len = str.length();
8         if (len == 0) return this;
9         int newCount = count + len;
10         if (newCount > value.length)
11             expandCapacity(newCount);
12         str.getChars(0, len, value, count);
13         count = newCount;
14         return this;
15     }
16     void expandCapacity(int minimumCapacity) {
17         int newCapacity = (value.length + 1) * 2;
18         if (newCapacity < 0) {
19             newCapacity = Integer.MAX_VALUE;
20         } else if (minimumCapacity > newCapacity) {
21             newCapacity = minimumCapacity;
22         }
23         value = Arrays.copyOf(value, newCapacity);
24     }
      这段代码首先确保SB的内部char[]有足够的剩余空间,这导致创建了一个新的大小为34的char[],而之前的大小为16的char[]成为垃圾对象。标记点1,我们创建了第一个垃圾对象,大小为16个char。
      3)append(b).
      相同的逻辑,首先确保内部char[]有足够的剩余空间,这导致创建了一个新的大小为70的char[],而之前的大小为34的char[]成为垃圾对象。标记点2,我们创建了第二个垃圾对象,大小为34个char。
       4)toString()
      看源代码:
1 public String toString() {
2         // Create a copy, don't share the array
3         return new String(value, 0, count);
4     }
5     public String(char value[], int offset, int count) {
6         if (offset < 0) {
7             throw new StringIndexOutOfBoundsException(offset);
8         }
9         if (count < 0) {
10             throw new StringIndexOutOfBoundsException(count);
11         }
12         // Note: offset or count might be near -1>>>1.
13         if (offset > value.length - count) {
14             throw new StringIndexOutOfBoundsException(offset + count);
15         }
16         this.offset = 0;
17         this.count = count;
18         this.value = Arrays.copyOfRange(value, offset, offset+count);
19     }
      要重点注意一下这次的构造函数,它有public访问权限,所以它必须做拷贝保护,不然就有可能破坏String的不可变性。但这又创建了一个垃圾对象。标记点3,我们创建了第三个垃圾对象,大小为70个char。

      因此我们一共创建了3个垃圾对象,总大小为16+34+70=120个char! Java使用Unicode-16编码,这就意味着240byte的垃圾!

      有一件事情能够改善SB的性能,把代码改为:
    String result = new StringBuilder(a.length() + b.length()).append(a).append(b).toString();
      自己算一下吧,这次我们只创建了1个垃圾对象,大小为17+18=35个char,还是不怎么样,不是吗?

        所以当你要连接多于3个String时(不含3),我们应该使用SB,对吗?

      不全对!

      SB有一个天生固有的毛病,它使用一个可以动态增长的内部char[]来追加新的String,当你追加新String且SB达到了内部容量上限时,它就 必须扩大内部缓冲区。之后SB获得了一个更大的char[],而之前使用的char[]则变为了垃圾。如果我们能够精确的告诉SB最终的结果有多长,它就 可以省掉许多由无谓的增长产生的垃圾。但想要预测最终结果的长度并不容易!
   
     接下来我要介绍的StringBundler就是基于这一原理工作的。

1     public StringBundler() {
2         _array = new String[_DEFAULT_ARRAY_CAPACITY]; // _DEFAULT_ARRAY_CAPACITY = 16
3     }
4
5     public StringBundler(int arrayCapacity) {
6         if (arrayCapacity <= 0) {
7             throw new IllegalArgumentException();
8         }
9         _array = new String[arrayCapacity];
10     }
11

      第一个构造函数会创建一个默认数组大小为16的StringBundler,第二个构造函数允许你指定一个初始容量。每当你调用append()时,你并没有真正的执行String连接操作,而是将该String放置到缓存数组中。
1     public StringBundler append(String s) {
2         if (s == null) {
3             s = StringPool.NULL;
4         }
5         if (_arrayIndex >= _array.length) {
6             expandCapacity();
7         }
8         _array[_arrayIndex++] = s;
9         return this;
10     }
11
      如果你追加的String数量超过了缓存数组容量,内部的String[]会动态增长。
1     protected void expandCapacity() {
2         String[] newArray = new String[_array.length << 1];
3         System.arraycopy(_array, 0, newArray, 0, _array.length);
4         _array = newArray;
5     }
6

      扩充一个String[]要比扩充char[]便宜的多。因为String[]比较小,而且增长的频度要远比原来的char[]低。
      当你完成了全部追加后,调用toString()来获取最终结果。
1     public String toString() {
2         if (_arrayIndex == 0) {
3             return StringPool.BLANK;
4         }
5         String s = null;
6         if (_arrayIndex <= 3) {
7             s = _array[0];
8             for (int i = 1; i < _arrayIndex; i++) {
9                 s = s.concat(_array);
10             }
11         }
12         else {
13             int length = 0;
14             for (int i = 0; i < _arrayIndex; i++) {
15                 length += _array.length();
16             }
17             StringBuilder sb = new StringBuilder(length);
18             for (int i = 0; i < _arrayIndex; i++) {
19                 sb.append(_array);
20             }
21             s = sb.toString();
22         }
23         return s;
24     }
25
当你连接2或3个String时,使用String.concat()。
    如果你要连接多于3个String(不含3),并且你能够精确预测出最终结果的长度,使用StringBuilder/StringBuffer,并设定初始化容量。
    如果你要连接多于3个String(不含3),并且你不能够精确预测出最终结果的长度,使用StringBundler。
    如果你使用StringBundler,并且你能预测出要连接的String数量,使用指定初始化容量的构造函数。





欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2