String的性能分析

 延续上篇String的分析文章,<<深入理解String>>   本将章分析一下String的几个方法的性能优化;

一、 String substring的内存泄漏:

   在JDK6的源码实现:

  image.png

image.png

   在源码的注释中说明,这是一个包作用域的构造函数,其目的是为了能高效且快速地共享String内的char数组对象。但在这种通过偏移量来截取字符串的方法中,String的原生内容value数组被复制到新的子字符串中。设想,如果原始字符串很大,截取的字符串长度却很短,那么截取的子字符串中包含了原生字符串的所有内容,并占据了相应的空间,而仅仅通过偏移量和长度来决定自己的实际价值。这种算法提高了运算速度却浪费了大量的内存空间。

  对于为什么使用不当会出现内存的泄漏情况, 

 通过以下代码重现:

public class LeakTest {  
    public static void main(String...args) {  
        List<String> handler = new ArrayList<String>();  
        for(int i = 0; i < 100000; i++) {  
            Huge h = new Huge();  
            handler.add(h.getSubString(1, 5));  
        }  
    }  
}  
  
class Huge {  
    private String str = new String(new char[100000]);  
    public String getSubString(int begin, int end) {  
        return str.substring(begin, end);  
    }  
}

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space


      按理来说,Full GC 后是对Huge对象的回收,并且释放所占有的内存,但是由于JDK 6进行substring时,使用共享内容字符数组,依然持有原有char[100000]的空间引用. 这种方式的有点就是速度更加快,不需要重新申请内存 。

     Java 7 中substring的实现抛弃了之前的内容字符数组共享的机制,对于子字符串(自身除外)采用了数组复制实现单个字符串持有自己的应该拥有的内容。

   image.png

  这种方式采用了空间换时间的方式,每次操作substring性能上,会比jdk6方式的差点;

二、字符串分割和查找

    字符串的分割和查找也是字符串处理中最常用的方法之一,

       public String[] split(String regex)

    比如"a,b;c,d",我要分割该字符串,可以使用如下正则进行分割处理,

     String []arrays="a,b;c,d".split("[,|;]");

     split本身具有强大的功能,恰当使用,能否提供编程的效率,但是在性能的表现上并不理想;

     在使用上,可以使用效率更加高的StringTokenizer类分割字符串;

    StringTokenizer是jdk提供专门用来处理字符串分割的工具类,

    提供的函数:

        StringTokenizer st = new StringTokenizer(str,"\\s");

    String 与 StringTokenizer 之间的性能对比:

public class StringTest {

	public static void main(String[] args) {
		String str = buildString(1_000_000); //1.7新特性, 1000000
		long start;
		long end;
		
		System.out.println("-----------StringTokenizer start-----------");		
		start = System.currentTimeMillis();
		StringTokenizer st = new StringTokenizer(str);
		StringBuilder sb = new StringBuilder();
		while(st.hasMoreTokens()){
			sb.append(st.nextToken());
		}
		end = System.currentTimeMillis();
		System.out.println("StringTokenizer time use:" + (end-start));
		
		System.out.println("-----------StringSpilt start-----------");		
		start = System.currentTimeMillis();
		StringBuilder sb2 = new StringBuilder();
		String[] strs = str.split("\\s");
		for(String s: strs){
			sb2.append(s);
		}
		end = System.currentTimeMillis();
		System.out.println("StringSpilt time use:" + (end-start));		
		
	}
	
	//建立一个长字符串,
	//其中有空格,以便拆分成length长度的n个字符串
	private static String buildString(int length) {
		StringBuilder sb = new StringBuilder();
		Random r =new Random();
		for (int i = 0; i <length;i++ ){
			for (int j = r.nextInt(10); j>0 ;j--){
				sb.append((char)('a' + r.nextInt(26)));
			}
			sb.append(" ");
		}		
		return sb.toString();
	}
}

输入的大概结果:

    ———–StringTokenizer start———–

StringTokenizer time use:447

     ———–StringSpilt start———–

StringSpilt time use:2416

三、字符串的查找:

   在软件开发的过程中,我们常常判断一个字符串是否以"xx"开头或者结尾,使用startswith()或者endstartwith()来实现,

   String str="java_xx";

   boolean result=str.startswith("java");

   boolean result=str.endstartwith("xx");

   即便这种方式是java的内置函数,但在效率上也远远低于charAt()方法,

   如:

     if(str.charAt(0)=='j' && str.charAt(0)=='a'&& str.charAt(0)=='v' && str.charAt(0)=='a');

      if(str.charAt(str.length-1)=='x' && str.charAt(str.length-2)=='x');

  在高度频繁判断字符串是否以"xx"开头或者结尾,可以使用charAt方式实现,但是不灵活;

四、字符串的拼接上:

   在于拼接字符串上,推荐使用Stringbuilder和Stringbuffer,实在比较少字数的拼接,可以使用String.concat()方法来实现拼接;

发表评论