Java 之深入理解 String、StringBuilder、StringBuffer

打印 上一主题 下一主题

主题 852|帖子 852|积分 2556

媒介

          由于发现 String、StringBuilder、StringBuffer 面试的时间会常常问到,这里就顺便总结一下:本文重点会以这三个字符串类的性能、线程安全、存储布局这三个方面举行分析
  
   ✨上期回顾:Java 哈希表
  
   ✨目次
  媒介
   String 先容
  String 的不可变性
  String 在字符串常量池中的表现
  字符串常量池没有该字符串:
  字符串常量池有该字符串:
  总结
  
  StringBuilder 与 StringBuffer
   服从的比较
  线程安全的比较
  模仿面试
  


 String 先容


String 的不可变性

           跳转到 String 的实现就会发现:
  
String 类 不能被继承:该类被 final 修饰
String 类是不可变的:value[ ] 被 final 修饰,表明 value[ ] 自身的值不能改变
String 类可以序列化,可以和其他 String 比较其巨细:实现了 Comparable 接口
  

      通过下述代码,你就会发现 String 类中每一个看起来会修改 String 值的方法,实际上都是创建了一个全新的 String 对象包含修改后的字符串内容。而最初的 String 对象则丝毫未动 
  1. class Test{
  2.     static String Func(String s){
  3.         return s.toUpperCase();
  4.     }
  5.     public static void main(String[] args) {
  6.         String str = "hello world";
  7.         System.out.println(str);
  8.         String ret = Func(str);
  9.         System.out.println(ret);
  10.         System.out.println(str);
  11.     }
  12. }
  13. //
  14. 输出结果:
  15. hello world
  16. HELLO WORLD
  17. hello world
复制代码
           当把 str 传给 Func 方法时,实际传递的是引用的一个拷贝。其实,每当把 String 对象作为方法的参数时,都会复制一份引用,而该引用所指的对象其实一直待在单一的物理位置上,从未动过
  
String 在字符串常量池中的表现

        先从一段代码开始吧,以下这行代码总共创建了几个对象呢?
  1. String str = new String("Hello");
复制代码
        我想很多人看到会不暇思索的回答:“这不是一个吗?”,其实并否则它创建了两个对象:
   字符串常量池没有该字符串:

  

          假如字符串常量池中没有 Hello 这个字符,先在字符串常量池中创建一个 ‘Hello’ 的字符串对象,然后再在堆中创建一个 ‘Hello’ 的字符串对象,然后将堆中这个 ‘Hello’ 的字符串对象地点返回赋值给变量 str。因此需要创建个对象。
  
  字符串常量池有该字符串:

  1.         String str = new String("Hello World");
  2.         String ret = new String("Hello World");
复制代码

          Java 虚拟机会先在字符串常量池中查找有没有 ‘Hello’ 这个字符串对象,假如有,就不会在字符串常量池中创建 ‘Hello’ 这个对象了,直接在堆中创建一个‘Hello’ 的字符串对象,然后将堆中这个 ‘Hello’ 的对象地点返回赋值给变量 str。因此只需要创建个对象。
  (注意:ret 所指向的字符是 Hello,不是HHllo,画到末了没存档回不去了,将就着看吧)

           为什么要先在字符串常量池中创建对象,然后再在堆上创建呢
          由于字符串的利用频率实在是太高了,所以 Java 虚拟机为了提高性能和淘汰内存开销,在创建字符串对象的时间举行了一些优化,特意为字符串开辟了一块空间 -- 也就是字符串常量池
          通常情况下我们会接纳双引号的方式来创建字符串对象,而不是通过 new 关键字的方式,由于 new 会强制创建对象会对资源造成浪费。
        假如我们接纳双引号创建对象,如下图所示:
  1. String str1 = "Hello";
复制代码

  1. String str2 = "World";
