来自三段代码的疑惑~
几天前豆豆发现了发现的一段有意思的代码,主要是一开始没看明白,接着去找了洛洛,请教一下。你是不是很好奇是什么代码迷惑了豆豆的双眼?话不多说,我们先上代码。运行环境是jdk8。
洛洛看过代码以后说了这么一段话:“我们先来大胆的预测一下,我觉得下面的代码运行结果全是false或者全是true,豆豆,你觉得呢??”
String s1 = new String("aaa")+new String(""); s1.intern(); String s2 = "aaa"; System.out.println(s1==s2); String s3 = new String("bbb")+new String(""); s3 = s3.intern(); String s4 = "bbb"; System.out.println(s3==s4); String s5 = new String("hi") + new String("j"); s5.intern(); String s6 = "hij"; System.out.println(s5 == s6);
欲知后事如何,接着往下看……
实际代码的运行结果是:false true true
,惊不惊喜,刺不刺激?
如果说第一个输出false
,第二个输出true
还能理解,那么为什么到第三次比较又变成true
了呢?小朋友你是不是有很多问号……
在揭秘之前,我们先来补充一些基础知识。
堆
对所有线程可见 ,主要用来存储Java中的对象
方法区
主要存储类信息、常量池、静态变量、JIT编译后的代码等数据。可以理解为堆的逻辑部分。在《Java虚拟机规范》 中只规定了这个概念和作用,并没有具体实现。所以方法区只是一种规范。可以理解为代码中接口。
永久代
在HotSpot 中,在JDK1.2 ~ JDK6中使用永久代实现方法区,JDK1.7是永久代往元空间的过渡时期。
元空间
JDK1.8废弃永久代,变更为元空间,元空间不是虚拟机内存,而是本地内存。
永久代和元空间是具体实现, 方法区是一种规范。
class文件常量池
编译生成的 class 字节码文件,其结构中有一项是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
- 字面量就是我们经常用到的常量。包含字符串字面量(双引号括起来的字符串、类和方法名等)和声明为 final 的(基本数据类型)常量值
- 符号引用可以理解为是一组符号来描述所引用的目标 ,包括类和接口的全限定名(包括包路径的完整名)、字段的名称和描述符、方法的名称和描述符。
- 直接引用是指直接指向内存中某一地址的引用 。
运行时常量池
一个类加载到 JVM 中后对应一个运行时常量池
Class 文件常量池将在类加载后进入方法区的运行时常量池中存放
在类加载的解析阶段会把运行时常量池的符号引用替换成直接引用,这个过程需要查找字符串常量池
字符串常量池
- 是一个哈希表(StringTable) ,并且是全局的。
- 运行时常量池中的字符串字面量若是成员的,则在类的加载初始化阶段就使用到了字符串常量池;若是本地的,则在使用到的时候(执行此代码时)才会使用到字符串常量池。
- 在 jdk1.6(含)之前也是方法区的一部分,并且其中存放的是字符串的实例;
- 在 jdk1.7 中从永久代移动到了堆 ,存储的是字符串对象的引用,字符串实例是在堆中;
- jdk1.8 已移除永久代,字符串常量池跟随元空间被移出JVM内存,存放在本地内存当中,存储的也是对象引用。
接下来是时候理一理上面的代码了,代码中使用intern()
方法,官方对这块的解释如下所示:
/** * Returns a canonical representation for the string object. * <p> * A pool of strings, initially empty, is maintained privately by the * class {@code String}. * <p> * When the intern method is invoked, if the pool already contains a * string equal to this {@code String} object as determined by * the {@link #equals(Object)} method, then the string from the pool is * returned. Otherwise, this {@code String} object is added to the * pool and a reference to this {@code String} object is returned. * <p> * It follows that for any two strings {@code s} and {@code t}, * {@code s.intern() == t.intern()} is {@code true} * if and only if {@code s.equals(t)} is {@code true}. * <p> * All literal strings and string-valued constant expressions are * interned. String literals are defined in section 3.10.5 of the * <cite>The Java™ Language Specification</cite>. * * @return a string that has the same contents as this string, but is * guaranteed to be from a pool of unique strings. */ public native String intern();
一句话总结这个方法的主要内容:当调用intern
方法时,如果字符串常量池中已经包含一个与调用方法的对象相等的字符串,则返回池中的字符串。否则,将字符串对象添加到池中,并返回对对象的引用。
把第一次比较的代码分开解析,如下所示:
// 创建两个对象,常量池一个,s1为字符串引用对象,两者指向对中的同一块"aaa" String s1 = new String("aaa"); System.out.println("aaa:"+System.identityHashCode("aaa"));// 621009875 System.out.println("s1:"+System.identityHashCode(s1));// 1265094477 // 空串对象 String s2 = new String(""); // s1+s2以后,s3变成一个新的对象 String s3 = s1+s2; System.out.println("s3:"+System.identityHashCode(s3));// 2125039532 // s1和s3指向同一块地址,所以""没在常量池中? // 此处调用intern方法,因为常量池中包含"aaa"字符串,所以并没有实质性改变 System.out.println("s1 VS s3:"+(s1.intern()==s3.intern()));// true // 调用intern方法前后,s3字符串并没有发生变化 System.out.println("s3:"+System.identityHashCode(s3));// 2125039532 // s4指向常量池的对象,所以s3和s4的内存地址不一致,所以最后返回false String s4 = "aaa"; System.out.println("s4:"+System.identityHashCode(s4));// 621009875 System.out.println(s3==s4);// false
第二次比较的代码就容易理解了,正如上面提到的inter方法的作用,s3 = s3.intern();
,返回值为字符串常量池中的字符串对象,所以s3
的内存地址发生了变化,s3和s4相等。
最后看第三次比较的内容,这是一段很有意思的代码。
// 此处共创建了几个对象?常量池"hi","j",以及hi和j的字符串实例对象,s5也是字符串实例对象“hij”,注意此时常量池中并没有"hij"字符串 String s5 = new String("hi") + new String("j"); // 此时s5对象的内存地址为621009875 System.out.println("s5:"+System.identityHashCode(s5));// 621009875 // 调用intern方法,这一步很重要,因为常量池中并没有"hij"字符串,所有s5对象引用复制到hashtable中,字符串常量池和s5指向同一块内存 s5.intern(); // 打印出s5的内存地址,确认s5并没有变化 System.out.println("s5:"+System.identityHashCode(s5));// 621009875 // 创建一个字符串常量,因为字符串常量池中已经存在,直接引用 String s6 = "hij"; // 打印s6的内存地址,内存地址和s5的一致,所以s5和s6相等 System.out.println("s6:"+System.identityHashCode(s6));// 621009875 System.out.println(s5 == s6); // true