1.String的编译时优化方案
首先看以下代码的输出:
String a = "a" + "b" + "c"; String b = "abc"; System.out.println(a == b);
以上代码的输出为true。原因是当编译器在执行String a = "a" + "b" + "c"时,会将其编译成String a = "abc";编译器认为3个常量相加的会得到固定的常量值,无需等到运行时再进行计算。
String a = "a"; final String b = "a"; String aa = a + "c"; String bb = b + "c"; String c = "ac"; System.out.println(aa == c); System.out.println(bb == c); ------------------------------------------------------------------------------------------- false true
以上的代码在编译时,因为b是final类型,所以在编译String bb = b + "c"时,编译器执行了编译优化,直接编译成String bb ="ac"。而a的值在上述代码中虽然没有发生变化,但编译器并不会先跟踪查看a的值是否发生过变化。
2.intern()/equals()
String a = "a"; String b = a + "b"; String c = "ab"; System.out.println(b == c); //false System.out.println(b.intern() == c); //true
intern()方法是一个native方法,该方法的底层不是由java语言实现。关于native关键字的详细介绍,将在后续博客中给出。调用intern()方法时,JVM会在常量池中调用equals()方法查看是否有值相等的String,如果存在则直接返回该String对象的地址;如果没有找到,先创建等值的字符串,再返回新建字符串的地址。可以看出intern()方法需要比较多个字符串,而且为了保证唯一性,需要有锁的介入。
JDK1.7中equals()代码如下:
private final char value[]; public boolean equals(Object anObject) { if (this == anObject) { //传入对象是否为当前对象 return true; } if (anObject instanceof String) { //是否为字符串 String anotherString = (String) anObject; int n = value.length; if (n == anotherString.value.length) { //比较两个字符串的长度 char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
如果两个字符串匹配上,则需要遍历两个char数组。
3.StringBuilder.append()与"+"
通过"+"进行拼接时,如果拼接的是常量,则会在编译时进行优化,不需要在运行时再分配空间,这一点是append()操作做不到的。
对于运行时拼接的情况,如将"+"与append()操作放在循环中执行时:
编译前的代码 String a = ""; for(){ a += "aaa"; //拼接随机字符串 } 编译后的代码 String a = ""; for(){ StringBuilder tmp = new StrngBuilder(); tmp.append(a).append("aaa"); a = tmp.toString(); }
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
可以看到,对于"+"的操作,编译器在编译期间将其转成append()来完成字符串拼接,所以如果在循环中使用"+"造作,会在循环体内部创建出许多StringBuilder对象,同时调用toStriing()方法时会创建新的String对象,这些临时对象会占用大量内存,导致频繁的GC。
循环拼接操作,当字符串a达到一定程度之后会进入old区域。对于"+"操作,当a达到old区域的1/4时会发生OOM;而对于append(),当a达到old区域的1/3时会发生OOM。
首先来看JDK1.7中StringBuilder类append()的源码:
public StringBuilder append(String str) { super.append(str); return this; } public AbstractStringBuilder append(String str) { if (str == null) str = "null"; int len = str.length(); ensureCapacityInternal(count + len); //count为当前StringBuilder对象中的有效元素的个数 str.getChars(0, len, value, count); count += len; return this; } private void ensureCapacityInternal(int minimumCapacity) { if (minimumCapacity - value.length > 0) expandCapacity(minimumCapacity); } void expandCapacity(int minimumCapacity) { //以“count + 新字符串长度”和"char[]中长度的2倍"相比较,取较大值进行扩容 int newCapacity = value.length * 2 + 2; if (newCapacity - minimumCapacity < 0) newCapacity = minimumCapacity; if (newCapacity < 0) { if (minimumCapacity < 0) // overflow throw new OutOfMemoryError(); newCapacity = Integer.MAX_VALUE; } value = Arrays.copyOf(value, newCapacity); }
StringBuilder对象内部先分配16个长度的char[]数组,当发生append()操作时继续向数组后添加元素;若空间不够时则尝试扩容,扩容的规则如上述代码注释中所述。
对于"+"操作,当a对象达到old区域的1/4时,会先分配一个StringBuilder对象,初试的char[]数组长度为16,首先进行tmp.append(a)操作,空间不够大需要进行扩容,此时count=0,扩容的长度为"a的长度+count",扩容完成后存储拼接a,而且当a拼接完成时,StringBuilder已经没有空余的char[]空间了,而且此时原本的a对象还没有释放,所以此时占用了old区域的1/4+1/4=1/2的空间。
然后开始进行append("aaa")的操作,如上所述,此时StringBuilder已经没有空余的char[]空间了,需要进行再次扩容,"3(aaa的长度)+count"<char[]总长度的2倍,所以此次扩容是2倍扩容。扩容后的StringBuilder的char[]数组的空间是1/4*2=1/2,所以old空间的剩余一半也被用掉了。扩容操作完成后,扩容前的char[]数组空间才会被释放,如果append()的随机字符串是""空字符串,则拼接前的对象长度达到old空间的1/3时发生OOM。在执行toString()方法时,前面的扩容已经成功,所以扩容前的char[]数组已经被释放。
再看看在循环中直接使用append()操作的情况:
StringBuilder tmp = new StrngBuilder(); for(){ tmp.append("随机字符串"); }
在这段代码中,在第一次进行拼接时,StringBuilder首先也是分配16个长度的char[]数组,在扩容时,因为"count+随机字符串的长度(值较小)"一般都小于char[]数组的长度*2,所以都是进行2倍扩容,而且进行二倍扩容之后,存储随机字符串之后仍有一般左右的空余空间,所以能使用一段时间之后才会再次扩容,而且不会发生申请一个大的StringBuilder对象并很快将它当成垃圾的情况。这种擦偶偶在字符串达到old区域的1/3时会发生OOM。
与String的"+"不同的是,在差几个字节导致OOM时,String拼接在下一次拼接随机字符串时必然会发生OOM,但append()操作扩容后会经历很长一段时间才发生OOM。另外append()操作产生的垃圾都是小块的内存,主要是拼接的对象以及扩容时原来的空间。所以在这种场景下StringBuilder对象拼接字符串的效率会高出很多倍。
关于String的"+"的补充说明:String str = a + b + c + d;这行代码只会申请一个StringBuilder并执行多个append()操作。