本文转载自疯狂的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的地址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修饰的变量在准备阶段已经确定正确的值,会被认为是常量,存放在常量池中

再来一发

/**
* 比如我们玩游戏时候经常用的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 !

代码逻辑

  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方法的代码

   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

总结

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

疯狂的String的更多相关文章

  1. 疯狂Java笔记

    第四章: 4.6深入数组 1.栈内存和堆内存 每个方法都会建立自己的内存栈,在这个方法内定义的变量会逐个放入栈内存里,随着方法的执行结束,这个方法的内存栈也将自然销毁.因此,所有在方法中定义的局部变量 ...

  2. 疯狂java学习笔记之面向对象(八) - static和final

    一.static: 1.static是一个标识符: - 有static修饰的成员表明该成员是属于类的; - 没有static修饰的成员表明该成员是属于实例/对象的. 2.static修饰的成员(Fie ...

  3. 《疯狂Java:突破程序员基本功的16课》读书笔记-第二章 对象与内存控制

    Java内存管理分为两个方面:内存分配和内存回收.这里的内存分配特指创建Java对象时JVM为该对象在堆内存中所分配的内存空间.内存回收指的是当该Java对象失去引用,变成垃圾时,JVM的垃圾回收机制 ...

  4. 《疯狂java-突破程序员基本功的16课 》笔记总结

    本人最近读完<疯狂java-突破程序员基本功的16课 >读完后,感觉对java基础又有了新的认识,在这里总结一下:一.数组与内存控制 1.1 数组初始化     java语言的数组是静态的 ...

  5. Java基础进阶整理

    Java学习笔记整理 本文档是我个人整理的,首先是想通过完成本文档更加扎实自己的基础加强对java语言的理解,然后就是想给入了门的同志们做下贡献. 当然,本文档主要是对java语言基础(当然还有很多基 ...

  6. Java面向对象进阶篇(包装类,不可变类)

    一. Java 8的包装类 Java中的8种基本数据类型不支持面向对象的变成机制,也不具备对象的特性:没有成员变量,方法可以调用.为此,Java为这8 种基本数据类型分别提供了对应的 包装类(Byte ...

  7. 查询一周最近一周的数据,date String 随意转换,更有疯狂的排序

    --查看一周随访的详情70007PROCEDURE GET_PATIENT_WEEKFU ( userId in NUMERIC, time in date, V_LIST OUT MYCURSOR ...

  8. 0038 Java学习笔记-多线程-传统线程间通信、Condition、阻塞队列、《疯狂Java讲义 第三版》进程间通信示例代码存在的一个问题

    调用同步锁的wait().notify().notifyAll()进行线程通信 看这个经典的存取款问题,要求两个线程存款,两个线程取款,账户里有余额的时候只能取款,没余额的时候只能存款,存取款金额相同 ...

  9. [LeetCode] Reverse Words in a String 翻转字符串中的单词

    Given an input string, reverse the string word by word. For example, Given s = "the sky is blue ...

随机推荐

  1. Linux内存运维操作及常用命令

    Linux内存运维操作及常用命令 1.问题诊断 1.1 什么是 Linux 服务器 Load Average? 1.2如何查看 Linux 服务器负载? 1.3服务器负载高怎么办? 1.4如何查看服务 ...

  2. Lambda表达式及相关练习

    语法格式一无参数无返回值 语法格式二有一个参数并且无返回值 语法格式三若只有一个参数小括号可以省略不写 语法格式四有两个以上的参数有返回值并且Lambda体中有多条语句 语法格式五若Lambda体中只 ...

  3. 设计模式c++(3)——观察者模式

    观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖着都会收到通知并自动更新. 当两个对象之间松耦合,他们依然可以交互,但是不太清楚彼此的细节.观察者模式提供了一种对象设 ...

  4. redis学习教程三《发送订阅、事务、连接》

    redis学习教程三<发送订阅.事务.连接>  一:发送订阅      Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息.Redi ...

  5. JS:replace

    JavaScript中replace() 方法如果直接用str.replace("-","!") 只会替换第一个匹配的字符. 而str.replace(/-/g ...

  6. Spring boot 自定义注解标签记录系统访问日志

    package io.renren.common.annotation; import java.lang.annotation.Documented; import java.lang.annota ...

  7. Pytest(16)随机执行测试用例pytest-random-order

    前言 通常我们认为每个测试用例都是相互独立的,因此需要保证测试结果不依赖于测试顺序,以不同的顺序运行测试用例,可以得到相同的结果. pytest默认运行用例的顺序是按模块和用例命名的 ASCII 编码 ...

  8. 从单页应用(SPA)到服务器渲染(SSR)

    从单页应用(SPA)到服务器渲染(SSR) 情景回顾 在学习Vue开发一个电商网站的管理后台时,使用到了一个组件 vue-quill-editor 主要是一个快捷的一个富文本编辑器 在使用这个组件的组 ...

  9. Codeforces Round #684 (Div. 2)【ABC1C2】

    比赛链接:https://codeforces.com/contest/1440 A. Buy the String 题解 枚举字符串中 \(0\) 或 \(1\) 的个数即可. 代码 #includ ...

  10. 【poj 1961】Period(字符串--KMP 模版题)

    题意:给你一个字符串,求这个字符串到第 i 个字符为止的重复子串的个数. 解法:判断重复子串的语句很重要!!if (p && i%(i-p)==0) printf("%d % ...