您的位置:首页 > 编程语言 > Java开发

Java中的String不再纠结

2014-11-02 10:01 302 查看
又是新的一月,又是各种总结,先分享一下java中string的一些小专题吧,这部分比比较基础,但是也非常的有用。我发现很多面试官像中了邪一样就爱问这个。。string的种种,纠结,希望这篇文章让大家不再纠结。。

      string是我们经常用到的一个类型,其实有时候觉得写程序就是在反复的操作字符串,这是C的特点,在java中,jdk很好的封装了关于字符串的操作。今天主要讲的是三个类String 、StringBuffer 、 StringBuilder .这三个类基本上满足了我们在不同情景下使用字符串的需求。

    先说,第一个String。

    JDK的解释是 “Strings are constant; their values cannot be changed after they are created”也就是说String对象一旦被创建就是固定不变的了(你一定有问题,但请先等一等,耐心读下去),这样的一点好处就是可以多线程之间访问,因为只读不写。

   一般情况下我们以下面两种方式创建一个String对象

   


  两种方式是有区别的,这和java的内存管理有关,前面已经说过,string创建之后是不可变的,所以按照第一种方式创建的字符串会放在栈里,更确切的是常量池中,常量池就是用来保存在编译阶段确定好了大小的数据,一般我们定义的int等基本数据类型就保存在这里。

  其具体的一个流程就是,编译器首先检查常量池,看看有没有一个“string”,如果没有则创建。如果有的话,则则直接把str1指向那个位置。

  第二种创建字符串的方法是通过new关键字,还是java的内存分配,java会将new的对象放在堆中,这一部分对象是在运行时创建的对象。所以我们每一次new的时候,都会创建不同的对象,即便是堆中已经有了一个一模一样的。

   写一个小例子

String str1 = "string";
String str4 = "string";
String str2 = new String("string");
String str3 = new String("string");

/*用于测试两种创建字符串方式的区别*/
System.out.println(str1 == str4);
System.out.println(str2 == str3);
System.out.println(str3 == str1);

str3 = str3.intern(); //一个不常见的方法
System.out.println(str3 == str1);


  这个的运行结果是 

  true    //解释:两个字符串的内容完全相同,因而指向常量池中的同一个区域

  false   //解释:每一次new都会创建一个新的对象

  false  // 解释: 注意==比较的是地址,不仅仅是内容  

  true  //介绍一下intern方法,这个方法会返回一个字符串在常量池中的一个地址,如果常量池中有与str3内容相同的string则返回那个地址,如果没有,则在常量池中创建一个string后再返回。实际上,str3现在指向了str1的地址。

  

  这就是让人纠结的string了,现在你可以说话了。。。很多人有这样的疑问就是既然string是不变的,那么为什么str1 + "some"是合法的,其实,每次对string进行修改,都会创建一个新的对象。

  所以如果需要对一个字符串不断的修改的话,效率是非常的低的,因为堆的好处是可以动态的增加空间,劣势就是分配新的空间消耗是很大的,比如我们看下面的测试。

long start = System.currentTimeMillis();

for(int i = 0; i < 50000; i++)
{
str1+= " ";
}

long end = System.currentTimeMillis();
System.out.println("the run time is "+(end -start)+" ms");


 我的机器上运行结果是the run time is 3538 ms   如果你把循环的次数后面再增加几个0就会更慢。因为每一次循环都在创建心的对象,那么JDK如何解决这个问题?

   下面就要说第二个类StringBuffer。

    StringBuffer是一个线程安全的,就是多线程访问的可靠保证,最重要的是他是可变的,也就是说我们要操作一个经常变化的字符串,可以使用这个类,基本的方法就是append(与string的concat方法对应)和insert方法,至于怎么使用,就不多讲了,大家可以自己查看API。

StringBuilder sb = new StringBuilder("string builder");
StringBuffer sf = new StringBuffer("string buffer");

long start = System.currentTimeMillis();

for(int i = 0; i < 50000; i++)
{
//str1+= " ";
sb.append(" ");
}

long end = System.currentTimeMillis();
System.out.println("the run time is "+(end -start)+" ms");


  测试一下,这次只需要8ms,这就是效率。

  那么接下来,就要问StringBuilder是干什么的,其实这个才是我们尝使用的,这个就是在jdk 1.5版本后面添加的新的类,前面说StringBuffer是线程同步的,那么很多情况下,我们只是使用一个线程,那个同步势必带来一个效率的问题,StringBuilder就是StringBuffer的非线程同步的版本,二者的方法差不多,只是一个线程安全(适用于多线程)一个没有线程安全(适用于单线程)。

  其实看了一下jdk源代码就会发现,StringBuffer就是在各个方法上加上了关键字syncronized

  


    

  以上就是对三个字符串类的一个总结,总之不要在这上面纠结。。。。。。不想介绍太多的方法,总觉得那样会把一篇博客弄成API文档一样,而且还非常的繁琐。都是些体会,希望有所帮助。起码不要再纠结,尤其是面试。。。。

 

