大家好,今天小编来为大家解答以下的问题,关于stringbuilder,老程序员浅谈StringBuilder这个很多人还不知道,现在让我们一起来看看吧!
在浅谈JavaString内幕(1)中,字符串变量(非final修饰)通过"+"进行拼接,在编译过程中会转化为StringBuilder对象的append操作,注意是编译过程,而不是在JVM中。
publicclassStringTest{\npublicstaticvoidmain(String[]args){\nStringstr1="hello";\nStringstr2="java";\nStringstr3=str1+str2+"!";\nStringstr4=newStringBuilder().append(str1).append(str2).append("!").toString();\n}\n}\n
上述str3和str4的执行效果其实是一样的,不过在for循环中,千万不要使用"+"进行字符串拼接。
publicclasstest{\npublicstaticvoidmain(String[]args){\nrun1();\nrun2();\n}\npublicstaticvoidrun1(){\nlongstart=System.currentTimeMillis();\nStringresult="";\nfor(inti=0;i<10000;i++){\nresult+=i;\n}\nSystem.out.println(System.currentTimeMillis()-start);\n}\n\npublicstaticvoidrun2(){\nlongstart=System.currentTimeMillis();\nStringBuilderbuilder=newStringBuilder();\nfor(inti=0;i<10000;i++){\nbuilder.append(i);\n}\nSystem.out.println(System.currentTimeMillis()-start);\n}\n}\n
在for循环中使用"+"和StringBuilder进行1万次字符串拼接,耗时情况如下:
1、使用"+"拼接,平均耗时250ms;
2、使用StringBuilder拼接,平均耗时1ms;
for循环中使用"+"拼接为什么这么慢?下面是run1方法的字节码指令
5~34行对应for循环的代码,可以发现,每次循环都会重新初始化StringBuilder对象,导致性能问题的出现。
StringBuilder内部维护了一个char[]类型的value,用来保存通过append方法添加的内容,通过newStringBuilder()初始化时,char[]的默认长度为16,如果append第17个字符,会发生什么?
voidexpandCapacity(intminimumCapacity){\nintnewCapacity=value.length*2+2;\nif(newCapacity-minimumCapacity<0)\nnewCapacity=minimumCapacity;\nif(newCapacity<0){\nif(minimumCapacity<0)//overflow\nthrownewOutOfMemoryError();\nnewCapacity=Integer.MAX_VALUE;\n}\nvalue=Arrays.copyOf(value,newCapacity);\n}\n
如果value的剩余容量,无法添加全部内容,则通过expandCapacity(intminimumCapacity)方法对value进行扩容,其中minimumCapacity=原value长度+append添加的内容长度。
1、扩大容量为原来的两倍+2,为什么要+2,而不是刚好两倍?
2、如果扩容之后,还是无法添加全部内容,则将minimumCapacity作为最终的容量大小;
3、利用System.arraycopy方法对原value数据进行复制;
在使用StringBuilder时,如果给定一个合适的初始值,可以避免由于char[]数组多次复制而导致的性能问题。
不同初始容量的性能测试:
publicclassStringBuilderTest{\npublicstaticvoidmain(String[]args){\nintsum=0;\nfinalintcapacity=40000000;\nfor(inti=0;i<100;i++){\nsum+=cost(capacity);\n}\nSystem.out.println(sum/100);\n}\npublicstaticlongcost(intcapacity){\nlongstart=System.currentTimeMillis();\nStringBuilderbuilder=newStringBuilder(capacity);\nfor(inti=0;i<10000000;i++){\nbuilder.append("java");\n}\nreturnSystem.currentTimeMillis()-start;\n}\n}\n
执行一千万次append操作,不同初始容量的耗时情况如下:
1、容量为默认16时,平均耗时110ms;
2、容量为40000000时,不会发生复制操作,平均耗时85ms;
通过以上数据可以发现,性能损耗不是很严重。
1、StringBuilder内部进行扩容时,会新建一个大小为原来两倍+2的char数组,并复制原char数组到新数组,导致内存的消耗,增加GC的压力。
2、StringBuilder的toString方法,也会造成char数组的浪费。
publicStringtoString(){\n//Createacopy,don'tsharethearray\nreturnnewString(value,0,count);\n}\n
String的构造方法中,会新建一个大小相等的char数组,并使用System.arraycopy()复制StringBuilder中char数组的数据,这样StringBuilder的char数组就白白浪费了。
重用StringBuilder
publicclassStringBuilderHolder{\nprivatefinalStringBuildersb;\npublicStringBuilderHolder(intcapacity){\nsb=newStringBuilder(capacity);\n}\npublicStringBuilderresetAndGet(){\nsb.setLength(0);\nreturnsb;\n}\n}\n
通过sb.setLength(0)方法可以把char数组的内存区域设置为0,这样char数组重复使用,为了避免并发访问,可以在ThreadLocal中使用StringBuilderHolder,使用方式如下:
privatestaticfinalThreadLocal<StringBuilderHolder>stringBuilder=newThreadLocal<StringBuilderHolder>(){\n@Override\nprotectedStringBuilderHolderinitialValue(){\nreturnnewStringBuilderHolder(256);\n}\n};\n\nStringBuildersb=stringBuilder.get().resetAndGet();\n
不过这种方式也存在一个问题,该StringBuilder实例的内存空间一直不会被GC回收,如果char数组在某次操作中被扩容到一个很大的值,可能之后很长一段时间都不会用到如此大的空间,就会造成内存的浪费。
虽然使用默认的StringBuilder进行字符串拼接操作,性能消耗不是很严重,但在高性能场景下,还是推荐使用ThreadLocal下可重用的StringBuilder方案。
好了,文章到此结束,希望可以帮助到大家。