Java基础-面试中的 String 和 ==

面试中的String和**==**是一对高频出现的恩爱同志,是对求职者永不消失的爱👨‍❤️‍👨。

1. String s=“XXX” 和 String s=new String(“XXX”)

这是对String最常见的两种赋值。但是我们知道String不是基础类型,也不是包装类型,那他为什么能写String s=“XXX”;这样的语句?而且都是"XXX" 的字符串,他们能“==”吗?

测试代码:

    public static void main(String[] args) {
        //对s1,s2s3,s4都赋“A”
        String s1="A";
        String s2=new String("A");
        String s3="A";
        String s4=new String("A");
        //这里的==比较地址的,因为不是基础类型
        System.out.println(s1 == s2);
        System.out.println(s1 == s3);
        System.out.println(s2 == s4);
    }

结果:
在这里插入图片描述
这个答案,骚年想到了吗,我们再来看看字节码文件对四个A的操作:

   //s1
   L0
    LINENUMBER 23 L0
    LDC "A"       //常量池中常量值(int, float, string reference, object reference)入栈
    ASTORE 1     //将栈上值(这里是string reference)赋予变量1
   //s2
   L1
    LINENUMBER 24 L1
    NEW java/lang/String
    DUP
    LDC "A"
    INVOKESPECIAL java/lang/String.<init> (Ljava/lang/String;)V
    ASTORE 2
   //s3
   L2
    LINENUMBER 25 L2
    LDC "A"
    ASTORE 3
   //s4
   L3
    LINENUMBER 26 L3
    NEW java/lang/String
    DUP
    LDC "A"
    INVOKESPECIAL java/lang/String.<init> (Ljava/lang/String;)V
    ASTORE 4

对s1和s3选择的是LDC "A"语句,字节码LDC【其作用:常量池中常量值(int, float, string reference, object reference)入栈。那常量池中的这个string reference从哪里?简单来说,当第一次加载到“A”时,如果字符串常量池中没有,则创建一个返回引用地址;如果存在则直接返回引用地址】,而对s2和s4因为我们显示的选择了new String(),通过LDC从常量池获取“A”,然后INVOKESPECIAL调用String的初始化构造器。
因为s1和s3的string reference都来自常量池,他们都会指向同一内存地址。s2和s4则都进行了new String(),开辟了各自的内存空间,所以他们指向地址将不同。但是我们知道String中存放数据的是:
在这里插入图片描述
这个被final修饰value。有如下:
在这里插入图片描述

上面基本搞清后,我们还需要探讨一个方法String.intern():

在这里插入图片描述
简单来说就是:

  • 若当前String内容在常量池则返回常量池中String引用;
  • 要是不存在,则加入常量池,再返回引用。

同时这里要明确的是JDK 1.7后,HotSpot 把原本放在永久代的字符串常量池、静态变量等移出。此时的intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,区别在于,如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。

案列:

    public static void main(String[] args) {
        String s1="A";
        String s2=new String("A");
        String s3="A";
        String s4=new String("A");
        //这里的==比较地址的,因为不是基础类型
        System.out.println(s1 == s2.intern());
        System.out.println(s2 == s4.intern());
        System.out.println(s2.intern() == s4);
        System.out.println(s2.intern() == s4.intern());
    }

猜对了吗?
在这里插入图片描述
在这里插入图片描述

2. String s=“XXX” 遇上+“XXX” 和new String(“XXX”)

案列:

 public static void main(String[] args) {
        String s1="A";
        String s2="B";
        String s3="AB";
        String s4=new String("A");
        String s5=new String("B");
        String s6=new String("AB");

        String s7="A"+"B";
        String s8=s1+s2;
        String s9=s1+"B";
        String s10=s1+s5;
        String s11=s1+new String("B");
        String s12=s4+s5;
        String s13=new String("A")+new String("B");

        System.out.println(s3 == s7);
        System.out.println(s3 == s8);
        System.out.println(s3 == s9);
        System.out.println(s3 == s10);
        System.out.println(s3 == s11);
        System.out.println(s3 == s12);
        System.out.println(s3 == s13);
}

输出:
在这里插入图片描述
其实不难,我们看看字节码就知道再编译层面是怎么完成的这一步(从s7开始):

  • 对于 String s7=“A”+“B”;字节码:
 LINENUMBER 19 L6
    LDC "AB"   //在这里“A”+“B”直接就拼接成了“AB”,并查常量池
    ASTORE 7

这样ASTORE到s7的引用地址还是之前就在的s1指向的,所以两者的==必然相等。
结论:String s=“XXX”+“XXX“ ;=>String s=“XXXXXX“ ;其实很容易理解因为“XXX”和“XXX”都是很明确的String型了,所以他们的拼接类型要是明确的String。

  • 对于 String s8=s1+s2;字节码:
    LINENUMBER 20 L7
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    ALOAD 1
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 2
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ASTORE 8

在这里居然创建一个StringBuilder,然后通过ALOAD 指令将s1 s2 加载入操作区,再通过StringBuilder.append 加入到StringBuilder中,最后返回StringBuilder.toString ():正因为返回的是new String()导致开辟了新内存空间并指向,才使==为flase。
在这里插入图片描述
在整个过程中可能创建了三个对象StringBuilder和最后返回的new String()。可能还要常量池中的一个。
回到正题,为什么会这样?其实主要原因还是在编译过程中,java无法像 String s7=“A”+“B”;一样明确的知道 “A”、"B"的是String类型。在这里 String s8=s1+s2;如果我们想知道 s1、s2;类型是否是String类型的话还得通过反射方法进行判断,而且当不是String类型时,还是得进行转换,所以java在编译过程中对这种不能明确判断=后面所有类型的,都采用这种方法。

  • 对于 String s9=s1+“B”;字节码:
   LINENUMBER 21 L8
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    ALOAD 1
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "B"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ASTORE 9

