疯狂的String

在java中字符串是我们比较常用的一个类型,字符串是不可变的,类被声明为final , 存储字符的char[] value数据也被声明为final ,我们对String真的了解么?我们看一下String是有多么的疯狂。本文中是在JDK8下面测试,不同的JDK可能会有不一样的结果。

测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private static String B = "B";
private static String K = "K";
private static final String B1 = "B";
private static final String K1 = "K";
private static void demo1() {
String s1 = "BK";
String s2 = "BK";
String emp = "";
String s3 = "B" + "K";
String s4 = "B" + emp + "K";
String s5 = "B" + new String("K");
String s6 = new String("BK");
String s7 = s6.intern();
String s8 = "B";
String s9 = "K";
String s10 = s8 + s9;
String s11 = B + K;
String s12 = B1 + K1;
System.out.println("1 : s1 == s2 : " + (s1 == s2));
System.out.println("2 : s1 == s3 : " + (s1 == s3));
System.out.println("3 : s1 == s4 : " + (s1 == s4));
System.out.println("4 : s1.equals(s4): " + s1.equals(s4));
System.out.println("5 : s1 == s5 : " + (s1 == s5));
System.out.println("6 : s1 == s10 : " + (s1 == s10));
System.out.println("7 : s5 == s6 : " + (s5 == s6));
System.out.println("8 : s1 == s7 : " + (s1 == s7));
System.out.println("9 : s1 == s11 : " + (s1 == s11));
System.out.println("10: s1 == s12 : " + (s1 == s12));
}
public static void main(String[] args) {
demo1();
}

看到这里可以停下来想一下每一个输出的结果是什么?

收藏一下本文,回家在电脑上亲自试一下结果,结果可能出乎你的意料。

输出结果

1
2
3
4
5
6
7
8
9
10
1 : s1 == s2 : true
2 : s1 == s3 : true
3 : s1 == s4 : false
4 : s1.equals(s4): true
5 : s1 == s5 : false
6 : s1 == s10 : false
7 : s5 == s6 : false
8 : s1 == s7 : true
9 : s1 == s11 : false
10: s1 == s12 : true

看到结果可能中有些会和我们想象中的不一样,出乎你的意料,到现在头脑已经有些疯狂了,静下心来仔细想一下

为什么是这样的结果

  • 常量池中一般存放.class文件中的常量,主要包含字面量(如文本字符串、声明为final的常量值等)和
    符号引用量(类和接口的全限定名、字段名称和描述符、方法名称和描述符)这些信息会存储在常量池中,这个常量池被称为静态常量池
    • 在类完成装载操作之后,在运行阶段也可以将新的常量放到池中,比如String的intern()方法就是这样的,这时候操作的常量池被称为动态常量池
  • 结果1. s1 == s2 : true
    对于这条输出应该不会有问题,”BK”是一个字符串常量,在编译阶段就会存放到静态常量池中比如存放地址为0x01,所以两个变量都指向常量池的同一个对象,比较它们的地址相等,结果是true
  • 结果2 : s1 == s3 : true
    s1的指向常量池中”BK”的内存地址0x01
    s3因为是两个常量相加,编译器会将其优化为s3="BK"是终指向的也地址0x01
    所以两个对象的地址也是相同的,结果为true
  • 结果3 : s1 == s4 : false
    s4因为连接的字符中存在一个变量emp引用类型所以不编译器不会对其进行优化,产生的对象不会被加入到字符串池中,而是在运行时在堆上创建一个新的对象s4值为”BK”,并将s4指向堆上对象的引用地址 0x02
    这时s1 的地址为0x01 s4的地址为0x02两个变量指向了不同的地址,所以返回结果是false
  • 结果4 : s1.equals(s4): true
    因为使用的是equals方法比较,所以首先比较两个对象地址是还相同,如果不相同,再去比较两个地址里面的内容是还相等,很显然,两个对象引用的地址不同,内容相同所以结果是true
  • 结果5 : s1 == s5 : false
    String s5 = "B" + new String("K");
    B是常量会在常量池,new操作这部分不是已知字面量,只能运行时才能确定结果,在编译器不优化的情况下,运行时会在堆上创建一个对象值为”BK”的对象, 同时让s5指各它的地址0x03
    s1的地址是0x01,所以比较两个对象的地址不是同一个结果 为false
  • 结果6 : s1 == s10 : false
    1
    2
    3
    >  String s8 = "B";
    > String s9 = "K";
    > String s10 = s8 + s9;
