<!--一个博主专栏付费入口结束-->
<link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/ck_htmledit_views-d284373521.css">
<link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/ck_htmledit_views-d284373521.css">
<div class="htmledit_views" id="content_views">
<p> String做为Java开发中常用的类,弄懂它是非常有必要的,但是往往很多工作了几年从业人员,也并没有特别熟悉过,所以楼主总结一下String的常量池,以及intern()方法等。技术无止境,当然本文也有不足之处,欢迎大家在评论区指正。</p>
前言
本次代码使用 jdk 1.8版本,并且以下代码示例除了第一个写了main()方法,并且所有的示例分别独立运行 ,其余为了简洁做了缺省main()。在创建字符串分析的同时,都默认省略了栈中的句柄指向分析。进入正题之时,先科普几个知识点
- String源码里面标注为final修饰的类,是一个不可改变的对象,那平时用到字符串A+字符串B怎么改变了呢,其实这里有涉及到String的常量池,首先常量池存放在方法区。
- 在jdk1.6时,方法区是存放在永久代(java堆的一部分,例如新生代,老年代)而在jdk1.7以后将字符串常量池移动到了的堆内存中
- 在jdk1.8时,HotspotVM正式宣告了移除永久代,取而代之的是元数据区,元数据区存放在内存里面(存放一些加载class的信息),但是常量池还是和jdk1.7存放位置一样还是存放在堆中。
先看一波常见面试题:
首先看一道常见的面试题,问输出的是什么?
-
public static void main(String[] args){
-
String s1 =
new String(
"123");
-
String s2 =
"123";
-
System.out.println(s1 == s2);
-
}
基本上大家都能知道是false,但是再这么深究一次,问 String s1 = new String("123") 创建了几个对象,String s2 = "123" 创建 了几个对象,那如果题目稍微改变一下成下面这样,那输出的又是什么?
-
String s1 =
new String(
"123").intern();
-
String s2 =
"123";
-
System.out.println(s1 == s2);
// true
-
-
// 如果这样再改一下
-
String s1 =
new String(
"123");
-
s1.intern();
-
String s2 =
"123";
-
System.out.println(s1 == s2);
// false
如果对输出结果不是很明白的,本文都会一一解答并且进行拓展。
创建字符串分析:
首先要分析String,一定要知道String几种常见的创建字符串的方式,以及每一种不同的方式常量池和堆分别是什么储存情况。
1.直接写双引号常量来创建
判断这个常量是否存在于常量池,
如果存在,则直接返回地址值(只不过地址值分为两种情况,1是堆中的引用,2是本身常量池的地址)
如果是引用,返回引用地址指向的堆空间对象地址值
如果是常量,则直接返回常量池常量的地址值,
如果不存在,
在常量池中创建该常量,并返回此常量的地址值
-
String s =
"123";
-
//true,因为s已经在常量池里面了,s.intern()返回的也是常量池的地址,两者地址一样为true
-
System.out.println(s == s.intern());
2. new String创建字符串
与上面第一种方式相比,第一种方式效率高,下图解决了本文中的最开始出的部分面试题。
首先在堆上创建对象(无论堆上是否存在相同字面量的对象),
然后判断常量池上是否存在字符串的字面量,
如果不存在
在常量池上创建常量(并将常量地址值返回)
如果存在
不做任何操作
-
String s =
new String(
"123");
-
/*
-
严格来说首先肯定会在堆中创建一个123的对象,然后再去判断常量池中是否存在123的对象,
-
如果不存在,则在常量池中创建一个123的常量(与堆中的123不是一个对象),
-
如果存在,则不做任何操作,解决了本文第一个面试题有问到创建几个对象的问题。
-
因为常量池中是有123的对象的,s指向的是堆内存中的地址值,s.intern()返回是常量池中的123的常量池地址,所以输出false
-
*/
-
System.out.println(s == s.intern());
3.两个双引号的字符串相加
判断这两个常量、相加后的常量在常量池上是否存在
如果不存在
则在常量池上创建相应的常量(并将常量地址值返回)
如果存在,则直接返回地址值(只不过地址值分为两种情况,1是堆中的引用,2是本身常量池的地址)
如果是引用,返回引用地址指向的堆空间对象地址值,
如果是常量,则直接返回常量池常量的地址值,
-
String s1 =
new String(
"123").intern();
-
String s2 =
"1"+
"23";
-
/*
-
* 首先第一句话 String s1 = new String("123") 以上分析过创建了两个对象(一个堆中,一个常量池 中)此时s1指向堆中
-
* 当s1调用.intern()方法之后,发现常量池中已经有了字面量是123的常量,则直接把常量池的地址返回给s1
-
* 在执行s2等于123时候,去常量池查看,同上常量池已经存在了,则此时s2不创建对象,直接拿常量池123的地址值使用
-
* 所以此时s1 和 s2 都代表是常量池的地址值,则输出为true
-
*/
-
System.out.println(s1 == s2);
如果这里看不懂 intern()方法时,可以快速滑动到文章尾部,先看intern()方法的分析。
4.两个new String()的字符串相加
首先会创建这两个对象(堆中)以及相加后的对象(堆中)
然后判断常量池中是否存在这两个对象的字面量常量
如果存在
不做任何操作
如果不存在
则在常量池上创建对应常量
-
String s1 =
new String(
"1")+
new String(
"23");
-
/*
-
* 首先堆中会有 1 ,23 ,以及相加之后的123 这三个对象。如果 1,23 这两个对象在常量池中没有相等的字面量
-
* 那么还会在常量池中创建2个对象 最大创建了5个对象。最小创建了3个对象都在堆中。
-
*/
-
s1.intern();
-
String s2 =
"123";
-
System.out.println( s1 == s2);
// true
这个地方比较复杂 ,如果我把String s2 = "123" 代码放在s1.intern()前面先执行,其余代码不变,那么输出结果又为false,这里等会楼主会在分析 intern()方法的时候再重点分析一次。
-
String s2 =
"123";
-
s1.intern();
-
System.out.println( s1 == s2);
// false
5.双引号字符串常量与new String字符串相加
首先创建两个对象,一个是new String的对象(堆中),一个是相加后的对象(堆中)
然后判断双引号字符串字面量和new String的字面量在常量池是否存在
如果存在
不做操作
如果不存在
则在常量池上创建对象的常量
-
String s1 =
"1"+
new String(
"23");
-
/*
-
*首先堆中会有 23 ,以及相加之后的123 这2个对象。如果23,1 这两个对象在常量池中没有相等的字面量
-
*那么还会在常量池中创建2个对象最大创建了4个对象(2个堆中,2个在常量池中)。最小创建了2个对象都堆中。
-
*/
-
String s2 =
"123";
-
System.out.println( s1.intern() == s2);
// true
6.双引号字符串常量与一个字符串变量相加
首先创建一个对象,是相加后的结果对象(存放堆中,不会找常量池)
然后判断双引号字符串字面量在常量池是否存在
如果存在
不做操作
如果不存在
则在常量池上创建对象的常量
-
String s1 =
"23";
-
/*
-
* 这里执行时,常量“1” 会首先到字符串常量池里面去找,如果没有就创建一个,并且加入字符串常量池。
-
* 得到的123结果对象,不会存入到常量池。这里特别注意和两个常量字符串相加不同 “1”+“23” 参考上面第三点
-
* 由于不会进入常量池,所以s2 和 s3 常量池地址值不同,所以输出为false
-
*/
-
String s2 =
"1"+s1;
-
String s3 =
"123";
-
System.out.println( s2 == s3.intern());
Q: 有人会问为什么两个常量字符串相加得到的对象就会入常量池(参考上面第3点),而加上一个变量就不会???
A: 这是由于Jvm优化机制决定的,Jvm会有编译时的优化,如果是两个常量,Jvm会认定这已经是不可变的,就会直接在编译 时和常量池进行判断比对等,但是如果是加上一个变量,说明最后运行得出的结果是可变的,Jvm无法在编译时就确定执 行之后的结果是多少,所以不会把该结果和常量池比对。
String.intern()方法分析:
在分析intern()方法时候,首先去官网查看api的相关解释
楼主大概翻译一下,意思就是:当调用这个方法时候,如果常量池包含了一个<调用 code equals(Object)>相等的常量,就把该 常量池的对象返回,否则,就把当前对象加入到常量池中并且返回当前对象的引用。楼主用更加白话的方式解释一下:
判断这个常量是否存在于常量池。
如果存在,则直接返回地址值(只不过地址值分为两种情况,1是堆中的引用,2是本身常量池的地址)
如果是引用,返回引用地址指向的堆空间对象地址值
如果是常量,则直接返回常量池常量的地址值,
如果不存在,
将当前对象引用复制到常量池,并且返回的是当前对象的引用(这个和上面最开始的字符串创建分析有点不同)
实战分析问题:
基本上读者看到这里就可以尝试着去回过头文章一些示例代码,看看输出结果,这里分析一下上文存在的一个例子
-
public static void main(String[] args){
-
String s1 =
new String(
"1")+
new String(
"23");
-
s1.intern();
-
String s2 =
"123";
-
System.out.println( s1 == s2);
-
}
分析: 1 首先看第一行是两个new String类型的字符串相加(详见上文第4点)可知道,这里创建了堆中有3个对象 一个是1, 一个是23,还有一个是结果 123,由于程序刚启动常量池也没有 1,23 所以会在常量池创建2个对象 (1 , 23)
2 当s1执行intern()方法之后,首先去常量池判断有没有123,此时发现没有,所以会把对象加入到常量池,并且返回 当前对象的引用(堆中的地址)
3 当创建s2时候(详见上文第1点),并且找到常量池中123,并且把常量池的地址值返回给s2
4 由于常量池的地址值就是s1调用intern()方法之后得到的堆中的引用,所以此时s1和s2的地址值一样,输出true。
-
public static void main(String[] args){
-
String s1 =
new String(
"1")+
new String(
"23");
-
String s2 =
"123";
-
s1.intern();
-
System.out.println( s1 == s2);
-
}
如果把中间两行换一个位置,那输出就是false了,下面在分析一下不同点,上面分析过的不再赘述。
1.在执行到第二行的时候String s2 = "123"时,发现常量池没有123,所以会先创建一个常量
2.在当s1调用intern()方法时,会发现常量池已经有了123对象,就会直接把123的常量给返回出去,但是由于返回值并没有接 收,所以此时s1还是堆中地址,则输入false;如果代码换成 s1 = s1.intern();那s1就会重新指向常量池了,那输出就为true;
结尾:
由于本文都是在Jdk1.8版本(1.7由于已经把常量池放在堆中了和1.8结果应该一样)执行,如果有读者要探究1.6的相关问题,主要知道jdk1.6在创建String和1.8(1.7)的堆和常量池有一些不同实现,那相关问题就很清楚了,这里楼主没有涉及到1.6的相关问题,以免混淆读者,如果需要探究1.6以及相关问题,欢迎在评论区留言。
楼主很多时候都说了句柄指针的概念相对抽象,如果读者想知道两个对象是否是指向了相同的地址,可用 System.identityHashCode(Object x) 来验证。
最后本文也肯定有一些不足之处,欢迎大家在评论区留言,学无止境,大家加油。(如需转载,请标注出处,谢谢!)