疯狂的String
本文转载自疯狂的String
导语
在java中字符串是我们比较常用的一个类型,字符串是不可变的,类被声明为final , 存储字符的char[] value数据也被声明为final ,我们对String真的了解么?我们看一下String是有多么的疯狂。本文中是在JDK8下面测试,不同的JDK可能会有不一样的结果。
测试一下
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 : 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
> String s8 = "B";
> String s9 = "K";
> String s10 = s8 + s9;
在编译时`s8`,`s9`的字面量是确定的,所以在常量池中会有`B`和`K`,`s8`,`s9` 分别指向常量池的两个地址
s10赋值时,使用的是s8,s9两个变量,变量初始化时候是指向常量池,但是在运行时候指向什么地址,鬼才知道,所以在编译期是不可预料的,编译器是不做优化的,只有在运行时才会在堆中拼接B和K生成新对象在堆中,并将引用赋给s10
,比如这时候分配的地址是0x04
,这时候对比s1的地址0x01
的s10
的地址0x04
, 返回结果一定是false
-
结果 7 : s5 == s6 : false
s5和s6的赋值时,因为存在new对象,所以在编译其无法确定其字面量,只能在运行时才会确定,所以s5和s6都是堆上的两个对象,在比较两个对象的地址,一定是不相等的,所以结果一定是false -
结果8 : s1 == s7 : true
String s7 = s6.intern();
在运行到该行代码时,s6
的值是确定的,然后调用intern
方法,发现常量池中已经存在BK,所以s7
指向常量池中的地址,在比较s1
和s7
的值时,返回结果为 true -
结果9 : s1 == s11 : false
String s11 = B + K;
B
和K
是静态变量,在编译期是无法确定字面量,所以只能在运行时才能确定其真实值,所以s11
指向的是堆上的一个地址,在比较s1
和s11
时候,返回的结果为false -
结果10: s1 == s12 : true
String s12 = B1 + K1;
因为B1
和K1
被static final
修饰对于static final
类型,在类加载的准备阶段
就会被赋上正确的值,因为static final
类型被认为是常量,两个常量相加之后的值也是常量,字面量是确定的,这时候BK
在常量池中已经存在,所以s12
也是指向常量池中的地址,在比较s1
和s12
的地址返回的结果是true
总结
按照下面的规则来判断,不会被String搞迷路
- 变量在定义时如果存在
new String()
与非static final修饰的变量进行+运算
,都只能在运行时才能确定结果,所产生的对象一定是在堆上面 - 如果一定变量在定义时字面量已经确定,会在常量池中创建,并且变量指向常量池中的地址
- 在编译期可以确定的常量才会被放入常量池,在运行时的变量,如果不调用
intern
方法是不会把常量添加到常量池中的 statci final
修饰的变量在准备阶段已经确定正确的值,会被认为是常量,存放在常量池中
再来一发
/**
* 比如我们玩游戏时候经常用的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.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
为什么会输出这样的结果
没错,这结果简直让人抓狂,太离谱了,
6.skillA.equals("QQQQ") : true
7.skillA.equals("QWER") : true
8."QWER".equals("QQQQ") : true
凭直觉大多数人会认为6 和 7 应该是一个对一个错,8应该是false,可这结果结果倒底怎么了,刚看到这结果感觉很惊讶what a fuck !
代码逻辑
- 首先我们先定义一个操作A QWER,
- 对A底层的字符数组进行修改,修改为QQQQ(直接对底层数据修改,直接改的地址里面存放的内容,而不是通过String运算符修改)
- 再定义一个操作B,同样为QWER
- 然后进行各种比较,判断输出内容
分析
编译阶段搞的事情
1、由于QWER
在编译阶段是一个字面量,所以QWER
在常量池中分配空间0x01
,并存储
2、operateA
指向常量池中QWER
所在的地址0x01
3、operateB
的字面量也是QWER
,这时候常量池中也存在,引用直接指向地址0x01
最终的结果是operateA
和operateB
指向了同一个地址0x01
,字面量为QWER
的地址是0x01
字面量为QQQQ
的变量指向了0x05
的地址
运行阶段搞的事情
-
读取operateA的值,然后通过反射获取到字符存储数据的char[]数组value
-
将value里面的内容个性为QQQQ
String类equals方法的代码
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方法的逻辑
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
总结
其实这个示例中,主要是直接操作了底层的数组,破坏了字符串的不变性,才会出现这么奇怪的现象。