面试中的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。