在编译时`s8`,`s9`的字面量是确定的,所以在常量池中会有`B`和`K`,`s8`,`s9` 分别指向常量池的两个地址

s10赋值时,使用的是s8,s9两个变量,变量初始化时候是指向常量池,但是在运行时候指向什么地址,鬼才知道,所以在编译期是不可预料的,编译器是不做优化的,只有在运行时才会在堆中拼接B和K生成新对象在堆中,并将引用赋给s10,比如这时候分配的地址是0x04,这时候对比s1的地址0x01s10的地址0x04, 返回结果一定是false

  • 结果 7 : s5 == s6 : false
    s5和s6的赋值时,因为存在new对象,所以在编译其无法确定其字面量,只能在运行时才会确定,所以s5和s6都是堆上的两个对象,在比较两个对象的地址,一定是不相等的,所以结果一定是false
  • 结果8 : s1 == s7 : true
    String s7 = s6.intern();
    在运行到该行代码时,s6的值是确定的,然后调用intern方法,发现常量池中已经存在BK,所以s7指向常量池中的地址,在比较s1s7的值时,返回结果为 true
  • 结果9 : s1 == s11 : false
    String s11 = B + K;
    BK是静态变量,在编译期是无法确定字面量,所以只能在运行时才能确定其真实值,所以s11指向的是堆上的一个地址,在比较s1s11时候,返回的结果为false
  • 结果10: s1 == s12 : true
    String s12 = B1 + K1;
    因为B1K1static final修饰
    对于static final类型,在类加载的准备阶段就会被赋上正确的值,因为static final类型被认为是常量,两个常量相加之后的值也是常量,字面量是确定的,这时候BK在常量池中已经存在,所以s12也是指向常量池中的地址,在比较s1s12的地址返回的结果是true

    总结

    按照下面的规则来判断,不会被String搞迷路
    • 变量在定义时如果存在new String()非static final修饰的变量进行+运算,都只能在运行时才能确定结果,所产生的对象一定是在堆上面
    • 如果一定变量在定义时字面量已经确定,会在常量池中创建,并且变量指向常量池中的地址
    • 在编译期可以确定的常量才会被放入常量池,在运行时的变量,如果不调用intern方法是不会把常量添加到常量池中的
    • statci final修饰的变量在准备阶段已经确定正确的值,会被认为是常量,存放在常量池中

再来一发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 比如我们玩游戏时候经常用的QWER四个键,可以组合出不同的操作
*/
private static void demo2() throws NoSuchFieldException, IllegalAccessException {
//定义操作A QWER
String operateA = "QWER";
//获取字符串对象中存储字符的value字段 private final char value[];
Field valueFieldString = String.class.getDeclaredField("value");
valueFieldString.setAccessible(true);
//获取value数组中的值 [Q,W,E,R]
char[] value = (char[]) valueFieldString.get(operateA);
//将value数组的值改为 [Q,Q,Q,Q]
value[1] = 'Q';
value[2] = 'Q';
value[3] = 'Q';
//定义操作B和操作A一样 QWER
String operateB = "QWER";
System.out.println("1.operateA :" + operateA);
System.out.println("2.operateB :" + operateB);
System.out.println("3.operateA == operateB :" + (operateA == operateB));
System.out.println("4.\"QWER\" == operateB :" + ("QWER" == operateB));
System.out.println("5.\"QQQQ\" == operateA : " + ("QQQQ" == operateA));
System.out.println("6.operateA.equals(\"QQQQ\") : " + operateA.equals("QQQQ"));
System.out.println("7.operateA.equals(\"QWER\") : " + operateA.equals("QWER"));
System.out.println("8.\"QWER\".equals(\"QQQQ\") : " + "QWER".equals("QQQQ"));
}

输出结果

1
2
3
4
5
6
7
8
1.operateA :QQQQ
2.operateB :QQQQ
3.operateA == operateB :true
4."QWER" == operateB :true
5."QQQQ" == operateA : false
6.operateA.equals("QQQQ") : true
7.operateA.equals("QWER") : true
8."QWER".equals("QQQQ") : true

为什么会输出这样的结果

图片.png

没错,这结果简直让人抓狂,太离谱了,

1
2
3
6.skillA.equals("QQQQ") : true
7.skillA.equals("QWER") : true
8."QWER".equals("QQQQ") : true

