疯狂的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的值为QQQQ2.operateB :QQQQ
operateB和operateA指向了同一个引用,在运行到该行代码时候,地址中的值已经是QQQQ了 ,所以operateB的值为QQQQ3.operateA == operateB :true
因为operateA和operateB的指向的地址都是0x01所以比较两个对象的地址值是true4.”QWER” == operateB :true
“QWER”这个匿名变量的字面量是个常量,并且在常量池中已经存在,所以指向常量池的0x01地址,operateB的地址也是0x01所以比较两个对象的地址值是true5.”QQQQ” == operateA : false
“QQQQ”这个匿名变量的字面量是个常量,在常量池中不存在,所以会被加入到常量池中地址为 0x05,operateA的地址也是0x01所以比较两个对象的地址值是false6.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的值相等,所以结果是 true8.”QWER”.equals(“QQQQ”) : true
“QWER”指向的内存地址是0x01,值是QQQQ
“QQQQ”指向的内存地址是0x05,值为QQQQ
在对比equals方法时,先比较两个对象的地址值是false,然后再去比较两个地址中的值是否相等,因为0x01地址中的值是QQQQ与0x05地址的值QQQQ的值相等,所以结果是 true
总结
其实这个示例中,主要是直接操作了底层的数组,破坏了字符串的不变性,才会出现这么奇怪的现象。
疯狂的String的更多相关文章
- 疯狂Java笔记
第四章: 4.6深入数组 1.栈内存和堆内存 每个方法都会建立自己的内存栈,在这个方法内定义的变量会逐个放入栈内存里,随着方法的执行结束,这个方法的内存栈也将自然销毁.因此,所有在方法中定义的局部变量 ...
- 疯狂java学习笔记之面向对象(八) - static和final
一.static: 1.static是一个标识符: - 有static修饰的成员表明该成员是属于类的; - 没有static修饰的成员表明该成员是属于实例/对象的. 2.static修饰的成员(Fie ...
- 《疯狂Java:突破程序员基本功的16课》读书笔记-第二章 对象与内存控制
Java内存管理分为两个方面:内存分配和内存回收.这里的内存分配特指创建Java对象时JVM为该对象在堆内存中所分配的内存空间.内存回收指的是当该Java对象失去引用,变成垃圾时,JVM的垃圾回收机制 ...
- 《疯狂java-突破程序员基本功的16课 》笔记总结
本人最近读完<疯狂java-突破程序员基本功的16课 >读完后,感觉对java基础又有了新的认识,在这里总结一下:一.数组与内存控制 1.1 数组初始化 java语言的数组是静态的 ...
- Java基础进阶整理
Java学习笔记整理 本文档是我个人整理的,首先是想通过完成本文档更加扎实自己的基础加强对java语言的理解,然后就是想给入了门的同志们做下贡献. 当然,本文档主要是对java语言基础(当然还有很多基 ...
- Java面向对象进阶篇(包装类,不可变类)
一. Java 8的包装类 Java中的8种基本数据类型不支持面向对象的变成机制,也不具备对象的特性:没有成员变量,方法可以调用.为此,Java为这8 种基本数据类型分别提供了对应的 包装类(Byte ...
- 查询一周最近一周的数据,date String 随意转换,更有疯狂的排序
--查看一周随访的详情70007PROCEDURE GET_PATIENT_WEEKFU ( userId in NUMERIC, time in date, V_LIST OUT MYCURSOR ...
- 0038 Java学习笔记-多线程-传统线程间通信、Condition、阻塞队列、《疯狂Java讲义 第三版》进程间通信示例代码存在的一个问题
调用同步锁的wait().notify().notifyAll()进行线程通信 看这个经典的存取款问题,要求两个线程存款,两个线程取款,账户里有余额的时候只能取款,没余额的时候只能存款,存取款金额相同 ...
- [LeetCode] Reverse Words in a String 翻转字符串中的单词
Given an input string, reverse the string word by word. For example, Given s = "the sky is blue ...
随机推荐
- Java——I/O入门相关练习代码
流的概念 读取文件 读取文件1 读取文件2 读取文件3 读取文件4 skip跳过n个字节后再开始读取 读取过程中暂停给当前位置做一个标记下一次从标记位置开始读取 序列流集合流 把三个流添加到集合中合并 ...
- Spark练习之Transformation操作开发
Spark练习之Transformation操作开发 一.map:将集合中的每个元素乘以2 1.1 Java 1.2 Scala 二.filter:过滤出集合中的偶数 2.1 Java 2.2 Sca ...
- POE供电
1.定位:POE (Power Over Ethernet)指的是在现有的以太网Cat.5布线基础架构不作任何改动的情况下,在为一些基于IP的终端(如IP电话机.无线局域网接入点AP.网络摄像机等)传 ...
- idea--忽略隐藏文件、文件夹的设置操作
文章由来 公司同事在群里问了个问题,如下: 为了大家看清,将图特意贴出来: 这人还删除idae重装了下,哈哈,才到群里问的. 解决思路(按顺序) 1.我让他直接拉会,共享桌面我给看了下,首先是open ...
- jvm系列五-java内存模型(2)
原作者系列文章链接:并发编程系列博客传送门 前言# 在网上看了很多文章,也看了好几本书中关于JMM的介绍,我发现JMM确实是Java中比较难以理解的概念.网上很多文章中关于JMM的介绍要么是照搬了一些 ...
- linux(6)查看进程ps命令
ps命令 Linux ps (英文全拼:process status)命令用于显示当前进程的状态,类似于 windows 的任务管理器 查看所有进程 ps -A 显示所有进程信息,连同命令行 ps - ...
- MySQL8.0数据库出现的问题——外码创建方式、外键约束两个引用列不兼容问题、check约束问题、用触发器代替check约束、关键字DELIMITER、删除添加索引、删除添加外键约束、和一些数据库方面的操作
一.首先先说一下我们都需要建立那些表 mysql> CREATE TABLE IF NOT EXISTS `student`( -> `sno` CHAR(8) NOT NULL, -&g ...
- .Net反编译实践记录
去壳 去壳可以使用 de4dot,源码在 这里.可用版本 下载地址. 使用方式为:.\de4dot.exe [path] 修改代码 反编译修改代码可以使用 dnSpy,源码在 这里.可用版本 下载地址 ...
- SPU与SKU概念
1. 什么是SPU SPU 是商品信息聚合的最小单位,是一组可复用.易检索的标准化信息的集合,该集合描述了一个产品的特性.即:某一款商铺的公共属性. 通俗点讲,属性值.特性相同的货品就可以称为一个 S ...
- OpenStack-知识点补充
登录计算节点查看进程 [root@compute ~]# ps aux | grep kvm root 824 0.0 0.0 0 0 ? S< 10:19 0:00 [kvm-irqfd-cl ...