复制代码

        Java 虚拟机会先在字符串常量池中查找是否存在该字符串,假如存在则不创建任何对象直接返回常量池中的对象引用假如不存在,则在常量池中创建该字符串,并返回对象引用。这样做的利益是制止了重复创建多个雷同的字符串对象,淘汰了内存的开销。
        接下来我们来研究一个经典的面试问题:
  1.     public static void main(String[] args) {
  2.             String a = "abc";
  3.             String b = new String("abc");
  4.             String c = new String("abc");
  5.             String d = b.intern();
  6.             System.out.println(a == b);
  7.             System.out.println(b == c);
  8.             System.out.println(a == d);
  9.         }
复制代码
     请问上述步伐打印的布局是什么?
  1. // 打印结构为:
  2. false
  3. false
  4. true
复制代码

           通过 String a = "abc" 这样创建一个字符串对象时,JVM会起首在字符串常量池中探求这个字符串,我们发现 "abc" 不存在,则在常量池中创建该字符串并将 a 指向它
  
          通过 String b = new String("abc") 这样创建字符串时,情况就不一样了,同样先在字符串常量池中探求这个字符串,我们发现 "abc" 存在。它会在堆区创建该字符串对象并使 b 指向它,同样调用 String c = new String("abc") 时,也会在堆区再创建一个 String 对象并使 c 指向它。由于我们字符串中 “==” 比较的是地点,而我们的 b、c 创建的是两个不同的对象所以返回 false。a、b 同理返回 false。
  
          当调用 String d = b.intern() 时,intern方法(该方法为 native 方法)会在字符串常量池中查找是否存在该字符串对象,假如存在,则将 d 指向该常量池中的字符串对象,假如不存在则在常量池中创建该字符串并指向它,所以 a == d 返回 true。
  
总结

利用双引号声明的字符串对象会生存在字符串常量池中
利用 new 关键字创建的字符串对象会先从字符串常量池中找,假如没找到就创建一个,然后再在堆中创建字符串对象;假如找到了,就直接在堆中创建字符串对象
在存在字符串常量池的条件下,利用 new 关键字但是不想创建对象,可以利用 intern 方法直接获取字符串常量池的引用


StringBuilder 与 StringBuffer

 服从的比较

           通过以上内容,信赖你已经对 String 有一定相识。由于字符串是不可变的,所以当遇到字符串的拼接(尤其是利用 + 号操作符)的时间,就需要考量性能的问题,你不能毫无顾虑地生产太多 String 对象,对珍贵的内存造成不必要的压力
          于是 Java 就计划了两个专门用来解决此问题的 StringBuilder、StringBuffer 类 ~
        可能有人会问 String 能做的事情干嘛还要用 StringBuilder、StringBuffer 呢?我们可以对一个字符举行多次拼接查看步伐的运行服从,如下述代码:
  1.     public static void main(String[] args) {
  2.             String s = "";
  3.             long st = System.currentTimeMillis();
  4.             for(int i = 0; i < 100000; i++) {
  5.                 s += "a";
  6.             }
  7.             long ed = System.currentTimeMillis();
  8.             System.out.println("String时间:" + (ed - st) + "毫秒");
  9.             st = System.currentTimeMillis();
  10.             StringBuilder sb = new StringBuilder();
  11.             for(int i = 0; i < 100000; i++) {
  12.                 sb.append("a");
  13.             }
  14.             ed = System.currentTimeMillis();
  15.             System.out.println("StringBuilder时间:" + (ed - st) + "毫秒");
  16.             st = System.currentTimeMillis();
  17.             StringBuffer sf = new StringBuffer();
  18.             for(int i = 0; i < 100000; i++) {
  19.                 sf.append("a");
  20.             }
  21.             ed = System.currentTimeMillis();
  22.             System.out.println("StringBuffer时间:" + (ed - st) + "毫秒");
  23.         }
复制代码
代码运行结果:
  1. String时间:827毫秒
  2. StringBuilder时间:1毫秒
  3. StringBuffer时间:3毫秒
复制代码
        可以看出,在大量对字符串举行连接操作的情况下,StringBuilder、StringBuffer 上风非常明显。由于 String 拼接会产生大量对象,而 StringBuilder、StringBuffer 无论是创建、拼接、修改、删除都是直接作用于原字符串,并不会产生多余的对象。其次 StringBuilder 比 StringBuffer 的服从稍微高一点也是有缘故原由的:这就涉及到线程安全问题,待会再讲。
 
线程安全的比较

        我们可以来对比一下它们的底层源码,再来做分析:
  1. //String
  2. public final class String
  3.     implements java.io.Serializable, Comparable<String>, CharSequence {
  4.     private final char value[];
  5.     ...
  6. }
  7. //StringBuilder
  8. public final class StringBuilder
  9.     extends AbstractStringBuilder
  10.     implements Serializable, CharSequence
  11. {
  12.     @Override
  13.     public StringBuilder append(Object obj) {}
  14.     @IntrinsicCandidate
  15.     public String toString() {...}
  16.     ...
  17. }
  18. //StringBuffer
  19. public final class StringBuffer
  20.     extends AbstractStringBuilder
  21.     implements Serializable, CharSequence
  22. {
  23.     //方法有synchronized关键字
  24.     @Override
  25.     public synchronized StringBuffer append(Object obj){...}
  26.     @IntrinsicCandidate
  27.     public synchronized String toString() {...}
  28.     ...
  29. }
  30. //AbstractStringBuilder
  31. abstract sealed class AbstractStringBuilder implements Appendable, CharSequence permits StringBuilder, StringBuffer {
  32.     byte[] value;
  33.     ...
  34. }
复制代码
 
           <1> 我们可以看到在 String 中,value 是被 final 修饰的是不可变的;StringBuilder、StringBuffer 都继承于 AbstractStringBuilder 这个类,而这个类中 的 value 是可变数组,所以我们举行拼接等操作时是直接作用于原字符串实现的,这就是服从高的由来。
   
           <2> 我们观察 StringBuilder、StringBuffer  的 toString、append 方法:由于 StringBuffer 操作字符串的方法加了synchronized 举行了同步,所以每次操作字符串时都会加锁,所以线程安全、但是性能低。这就是 StringBuilder 比 StringBuffer 运行服从略高的缘故原由。
  
总结:
String 类
不可变性:一旦创建,内容不可改变
线程安全:由于不可变性,String 对象天生线程安全
性能:频繁的字符串操作会导致大量的对象创建和内存斲丧
StringBuilder 类
可变性:内容可以被改变
非线程安全:实用于单线程环境
性能:比 String 更得当频繁的字符串操作,由于不会创建大量的中间对象
StringBuffer 类
可变性:内容可以被改变
线程安全:所有方法都是同步的,实用于多线程环境
性能:由于同步机制,性能略低于 StringBuilder

模仿面试

   假如HR问你:String、StringBuffer、StringBuilder 的区别?(你会怎么回答)
  答:关于String、StringBuffer、StringBuilder的区别,我有四个方面来说:
          第一个是可变性,String 内部的 value 是 final 修饰的,所以它是一个不可变的类,所以每次修改 String 的值的时间都会产生一个新的对象。而 StringBuffer、StringBuilder 是一个可变类,字符串的变动不会产生新的对象。
          第二个是线程的安全性,由于 String 是一个不可变的类,所以它是线程安全的;而 StringBuffer 也是线程安全的,由于它的每个操作方法中都有一个 synchronized  一个同步关键字;StringBuilder 不是线程安全的,所以在多线程环境下对字符串举行操作的时间我们应该利用 StringBuffer 否者利用 StringBuilder。
          第三个是性能方面,String 服从是最低的,由于其不可变性导致做字符串的拼接大概修改的时间,我们需要创建新的对象,以及分配内存;其次是 StringBuffer  比 String 的服从更高一点,由于它的可变性意味值字符串可以直接被修改;末了性能最高的是 StringBuilder ,由于 StringBuilder 比 StringBuffer 的性能要高,由于 StringBuffer 加了同步锁意味着对性能产生了影响。
          第四个是存储方面,String 存储在字符串常量池中,而 StringBuffer、StringBuilder 则是存储在堆的内存空间。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

科技颠覆者

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表