凭直觉大多数人会认为6 和 7 应该是一个对一个错,8应该是false,可这结果结果倒底怎么了,刚看到这结果感觉很惊讶what a fuck !

代码逻辑

  1. 首先我们先定义一个操作A QWER,
  2. 对A底层的字符数组进行修改,修改为QQQQ(直接对底层数据修改,直接改的地址里面存放的内容,而不是通过String运算符修改)
  3. 再定义一个操作B,同样为QWER
  4. 然后进行各种比较,判断输出内容

分析

编译阶段搞的事情

1、由于QWER在编译阶段是一个字面量,所以QWER在常量池中分配空间0x01,并存储

2、operateA指向常量池中QWER所在的地址0x01

3、operateB的字面量也是QWER,这时候常量池中也存在,引用直接指向地址0x01

最终的结果是operateAoperateB指向了同一个地址0x01 ,字面量为QWER的地址是0x01

字面量为QQQQ的变量指向了0x05的地址

运行阶段搞的事情

  1. 读取operateA的值,然后通过反射获取到字符存储数据的char[]数组value

  2. 将value里面的内容个性为QQQQ

    String类equals方法的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public boolean equals(Object anObject) {
    if (this == anObject) {
    return true;
    }
    if (anObject instanceof String) {
    String anotherString = (String)anObject;
    int n = value.length;
    if (n == anotherString.value.length) {
    char v1[] = value;
    char v2[] = anotherString.value;
    int i = 0;
    while (n-- != 0) {
    if (v1[i] != v2[i])
    return false;
    i++;
    }
    return true;
    }
    }
    return false;
    }

结果分析

接下来就是进行各种比较了,在看结果之间先看一下String equals方法的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}

先判断对象的地址是不是同一个,如果指向同一个地址,那么就认为两个对象相等

如果指向的地址不相等,然后判断长度是还相等,如果长度不相等,则返回false

如果地址不等,长度相等的话,就取出地址中的值,逐位进行比较,如果有一位不相等则返回false ,否则返回 true

接下来我们逐个看一下结果

  • 1.operateA :QQQQ
    ​ 在运行到该行代码时候,地址中的值已经被修改了,所以operateA的值为QQQQ

  • 2.operateB :QQQQ
    operateB和operateA指向了同一个引用,在运行到该行代码时候,地址中的值已经是QQQQ了 ,所以operateB的值为QQQQ

  • 3.operateA == operateB :true
    因为operateA和operateB的指向的地址都是0x01所以比较两个对象的地址值是true

  • 4.”QWER” == operateB :true
    “QWER”这个匿名变量的字面量是个常量,并且在常量池中已经存在,所以指向常量池的0x01地址,operateB的地址也是0x01所以比较两个对象的地址值是true

  • 5.”QQQQ” == operateA : false
    “QQQQ”这个匿名变量的字面量是个常量,在常量池中不存在,所以会被加入到常量池中地址为 0x05,operateA的地址也是0x01所以比较两个对象的地址值是false

  • 6.operateA.equals(“QQQQ”) : true
    operateA指向的内存地址是0x01,但是值是QQQQ
    “QQQQ”指向的内存地址是0x05,值为QQQQ

    在对比equals方法时,先比较两个对象的地址值是false,然后再去比较两个地址中的值是否相等,因为0x01地址中的值是QQQQ与0x05地址的值QQQQ的值相等,所以结果是 true

  • 7.operateA.equals(“QWER”) : true
    “QWER” 指向的内存地址是0x01,值是QQQQ
    operateA指向的内存地址是0x01,值是QQQQ
    在对比equals方法时,先比较两个对象的地址值是false,然后再去比较两个地址中的值是否相等,因为0x01地址中的值已经是QQQQ与0x05的值相等,所以结果是 true

  • 8.”QWER”.equals(“QQQQ”) : true
    “QWER”指向的内存地址是0x01,值是QQQQ
    “QQQQ”指向的内存地址是0x05,值为QQQQ
    在对比equals方法时,先比较两个对象的地址值是false,然后再去比较两个地址中的值是否相等,因为0x01地址中的值是QQQQ与0x05地址的值QQQQ的值相等,所以结果是 true

总结

其实这个示例中,主要是直接操作了底层的数组,破坏了字符串的不变性,才会出现这么奇怪的现象。

此文结束

喜欢可以关注个人公众号albk

BK wechat
扫一扫,用手机访问本站
---------------- 本文结束 ----------------