字符串优化处理
一)、字符串的内部结构
1)、char数组
表示String的内容,所有字符串的超集。
2)、offset偏移
3)、count长度
注:String的真实内容由offset和count进行定位和截取。
二)、字符串的特性
1)、不变性
当一个对象被多线程共享,并且频繁使用时 ,可以省略同步和锁等待时间。
2)、字符串常量池
3)、String由final修饰不能被继承
三)、subString()方法的内存泄露
-
subString(int beginIndex)
-
subString(int beginIndex, int endIndex)
subString()源码剖析:
1)、JDK1.6版本实现:
通过offset = offset + beginIndex, count = endIndex - beginIndex,采用时间换空间的方式重新生成一个字符串。
new String(int offset, int count, char[] value);
每生成一个新的子字符串都是将原char[]value数组赋值到新的子字符串中,然后通过偏移量和长度来决定实际的取值。
此时,若有大量的长字符串进行截取就会出现内存泄露的情况。
改进:
new String(subString(0,1)):
重新生成一个字符串,此时字符串的value值则为子字符串的value值,而new String(offset, count , cahr[] value)中生成的字符串因为没有被使用,则被GC回收,解决了内存泄露的问题。
2)、在JDK1.7版本后对该功能进行了改进,解决了内存泄露的问题
new String(int offset, int count, char[] value)
实现:通过offset和count重新生成一个数组,并根据偏移量将原数组对应的数据复制到新数组中。
四)、字符串的分割和查找
字符串的三种分割方式:
1)、split(String regex)
2)、StringTokenier(String str, String delim)
hasMoreTokens();
nexToken();
3)、自定义字符串分割
通过indexOf(int ch)和subString()
三种分割方法的速度比较:
split < StringTokenier < 自定义
(参考书籍是这么说,但通过自测试发现自定义截取子符串耗时却长得多)
/**
* 字符串分割
*/
public class StringSplit {
public static void main(String[] args) {
String orgStr = null;
StringBuffer sb = new StringBuffer();
for(int i = 0; i < 10000; i++){
sb.append(i);
sb.append(";");
}
orgStr = sb.toString();
//对orgStr字符串进行分割
Long start = System.currentTimeMillis();
/**
* 使用split分割
*/
for(int i = 0; i < 10000; i++){
orgStr.split(";");
}
//使用split分割耗时2655
System.out.println(System.currentTimeMillis()-start);
/**
* 使用StringTokenier分割
*/
StringTokenizer st = new StringTokenizer(orgStr,";");
Long start1 = System.currentTimeMillis();
for(int i = 0; i < 10000; i++){
while(st.hasMoreElements()){
st.nextToken();
}
st = new StringTokenizer(orgStr,";");
}
//使用StringTokenizer分割耗时2719秒,在使用个StringTokenizer分割时还有创建和销毁StringTokenizer对象的时
// 因此总体来说,使用StringTokenizer的性能比split的性能要好
//耗时2719
System.out.println(System.currentTimeMillis()-start1);
/**
* 使用indexOf和subString 来分割
*/
String temp = orgStr;
Long start2 = System.currentTimeMillis();
for(int i = 0; i < 10000; i++){
while(true) {
String splitStr = null;
//获取第一个;所在的位置
int j = temp.indexOf(';');
if(j < 0){
break;
}
splitStr = temp.substring(0, j);
temp = temp.substring(j + 1);
}
temp = orgStr;
}
//耗时413933
System.out.println(System.currentTimeMillis()-start2);
}
}
五)、StringBuffer和StringBuilder
字符串的累加
== 静态字符串的累加 ==:
String result = "String" + "and" + "String" + "append";
StringBuffer sb = new StringBuffer();
sb.append("String");
sb.append("and");
sb.append("String");
sb.append("append");
我们想象中result拼接程序的执行过程应该是Sting + and 两个字符串生一个Stringand,Stringand再个String结合生成StringandString,生成多个字符串实例,最后的到result拼接结果。
sb中程序执行过程是,产生一个StringBuffer实例,然后往sb容器中添加数据。
测试两端代码的执行时间,发现result拼接的耗时小于sb添加的耗时,为什么呢?
通过反编译发现,在编译时期,java虚拟机自动将String result = "String" + "and" + "String" + "append";编译成String result = "StringandStringappend";直接生成拼接结果对象,而sb还要进行append操作,因此,对于显示的(静态的)字符串拼接性能优于StringBuffere拼接。
== 变量字符串的累加 ==:
String str1 = "String";
String str2 = "and";
String str3 = "String";
String str4 = "append";
String result = str1 + str2 + str3 + str4;
StringBuffer sb = new StringBuffer();
sb.append("String");
sb.append("and");
sb.append("String");
sb.append("append");
测试两端代码的执行速度,发现两端代码的执行速度几乎一样
原因:
通过反编译发现代码段1 的编译结果为
String str1 = "String";
String str2 = "and";
String str3 = "String";
String str4 = "append";
String result = new StringBuffer(str1).append(str2).append(str3).append(str4).toString();
结论:java虚拟机自动在编译时期就对字符串的累加进行了优化,对于静态字符串的累加在编译时期自动合成一个单独的长字符串,对于变量字符串的累加,使用StringBuffer对象来完成累加操作。
另一种字符串累加方式:
String str1 = "String";
String str2 = "and";
String str3 = "String";
String str4 = "append";
String result = str1.concat(str2).concat(str3).concat(str4);
字符串的累加效率:
+= / + < concat < StringBuffer/StringBuilder
StringBuffer和StringBuilder的区别:
1)、StringBuffer
线程同步,保证线程安全,但同步方法消耗一定的系统资源。
2)、StringBuilder
线程不同步,效率高,当线程不安全。
3)、StringBuffer和StringBuilder底层都是一个char[]类型的数组,默认数组长度为16个字节。
4)、当创建StringBuffer和StringBuilder对象时,为了提高系统的性能,最好声明对象的长度。
因为,当所需长度大于内部数组的长度会会对内部数组进行扩容(扩容后容量翻倍),并将原数据复制到扩容后的新数组中,存在大量的内存复制操作,消耗资源。
使用场景:不考虑线程安全时使用StringBuilder, 考虑线程安全时使用StringBuffer