几张图彻底理解Java字符串常量池、String.intern()(非复制粘贴,准确解释)
引言实话说,网上95%的文章都是错的,各种复制粘贴,我通过逐一排查以及实验证明,总结了比较准确的字符串在常量池中的存储过程,以及String.intern()方法的分析。String类常量池存储过程不废话,几幅图直接彻底搞清几种情况:(1)字面量直接赋值String s1 = "aaa";String s2 = "aaa";String s3 = "bbb";(2)new新对象String s1
引言
实话说,网上95%的文章都是错的,各种复制粘贴,我通过逐一排查以及实验证明,总结了比较准确的字符串在常量池中的存储过程,以及String.intern()方法的分析。全网独一份,最准确没有之一!!
String类常量池存储过程(JDK1.7及以上版本)
首先说明,所有示例图中字符串常量池都画在了堆中,这是因为jkd1.7之后,字符串常量池和静态变量被移动到堆中存放了。
不废话,几幅图直接彻底搞清几种情况:
(1)字面量直接赋值
String s1 = "aaa";
String s2 = "aaa";
String s3 = "bbb";
解释:栈中存放的是引用,其值为所指向的地址值
(2)new新对象
String s1 = new String("aaa");
String s2 = new String("aaa");
String s3 = new String("bbb");
注意: jdk1.7及之后虽然把字符串常量池和静态变量移到堆中了,但是!!字符串常量池和new的String对象虽然都在堆中,但并不在同一个地方,也是分开存的!
解释:只要是new,就一定会在堆中创建一个String对象,这是雷打不动的第一条。然后再看常量池中是否已经有该字符串了:
- 如果没有,就在常量池中创建一个新的String对象,对象里的value属性存具体的字符串本体值(final型char数组)。堆中对象的value属性存指向常量池中对应对象的引用(地址值)
- 如果有,则直接将堆中对象的value属性置为常量池中对应对象的引用即可
这个逻辑通过String类的构造器源码可验证(网上瞎说的都是没看源码):
如上,original.value就是将常量池中的char数组地址赋值给堆中String对象里的char数组,数组名赋值属于引用赋值
(3)字符串拼接
这种情况比较特殊,同时也是网上众说纷纭最乱的一个。
String s1 = "aaa";
String s2 = "bbb";
String s3 = "aaabbb";
String s4 = "aaa" + "bbb";
String s5 = s1 + "bbb";
String s6 = "aaa" + s2;
String s7 = s1 + s2;
String s8 = new String("aaa") + new String("bbb");
//常量拼接
System.out.println(s3 == s4); //true
//至少含一个变量的拼接
System.out.println(s3 == s5); //false
System.out.println(s3 == s6); //false
System.out.println(s3 == s7); //false
System.out.println(s3 == s8); //false
解释:
- 常量之间的拼接,都不会在堆中创建对象,而直接在字符串常量池中创建对象(如果已经有则直接返回引用);
- 只要拼接中有任意一个变量(包括new String),就不会在常量池中创建对象,而只在堆中创建,并且其中的value直接存字符串本体值。注意,new String(s1 + “bbb”)这种也算。
String.intern()
Intern()函数作用:一句话,获取字符串常量池中字符串本体值的引用。
- 如果是字符串常量调用该方法,则直接返回字符串常量池中该常量的引用。(没意义,相当于拷贝了一份)
- 如果是堆中字符串对象调用该方法,则先判断字符串常量池中是否有该字符串,如果有,直接返回字符串常量池中该常量的引用;如果没有,在常量池中添加一个堆中该字符串对象的引用地址,并返回该引用(即直接返回堆中该字符串对象的地址)。(注意版本区别:JDK1.7之前是在常量池中添加一个副本字符串本体值,然后返回它的引用)
一个直观实例:(非常重要!!)
//模拟字符串本体值在堆中而不在常量池中的情况,只能使用拼接来模拟
String s1 = new String("aaa") + new String("bbb");
String s2 = "aaabbb";
System.out.println(s1 == s2);
false
第一行代码:字符串常量池中创建了2个String对象,value的char数组置为aaa和bbb,堆中也创建了两个String对象,value指向常量池中的对象。最后在堆中再创建了1个拼接后的String对象,并在该对象的value中直接存了aaabbbb本体值。
第二行代码:字符串常量池中创建了1个String对象,value的char数组置为aaabbb,堆中无对象创建
此时s1和s2的值肯定不同,显而易见,一个指向堆中的对象(只要是带变量拼接,都在堆中存字符串本体值),一个指向常量池中的对象。接着我们加上intern操作:
String s1 = new String("aaa") + new String("bbb");
String s3 = s1.intern();
String s2 = "aaabbb";
System.out.println(s3 == s2);
System.out.println(s1 == s2);
true
true
第一行代码:同上
第二行代码:s1调用intern()函数,首先发现常量池中没有aaabbb这个串,因此在常量池中创建1个新对象,value中的char数组置为堆中本体值的引用地址,之后返回这个地址值给到s3,因此s3也是指向堆中那个对象,所以s1和s3相等。
第三行代码:先判断常量池中有没有aaabbb这个值,发现0x9001这个地址指向的值就是aaabbb,然后直接把这个地址值返回给s2,所以s2也指向了堆中的对象。所以s2和s1相等。
最后说一句,JDK1.7改为常量池中存堆内对象引用的目的:尽量保证字符串的本体值只存一份,无论是在堆里存还是在字符串常量池中存,节省空间消耗。
总结
- 字符串本体值只存一份(JDK1.7及以上)
- 堆中也可以直接存本体值,仅在带变量拼接的情况下才会出现,其余情况本体值都是在常量池中,单独new的时候会在堆中存一个到常量池的引用
- 把图记在脑子里比记文字会牢固的多
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)