String字符串常量池

1.String的不可变性

String str = new String("abc");

在内存中开辟了一块空间之后,该空间赋值"abc",该空间中的值即为"abc",无法改变,除非经过GC后,内存重新分配

从源码中分析,String底层是被final修饰的char数组,从jdk9之后,是被final修饰的byte数组,是不可变量

(从char改为byte大概原因是大部分String数据是字母或者拉丁文,只占一个byte,而一个char是两个byte,浪费资源空间而一个汉字占用两个byte,可以通过设置编码UTF8指定)

例1:

String s1 ="abc";

String s2 ="abc";

log.info(s1 == s2);//true

s1 ="def";

log.info(s1 == s2);//false

log.info(s1);//def

log.info(s2);//abc/**

* String s1 = "abc";

* 以字面量的方式赋值,"abc"存储在堆空间中的字符串常量池中, * 而字符串常量池中不允许相同的字符串出现,此"abc"的地址为0x001

* String s2 = "abc";

* 常量池中已经存在"abc",所以将0x001给变量s2,此时s1和s2指向同一个地址

* s1 = "def";

* "abc"并未被修改为"def",而是在字符串常量池中开辟新的空间赋值"def",地址为0x002,

* 然后将此地址给变量s1,此时s1指向的是地址0x002,而s2指向的地址是0x001

*/

例2:

/**

* String str = "aaa";

在内存中开辟空间,赋值"aaa",地址为0x001

public void change(String str) {

str = "bbb";

}

然后调用changge()方法,changge()入栈

str = "bbb";

在空间中开辟新的空间,赋值为"bbb",地址为0x002,将地址给局部变量str,方法出栈

main()方法中的实例变量str指向的是0x001地址,change()方法中的局部变量str指向的是0x002的地址

log.info(demo.str);打印的是实例变量str指向的地址0x001,aaa

*/

2.String的内存分配

jdk6以及以前,String对象存储在永久代中

jdk7以及以后,String对象存储在java堆中

3.字符串的拼接操作

1.常量与常量的拼接结果在常量池,原理是编译器优化

2.常量池中不会存在相同内容的常量

3.只要其中还有一个是变量,结果就在堆中(而不是在常量池中),变量拼接的原理是StringBuilder

4.如果拼接的对象调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址

  例1:

        String s1 = "a"+"b"+"c"; //编译器优化后等同于abc,地址为0x001

        String s2 = "abc"; //字面量赋值,abc是在常量池中,将此地址0x001给了s2

        log.info("{}",s1==s2); //true

        log.info("{}",s1.equals(s2)); //true

  查看字节码文件

例2:

String s1 = "javaEE";

        String s2 = "hadoop";

        String s3 = "javaEEhadoop";

        String s4 = "javaEE" + "hadoop"; //编译器优化

        //如果拼接符号前后有了变量,则相当于在堆中new String(),具体的内容为拼接的结果:javaEEhadoop

        //所以,s5,s6,s7是在堆空间中是三个不同的对象

        String s5 = s1 + "hadoop";

        String s6 = "javaEE" + s2;

        String s7 = s1 + s2;

        log.info(s3 == s4); //true

        log.info( s3 == s5); //false

        log.info( s3 == s6); //false

        log.info(s3 == s7); //false

        log.info( s5 == s6); //false

        log.info(s5 == s7); //false

        log.info(s6 == s7); //false

        //intern():判断字符串常量池中是否存在javaEEhadoop,如果存在,则返回常量池中javaEEhadoop的地址;

        //如果不存在,就在字符串常量池中加载一份javaEEhadoop,并返回这个对象的地址

        String s8 = s6.intern();

        log.info("{}", s4 == s8); //true

例3:

        String s1 = "a";

        String s2 = "b";

        String s3 = "ab";

        String s4 = s1 + s2;

        log.info("{}",s3 == s4); //false

        对该代码的字节码文件进行反编译后

        ldc #16 //这个a就是字符串a

        astore_1 //将这个字符串a存储在局部变量表的1位置

        ldc #17 //这个b就是字符串b

        astore_2 //将这个字符串b存储在局部变量表的2位置

        ldc #18 //这个ab就是字符串ab

        astore_3 //将这个字符串ab存储在局部变量表的3位置

        new #11

        dup

        invokespecial #12 >

        aload_1

        invokevirtual #13

        aload_2

        invokevirtual #13

        invokevirtual #14

        astore 4

        //在执行s1 + s2的时候,是在堆空间中new了一个StringBuilder,然后从局部变量表的1位置获取字符串a,

        执行append()方法将字符串a追加到StringBuilder中,然后再从局部变量表的2位置获取字符串b,调用append()方法

        将字符串b追加到StringBuilder中,最后调用toString()方法(约等于new String()),所以s1+s2的值是在堆内存中

        重新开辟了一块空间的值,而s3是在堆中的字符串常量池中,并不是同一个对象

例4:

        final String s1 = "a";

        final String s2 = "b";

        String s3 = "ab";

        String s4 = s1 + s2;

        log.info("{}",s3 == s4); //true

        对该代码的字节码进行反编译后

ldc #16

astore_1

ldc #17

astore_2

ldc #18

astore_3

ldc #18

astore 4

//可以看到被final修饰后,s1,s2其实就是常量的引用,而不是变量,s4 = s1+s2可以认为是s4 = "a"+"b"

结论:字符串拼接操作不一定使用的是StringBuilder

如果拼接符号左右两边都是字符串常量或者常量引用的话,则仍然使用编译器优化,即非StringBuilder的方式

针对final修饰类,方法,基本数据类型,引用数据类型的变量时,能用final修饰的尽量用final修饰

4. String str = new String("abc")创建了几个对象?

两个,看字节码

1.new关键字在堆中分配空间

2.ldc指令从字符串常量池中加载"abc",如果没有就在常量池中创建一个

String str = new String("a")+new String("b")

然后查看StringBuilder.class的字节码文件,找到toString()方法

toString方法中,第一行在堆中new了一块空间,并没有在字符串常量池中创建"ab"对象

所以,上面的操作一共创建了六个对象

1.StringBuilder对象

2.String对象,值时"a"

3.字符串常量池中的"a"

4.String对象,值时"b"

5.字符串常量池中的"b"

6.toString方法中new对象,存放"ab"字符串

5.intern()的理解

总结String的intern()的使用

    jdk1.6中,将这个字符串对象尝试放入字符串常量池中

    如果池中有,则不会放入,返回已有的池的对象地址

    如果池中没有,会把此对象赋值一份,放入池中,并返回池中的对象地址

    jdk1.7中,将这个字符串尝试放入字符串常量池中

    如果池中有,则不会放入,返回已有的池的对象地址

    如果池中没有,会把对象的引用地址赋值一份,放入池中,并返回池中的引用地址

例1:

        String s = new String("1");

        s.intern();

        String s2 = "1";

        log.info("{}",s == s2); //false

        /**

        *  new String("1");在堆中new一个对象存放字符串1,在字符串常量池中开辟一块空间存放字符串1,此时s指向的是堆空间中的字符串1

        *  s.intern();执行intern()方法,但是此时字符串常量池中已经存在字符串1,所以s.intern()方法没有任何作用,s指向的还是堆空间中的字符串1

        *  String s2 = "1"; 通过字面量赋值,s2指向的是字符串常量池中的1

        *  所以s1,s2是指向不同的地址,结果为false

        *

        */

例2:

String s = new String("1") + new String("1");

        s.intern();

        String s2 = "11";

        log.info("{}",s==s2); //jdk6 false;jdk7/8 true

        /**

        * new String("1")  在堆中new一个对象存放字符串1,在字符串常量池中开辟一块空间存放字符串1

        * new String("1")  在堆中new一个对象存放字符串1,由于字符串常量池中已经存在字符串1,所以不再创建字符串1

        * s.intern() jdk6:此时s的值是指向堆空间中的字符串11,去永久代的字符串常量池中查询,没有字符串11,在字符串常量池中开辟空间存入字符串11

        *  jdk7:此时s的值是指向堆空间中的字符串11,去堆中的字符串常量池中查询,没有字符串11,但是因为字符串常量池也在堆空间中,为了节省内存

        * 字符串常量池中开辟的新空间的地址就是堆空间中的11的地址

        * String s2 = "11" 字面量赋值会直接在字符串常量池中寻找,有,就将地址给s2,没有,就在字符串常量池中创建,

        * 而此时,显然上一步已经在字符串常量池中有了字符串11

        */

例3:

String s = new String("1") + new String("1");

        String s2 = "11";

        s.intern();

        log.info("{}", s == s2); //false

        s = s.intern();

        log.info("{}", s == s2); //true

        /**

        * new String("1")  在堆中new一个对象存放字符串1,在字符串常量池中开辟一块空间存放字符串1

        * new String("1")  在堆中new一个对象存放字符串1,由于字符串常量池中已经存在字符串1,所以不再创建字符串1

        * String s2 = "11" 这个操作会在字符串常量池中开辟一个空间,存放字符串11

        * s.intern() 此时在字符串常量池中已经存在了字符串11,所以这行代码没有什么效果

            s指向的是堆中的对象,s2指向的是栈中的对象

s = s.intern(); 字符串常量池中已经存在"11",将常量池中的引用给变量s,s2指向的也是常量池中的"11"

        */

你可能感兴趣的:(String字符串常量池)