同上无法明确判断类型,但是这里与上面不同的是有可能整个过程中创建了三个对象。StringBuilder和最后返回的new String()除外。有可能的第三个就是 LDC “B”,这个在常量池中的 “B”,要是此前没有就会新建一个。

  • 对于 String s10=s1+s5;字节码:
    同上String s8=s1+s2;
  • 对于 String s11=s1+new String(“B”);字节码:
 NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    ALOAD 1
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    NEW java/lang/String
    DUP
    LDC "B"
    INVOKESPECIAL java/lang/String.<init> (Ljava/lang/String;)V
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ASTORE 11

这里一样无法确认类型,唯一不同点在 LDC "B"而已。

  • 对于 String s13=new String(“A”)+new String(“B”);字节码:
    LINENUMBER 25 L12
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    NEW java/lang/String
    DUP
    LDC "A"
    INVOKESPECIAL java/lang/String.<init> (Ljava/lang/String;)V
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    NEW java/lang/String
    DUP
    LDC "B"
    INVOKESPECIAL java/lang/String.<init> (Ljava/lang/String;)V
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ASTORE 13

到这,我想最迷惑的还是什么会进常量池,像这的“A”和“B”通过 LDC "A"和LDC "B"获得,那他们肯定在常量池,那“AB”呢?在下一节我们试着讨论看看。

3. 亿点点升华

3.1 对于intern()
  • 若当前String内容在常量池则返回常量池中String引用;
  • 如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。

我们利用这一特性展开测试,讨论什么情况下String的值会在初始化时直接加入到常量池中呢?:

测试1:new String(“AB”);

    public static void main(String[] args) {
        String s1=new String("AB");
        //identityHashCode用于输出一个hashcode,不同对象其值不同
        System.out.println(System.identityHashCode(s1));
        System.out.println(System.identityHashCode(s1.intern()));
        //方法的调用,阔以不接收其返回值
        s1.intern();
        String s2="AB";
        System.out.println(System.identityHashCode(s2));
        System.out.println(s1 == s2);
    }

输出结果:
在这里插入图片描述
这里我们可以看到s1和s1.intern()返回值是不同的,而s1.intern()返回值相同的s2,在s1后才定义。
简单来说,就是我们在执行String s1=new String(“AB”)语句时,不仅会在内存中分配下图中的”AB,21685669“这块内存地址,同时AB字符串会进入到常量池中。这样我们执行System.identityHashCode(s1)时,其值就是21685669。到执行 System.identityHashCode(s1.intern())时,我们假设一下如果常量池中不存在AB,那么输出的是不是应该也是21685669(对s1的引用)。当然实际输出的是2133927002,因为AB在一开始就进入了常量池。情况就如下图:
在这里插入图片描述
当再执行String s2="AB"时,因为常量池中有了AB,此时的情况就如下:
在这里插入图片描述

测试2:A+new String(“B”);

 public static void main(String[] args) {
        String s1="A"+new String("B");
        System.out.println(System.identityHashCode(s1));
        System.out.println(System.identityHashCode(s1.intern()));
        s1.intern();
        String s2="AB";
        System.out.println(System.identityHashCode(s2));
        System.out.println(s1 == s2);
    }

所以这里s1中的“A”和“B”会进入字符串常量池,但是对“AB”不会进入
在这里插入图片描述
测试3:new String(“A”)+new String(“B”);

 public static void main(String[] args) {
        String s1=new String("A")+new String("B");
        System.out.println(System.identityHashCode(s1));
        System.out.println(System.identityHashCode(s1.intern()));
        s1.intern();
        String s2="AB";
        System.out.println(System.identityHashCode(s2));
        System.out.println(s1 == s2);
    }

同上
在这里插入图片描述
测试4:总结

  public static void main(String[] args) {
        String s1 = new String("A")+new String("B");
        s1.intern();
        String s2 = "AB";
        String s3 = new String("A")+new String("B");
        s3.intern();
        System.out.println(s2 == s1);
        System.out.println(s2 == s3);
    }

这里我们知道s1中的“AB”是不会进入字符串的,到调用s1.intern();将在常量池中生成一个对堆中的对s1的引用。当s2声明是将会指向常量池中的这个引用,但s3执行intern();仅仅是返回常量池中已经存在的引用而已,但是这个intern();执行完,其结果没有接收对象,所有s3还是指向内存。
在这里插入图片描述

3.2 对于=后面类型是否明确
public class Test_1 {
    public static final String s1 = "A";
    public static final String s2 = "B";
    public static final String s3;
    public static final String s4;
    static {
        s3 = "A";
        s4 = "B";
    }
    public static void main(String[] args) {
        String s5 = s1 + s2;
        String s6 = s3 + s4;
        String s = "AB";
        System.out.println(s==s5);
        System.out.println(s==s6);
    }
}

在这里插入图片描述
这个结果意外不?这说明s1和s2是类型明确的String了,但 s3 和 s4还不是明确的String。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值