本文完整源代码:https://github.com/octobershiner/Java-Taste/tree/master/StringDemo
欢迎关注JavaTaste项目 https://github.com/octobershiner/Java-Taste
系列文章:http://www.cnblogs.com/octobershiner/archive/2012/03/17/2404154.html

很早之前总结过java中一些String的理解和用法,最后还体会到了其中String的一点性能上的优化。那篇博文更多的是在讨论string存储的问题,感兴趣的童鞋可以看一下 传送连接
    这两天在淘测试的文章里看到一篇关于java string的文章,谈到了StringBuilder和StringBuffer的使用效率的问题,然后发现自己忽略了capacity这个概念。比如说下面的一段代码:

1        StringBuffer sf = new StringBuffer("");
2         sf.append("leeon");
3         System.out.println("length: "+sf.length());
4         System.out.println("capacity: "+sf.capacity());


    这个打印的结果就是
    length:5
    capacity:16
    length我们可以理解,那么capacity是什么呢?
    


     JDK源码中给出的解释就是用来存储新插入的字符空间的容量,可见一个默认的容量就是16了,当然如果我们像下面这样的去重写上面的代码,就会得到不一样的结果。

1         StringBuffer sf = new StringBuffer("leeon");
2         System.out.println("length: "+sf.length());
3         System.out.println("capacity: "+sf.capacity());


     这次的结果就是
     length:5
    capacity:21
    原因就是我们在构造sf的时候本身的value已经是5了,再加上了新准备插入的初始化容量,也就是5+16.具体实现的细节是:
    


     ----------------------------------------------------------------------------------------------------------
     以上是背景知识,下面开始讨论如何从capacity这个特性出发来优化性能。再仔细阅读jdk源码中append方法的实现的时候会发现,每次调用append都要进行容量的检查,因为要确保StringBuffer足够的大才能装得下新添加的字符串。不妨再一起看下代码:
   


  


   当调用append的时候,首先会通过ensureCapacityInternal检查所需的容量,如果容量不足再调用expandCapacity进行扩容,可以看见扩容的方式是将现在的字符串大小增加一倍然后再加上2.如果容量仍然不足,则直接扩展到所需的大小。我们发现扩展容量就是一个耗时的操作,虽然处理大量字符串的时候采用StringBuffer会比用String更加的提升性能,但是却仍然存在可优化的耗时部分。
  其实StringBuffer和StringBuilder在初始化的时候是可以指定capacity的,如果有一个大概的预估,初始化一个比较合适的capacity,减少扩展容量的操作,效率就会有更大的提高。比如下面的测试代码,我们对同样的字符串操作五百万次,统计一下结果。

1         StringBuilder sb = new StringBuilder(10*5000000);
2         StringBuffer sf = new StringBuffer(10*5000000);
3
4
5         long start = System.currentTimeMillis();
6         for(int i = 0; i < 5000000; i++)
7         {
8             sb.append("1234567890");
9         }
10         long end = System.currentTimeMillis();
11         System.out.println("the StringBuilder run time is "+(end -start)+" ms");
12
13         start = System.currentTimeMillis();
14         for(int i = 0; i < 5000000; i++)
15         {
16             sf.append("1234567890");
17         }
18         end = System.currentTimeMillis();
19         System.out.println("the StringBuffer run time is "+(end -start)+" ms");




 上面的结果我没有求平均值,测试也只是在单线程下进行的,但是数据波动不大,大致可以看出一些变化,我们发现当恰当的初始化了StringBuffer后,他的性能已经和未初始化的StringBuilder相当甚至超过了,这样我们就能够在保持和原来StringBuilder相当效率的基础上有更好的安全性了。
  可以看到采用了初始化的容量,我们获得的性能提升要高于从SF切换到SB。
  淘测试总结的结论中有一个我很喜欢,引用一下
  “ 用好现有的类比引入新的类更重要。很多程序员在使用 StringBuffer 时是不指定其容量的(至少我见到的情况是这样),如果这样的习惯带入 StringBuilder 的使用中,你将只能获得 10 %左右的性能提升(不要忘了,你可要冒多线程的风险噢);但如果你使用指定容量的 StringBuffer ,你将马上获得 45% 左右的性能提升,甚至比不使用指定容量的 StirngBuilder
都快 30% 左右。”
  这个问题其实后续还值得讨论,现在我们看到都是单线程的情况,那么多线程情况下,StringBuffer是不是可以优化的更多了?
  忽然发现,自己忽略了很多java的特性,学在当下。

作者:leeon
出处:http://www.cnblogs.com/octobershiner/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,否则保留追究法律责任的权利.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: