String、StringBuffer、StringBuilder区别和String字符串进入常量池的时机

一、区别

    String类是不可变类,每次对String的改变都会创建一个新的对象;StringBuffer和StringBuilder都是可变类,当对它们进行改变时不会创建新的对象,它们俩的区别就在于StringBuffer是线程安全的,而StringBuilder是线程不安全的,因此在多线程中应该使用StringBuffer,而在单线程中则推荐使用StringBuilder,因为它的效率会更高,下面看一下对它们效率的测试。
    
/**
* @author 一池春水倾半城
* @date 2019/9/27
*/
public class StringDemo1 {
    public static String BASEINFO = "Mr.Y";
    public static final int COUNT = 2000000;


    /**
     * 执行一项String赋值测试
     */
    public static void doStringTest() {


        String str = new String(BASEINFO);
        long starttime = System.currentTimeMillis();
        for (int i = 0; i < COUNT / 100; i++) {
            str = str + "miss";
        }
        long endtime = System.currentTimeMillis();
        System.out.println((endtime - starttime)
                + " millis has costed when used String.");
    }


    /**
     * 执行一项StringBuffer赋值测试
     */
    public static void doStringBufferTest() {


        StringBuffer sb = new StringBuffer(BASEINFO);
        long starttime = System.currentTimeMillis();
        for (int i = 0; i < COUNT; i++) {
            sb = sb.append("miss");
        }
        long endtime = System.currentTimeMillis();
        System.out.println((endtime - starttime)
                + " millis has costed when used StringBuffer.");
    }


    /**
     * 执行一项StringBuilder赋值测试
     */
    public static void doStringBuilderTest() {


        StringBuilder sb = new StringBuilder(BASEINFO);
        long starttime = System.currentTimeMillis();
        for (int i = 0; i < COUNT; i++) {
            sb = sb.append("miss");
        }
        long endtime = System.currentTimeMillis();
        System.out.println((endtime - starttime)
                + " millis has costed when used StringBuilder.");
    }


    /**
     * 测试StringBuffer遍历赋值结果
     *
     * @param mlist
     */
    public static void doStringBufferListTest(List<String> mlist) {
        StringBuffer sb = new StringBuffer();
        long starttime = System.currentTimeMillis();
        for (String string : mlist) {
            sb.append(string);
        }
        long endtime = System.currentTimeMillis();
        System.out.println("buffer cost:"
                + (endtime - starttime) + " millis");
    }


    /**
     * 测试StringBuilder迭代赋值结果
     *
     * @param mlist
     */
    public static void doStringBuilderListTest(List<String> mlist) {
        StringBuilder sb = new StringBuilder();
        long starttime = System.currentTimeMillis();
        for (Iterator<String> iterator = mlist.iterator(); iterator.hasNext();) {
            sb.append(iterator.next());
        }


        long endtime = System.currentTimeMillis();
        System.out.println("builder cost:"
                + (endtime - starttime) + " millis");
    }


    public static void main(String[] args) {
        doStringTest();
        doStringBufferTest();
        doStringBuilderTest();


        List<String> list = new ArrayList<String>();
        for (int i = 0; i < 10000000; i++) {
            list.add("aaaa");
        }


        doStringBufferListTest(list);
        doStringBuilderListTest(list);
    }
}

 

运行结果
1015 millis has costed when used String.
60 millis has costed when used StringBuffer.
20 millis has costed when used StringBuilder.
buffer cost:307 millis
builder cost:120 millis
 
    可以看到,不考虑多线程,对String对象进行改变耗费的时间远大于另外两个,而使用StringBuilder耗费的时间也很明显的小于StringBuffer。因此,如果在 单线程下遇到需要 频繁改变字符串 的情况时,应该优先使用StringBuilder;而如果要 保证线程安全时,则应该使用StringBuffer。
 

二、String创建对象过程

    看到了效率对比之后,可能会有疑问,为什么使用String和StringBuffer/StringBuiler进行对象的改变之间的差别会如此明显?其实,这主要是由于它们在创建对象时遵循的机制造成的,下面就来分析一下。
    先来举几个栗子:
String s = "abc";
     对象创建过程分析:在class文件被JVM装载到内存中,JVM会创建一块String Pool(字符串常量池)。当执行String s = "abc"时,JVM会首先在String Pool中查看是否已经存在字符串对象“abc”,如果已存在,则不用创建新的对象,直接将s指向String Pool中已存在的对象"abc"。如果不存在,则先在String Pool中创建一个新的字符串"abc",然后将s指向它。
String s = new String("abc");
    创建过程分析:当执行String s = new String("abc")时,JVM会首先在String Pool中查看是否已经存在字符串对象“abc”,如果已存在,则不用在String Pool中创建新的对象,直接执行new String("abc")构造方法,在堆里创建一个字符串对象"abc",然后将引用s指向它。如果在String Pool中不存在该对象,则先在String Pool中创建一个新的字符串"abc",然后再进行new String("abc")等操作。
 
    知道了String的创建过程后,来让我们看几段代码:
 public class TestString{    
     public static void main(String args[]){    
         String s1 = new String("abc");//语句1    
         String s2 = "abc";//语句2    
         String s3 = new String("abc");//语句3    
    
         System.out.println(s1 == s2);//语句4    
         System.out.println(s1 == s3);//语句5    
         System.out.println(s2 == s3);//语句6    
     
         System.out.println(s1 == s1.intern());//语句7    
         System.out.println(s2 == s2.intern());//语句8    
         System.out.println(s1.intern() == s2.intern());//语句9    
     
         String hello = "hello";//语句10    
         String hel = "hel";//语句11    
         String lo = "lo";//语句12    
     
         System.out.println(hello == "hello");//语句13    
         System.out.println(hello == "hel" + "lo");//语句14    
         System.out.println(hello == "hel" + lo);//语句15    
         System.out.println(hello == hel + lo);//语句16    
     }    
 }   

 

问题1:当执行完语句(1)时,在内存里面生成几个对象?它们是什么?在什么地方? 
--->当执行完语句(1)时,在内存里面创建了两个对象,它们的内容分别都是abc,分别在String Pool(常量池)和Heap(堆)里。
其字符串的创建过程如下:首先在String Pool里面查找查找是否有 "abc",如果有就直接使用,但这是本程序的第一条语句,故不存在一个对象"abc",所以要在String Pool中生成一个对象"abc",接下来,执行new String("abc")构造方法,new出来的对象都放在Heap里面。在Heap里又创建了一个"abc"的对象。这时内存里就有两个对象了,一个在String Pool 里面,一个在Heap里面。 
 
问题2:当执行完语句(2)时,在内存里面一共有几个对象?它们是什么?在什么地方? 
当执行完语句(2)时,在内存里面一个对象也没有创建。当我们定义语句(2)的时候,如果我们用字符串的常量值(字面值)给s2赋值的话,那么首先JVM还是从String Pool里面去查找有没有内容为abc的这样一个对象存在,我们发现当我们执行完语句(1)的时候,StringPool里面已经存在了内容为abc的对象,那么就不会再在String Pool里面去生成内容为abc的字符串对象了。而是会使用已经存在String Pool里面的内容为abc的字符串对象,并且会将s2这个引用指向String Pool里面的内容为abc的字符串对象,s2存放的是String Pool里面的内容为abc的字符串对像的地址。也就是说当你使用String s2 = "abc",即使用字符串常量("abc")给定义的引用(str2)赋值的话,那么它首先是在String Pool里面去找有没有内容为abc的字符串对象存在,如果有的话,就不用创建新的对象,直接引用String Pool里面已经存在的对象;如果没有的话,就在 String Pool里面去创建一个新的对象,接着将引用指向这个新创建的对象。所以,当执行完语句(2)时内存里面一共有2个对象,它们的内容分别都是abc,在String Pool里面一个内容abc的对象,在Heap里面有一个内容为abc的对象。 
 
问题3:当执行完语句(3)时,在内存里面一共有几个对象?它们是什么?在什么地方?  
当执行完语句(3)时,其执行过程是这样的:它首先在String Pool里面去查找有没有内容为abc的字符串对象存在,发现有这个对象存在,它就不去创建 一个新的对象。接着执行new...,只要在java里面有关键字new存在,不管内容是否相同,都表示它将生成一个新的对象,new多少次,就生成多少个对象,而且新生成的对象都是在Heap里面,所以它会在Heap里面生成一个内容为abc的对象,并且将它的地址赋给了引用s3,s3就指向刚在Heap里面生成的内容为abc的对象。所以,当执行完语句(3)时,内存里面一共有3个对象,其中包含了在String Pool里面一个内容为abc的字符串对象和在Heap里面包含了两个内容为abc的字符串对象。 
 
问题4:当执行完语句(4)(5)(6)后,它们的结果分别是什么?  
在java里面,对象用"=="永远比较的是两个对象的内存地址,换句话说,是比较"=="左右两边的两个引用是否指向同一个对象。对于java里面的8种原生数据类型来说,"=="比较的是它们的字面值是不是一样的;对应用类型来说,比较的是它们的内存地址是不是一样的。在语句(1)(2)(3)中,由于s1、s2、s3指向不同的对象,它们的内存地址就不一样,因此可以说当执行完语句(4)(5)(6),它们返回的结果都是false。 
 
问题5:当执行完语句(7)(8)(9)后,它们的结果分别是什么?  
首先,s1这个对象指向的是堆中第一次new...生成的对象,当调用 intern 方法时,如果String Pool已经包含一个等于此 String 对象的字符串(该对象由equals(Object)方法确定),则返回指向String Pool中的字符串对象的引用。因为String Pool中有内容为abc的对象,所以s1.intern()返回的是String Pool中的内容为abc的字符串对象的内存地址,而s1却是指向Heap上内容为abc的字符串对象的引用。因而,两个引用指向的对象不同,所以,s1 == s1.intern() 为false,即语句(7)结果为false。 
对于s2.intern(),它还是会首先检查String Pool中是否有内容为abc的对象,发现有,则将String Pool中内容为abc的对象的地址赋给s2.intern()方法的返回值。因为s2和s2.intern()方法的返回值指向的是同一个对象,所以,s2 == s2.intern()的结果为true,,即语句(8)结果为true。 
对于s1.intern(),它首先检查String Pool中是否有内容为abc的对象,发现有,则将String Pool中内容为abc的对象的赋给s1.intern()方法的返回值。对于s2.intern(),首先检查String Pool中是否有内容为abc的对象,发现有,则将String Pool中内容为abc的对象的地址赋给s2.intern()方法的返回值。因为两者返回的地址都指向同一个对象,所以,s1.intern() == s2.intern()的结果为true,,即是语句(9)结果为true。 
因此,当执行完语句(7)(8)(9)后,它们的结果分别是false、true、true。 
 
问题6:当执行完语句(13)(14) (15)(16)后,它们的结果分别是什么?  
hello == "hello"引用hello指向的对象就是String Pool中的“hello”,即语句(13)的结果为true。 
hello == "hel" + "lo"当加号两边都是常量值时,就会组成一个新的常量值"hello"在String Pool里面,如果String Pool已经有相同内容的就不会再创建,则直接返回String Pool里面的内容为"hello"的字符串对象的内存地址,所以,hello == "hel" + "lo"结果为true。 
hello =="hel" + lo 当加号两边有一个不是常量值,会在堆里面创建一个新的"hello"对象,一个在String Pool中,一个在Heap中,故输出false 。 
hel + lo 同上,输出false。 
因此,当执行完语句(7)(8)(9)后,它们的结果分别是true、true、false、false。 
 

三、从intern()方法看String字符串进入常量池的时机

    在String类中有这么一个方法intern(),我们先来解释下这个方法,在调用"ab".intern()方法的时候会返回"ab",但是这个方法会首先检查字符串池中是否有"ab"这个字符串,如果存在则返回这个字符串的引用,否则就将这个字符串添加到字符串池中,然会返回这个字符串的引用。是不是有点晕?没事,来一段代码解释下。
 
public static void main(String[] args) {
        String s1=new String("he")+new String("llo");     // 1
        s1.intern();                                      // 2
        String s2="hello";                                // 3
        System.out.println(s1==s2);                       // 4
}
    我们一行行来看。执行第1行代码时,首先会在堆中创建"he"和"llo"的对象,然后再将它们引用保存到字符串常量池中,然后有个+号对吧,内部是创建了一个StringBuilder对象,一路append,最后调用StringBuilder对象的toString方法得到一个String对象(内容是hello,注意这个toString方法会new一个String对象),并把它赋值给s1。注意啊,没有把hello的引用放入字符串常量池。
    然后来到第2行,执行s1.intern(),jvm首先会到字符串常量池中寻找字符串"hello",发现并不存在,这时jvm会将s1对象的引用保存到字符串常量池中,然后返回这个引用,但这个引用没有被接收,所以没有用。
    到了第3行,这时字符串常量池中已经有"hello"了,直接用。
    第4行,s1表示在堆中的对象"hello"的引用,而s2拿到的"hello"则是堆中"hello"对象在字符串中的一个引用,所以它们指向了同一个对象,所以返回为true。
 
String s1=new String("he")+new String("llo");   // 1. 新建一个引用s1指向堆中的对象s1,值为"hello"
String s2=new String("h")+new String("ello");   // 2. 新建一个引用s2指向堆中的对象s2,值为"hello"
String s3=s1.intern();  // 3. 执行s1.intern()会在字符串常量池中新建一个引用"hello",该引用指向s1在堆中的地址,并新建一个引用s3指向字符串常量池中的"hello"
String s4=s2.intern();  // 4. 执行s2.intern()不会在字符串常量池中创建新的引用,因为"hello"已存在,因此只执行了新建一个引用s4指向字符串常量池中"hello"的操作
System.out.println(s1==s3); // s3和s4指向的都是字符串常量池中的"hello",而这个"hello"都指向堆中s1的地址,因此下面两句代码都为true
System.out.println(s1==s4);
System.out.println(s2 == s3);// s3和s4最终关联堆中的地址是对象s1,因此下面两句为false
System.out.println(s2 == s4);
    再来看上面这几行代码。第1行首先在堆中创建"he"和"llo"的对象,并将它们的引用放入字符串常量池,然后在堆中创建一个"hello"对象,没有放到字符串常量池,s1指向这个"hello"对象。
    第2行在堆中创建"h"和"ello"对象,并放入字符串常量池,然后在堆中创建一个"hello"对象,没有放到字符串常量池,s2指向这个"hello"对象。
    第3行,字符串常量池里没有"hello",因此会把s1指向的堆中的"hello"对象的引用放入字符串常量池(也就是说字符串常量池中的引用和s1指向了同一个对象),然后把这个引用返回给了s3,所以呢执行s3 == s1为true;
    第4行,字符串常量池里已经有"hello"了,因此直接将它返回给了s4,所以s4 == s1也为true。
    至于s2 == s3和s2 == s4为false则很明显了吧,s3和s4指向的字符串常量池中的引用和s1指向的对象是同一个,而s2则指向了另一个对象,因此返回false。
    
    看到这儿,有些小伙伴可能还会有些疑问,按照上面的说法,下面这段代码应该返回true才对啊---先在堆中创建一个"hello"对象,将它的引用放到字符串常量池中并赋给s,s.intern()返回的是字符串常量池中的引用,s.intern()和s最终指向堆中的对象相同。但程序仍然返回false,为什么呢?下面就来解释一下。
String s = new Sting("hello");
System.out.println(s.intern()==s);    // false
    第一行代码会在堆中创建两个对象(记为一号本体a1和二号本体a2),字符串常量池中存的是a1的引用,二号本体a2在字符串常量池无引用,s指向的是二号本体a2。然后s.intern()返回的是一号本体a1与二号本体肯定不同,所以返回false。
 
 
    再看下面这段代码
String s1=new String("he")+new String("llo");
System.out.println(s1.intern()==s1);
    执行第一段代码只在常量池中放了"he"和"llo"的引用,并没有放"hello"的引用,所以执行到s1.intern()时,会把s1的引用保存到常量池中,因此s1.intern()返回的引用与s1指向同一个地址,因此为true。
 
参考: Java 中new String("字面量") 中 "字面量" 是何时进入字符串常量池的? - 木女孩的回答 - 知乎 https://www.zhihu.com/question/55994121/answer/147296098
https://www.huberylee.com/2017/02/13/Java%E4%B8%ADString%E5%AF%B9%E8%B1%A1%E5%88%9B%E5%BB%BA%E8%BF%87%E7%A8%8B%E6%8E%A2%E7%A9%B6/

转载于:https://www.cnblogs.com/sxhjoker/p/11607457.html

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值