黑马程序员技术交流社区

标题: 【成都校区】弄懂String常量池,String常见面试题 [打印本页]

作者: 小刀葛小伦    时间: 2019-11-15 15:24
标题: 【成都校区】弄懂String常量池,String常见面试题
本次代码使用 jdk 1.8版本,并且以下代码示例除了第一个写了main()方法,并且所有的示例分别独立运行 ,其余为了简洁做了缺省main()。在创建字符串分析的同时,都默认省略了栈中的句柄指向分析。进入正题之时,先科普几个知识点
String源码里面标注为final修饰的类,是一个不可改变的对象,那平时用到字符串A+字符串B怎么改变了呢,其实这里有涉及到String的常量池,首先常量池存放在方法区。
在jdk1.6时,方法区是存放在永久代(java堆的一部分,例如新生代,老年代)而在jdk1.7以后将字符串常量池移动到了的堆内存中  
在jdk1.8时,HotspotVM正式宣告了移除永久代,取而代之的是元数据区,元数据区存放在内存里面(存放一些加载class的信息),但是常量池还是和jdk1.7存放位置一样还是存放在堆中。
先看一波常见面试题:
    首先看一道常见的面试题,问输出的是什么?
[Java] 纯文本查看 复制代码

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" 创建  了几个对象,那如果题目稍微改变一下成下面这样,那输出的又是什么?
[Java] 纯文本查看 复制代码

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是本身常量池的地址)
如果是引用,返回引用地址指向的堆空间对象地址值
如果是常量,则直接返回常量池常量的地址值,
如果不存在,
在常量池中创建该常量,并返回此常量的地址值、
[Java] 纯文本查看 复制代码

String  s = "123";
//true,因为s已经在常量池里面了,s.intern()返回的也是常量池的地址,两者地址一样为true
System.out.println(s == s.intern());

2. new String创建字符串
与上面第一种方式相比,第一种方式效率高,下图解决了本文中的最开始出的部分面试题。
首先在堆上创建对象(无论堆上是否存在相同字面量的对象),
然后判断常量池上是否存在字符串的字面量,
如果不存在
在常量池上创建常量(并将常量地址值返回)
如果存在
不做任何操作
[Java] 纯文本查看 复制代码
String  s = new String("123");
/*
严格来说首先肯定会在堆中创建一个123的对象,然后再去判断常量池中是否存在123的对象,
如果不存在,则在常量池中创建一个123的常量(与堆中的123不是一个对象),
如果存在,则不做任何操作,解决了本文第一个面试题有问到创建几个对象的问题。
因为常量池中是有123的对象的,s指向的是堆内存中的地址值,s.intern()返回是常量池中的123的常量池地址,所以输出false
*/
System.out.println(s == s.intern());

3.两个双引号的字符串相加
判断这两个常量、相加后的常量在常量池上是否存在
如果不存在
则在常量池上创建相应的常量(并将常量地址值返回)
如果存在,则直接返回地址值(只不过地址值分为两种情况,1是堆中的引用,2是本身常量池的地址)
如果是引用,返回引用地址指向的堆空间对象地址值,
如果是常量,则直接返回常量池常量的地址值,
[Java] 纯文本查看 复制代码

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()的字符串相加
首先会创建这两个对象(堆中)以及相加后的对象(堆中)
然后判断常量池中是否存在这两个对象的字面量常量
如果存在
不做任何操作
如果不存在
则在常量池上创建对应常量   
[Java] 纯文本查看 复制代码

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()方法的时候再重点分析一次。
[Java] 纯文本查看 复制代码
String s2 = "123";
s1.intern();
System.out.println( s1 == s2);// false

5.双引号字符串常量与new String字符串相加
首先创建两个对象,一个是new String的对象(堆中),一个是相加后的对象(堆中)
然后判断双引号字符串字面量和new String的字面量在常量池是否存在
如果存在
不做操作
如果不存在
则在常量池上创建对象的常量
[Java] 纯文本查看 复制代码

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.双引号字符串常量与一个字符串变量相加
    首先创建一个对象,是相加后的结果对象(存放堆中,不会找常量池)
然后判断双引号字符串字面量在常量池是否存在
如果存在
不做操作
如果不存在
则在常量池上创建对象的常量
[Java] 纯文本查看 复制代码

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是本身常量池的地址)
如果是引用,返回引用地址指向的堆空间对象地址值
如果是常量,则直接返回常量池常量的地址值,
如果不存在,
将当前对象引用复制到常量池,并且返回的是当前对象的引用(这个和上面最开始的字符串创建分析有点不同)
实战分析问题:
         基本上读者看到这里就可以尝试着去回过头文章一些示例代码,看看输出结果,这里分析一下上文存在的一个例子
[Java] 纯文本查看 复制代码

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。
[Java] 纯文本查看 复制代码

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。







欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2