来自三段代码的疑惑~

简介: 来自三段代码的疑惑~

来自三段代码的疑惑~

几天前豆豆发现了发现的一段有意思的代码,主要是一开始没看明白,接着去找了洛洛,请教一下。你是不是很好奇是什么代码迷惑了豆豆的双眼?话不多说,我们先上代码。运行环境是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&trade; 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
目录
相关文章
|
自然语言处理 安全 测试技术
如何写出优秀的代码
如何写出优秀的代码
|
5月前
|
Java 数据库连接
一篇文章讲明白Erlangpoolmanagement
一篇文章讲明白Erlangpoolmanagement
31 2
|
5月前
|
设计模式 监控 程序员
如何写好代码?
如何写好代码?
|
5月前
|
人工智能 Java BI
一篇文章讲明白MartianAddition
一篇文章讲明白MartianAddition
25 0
|
5月前
|
流计算 内存技术
一篇文章讲明白FreescaleKibbletest
一篇文章讲明白FreescaleKibbletest
23 0
|
5月前
|
存储 Java API
一篇文章讲明白luauserdata
一篇文章讲明白luauserdata
170 0
|
5月前
|
druid 数据库
一篇文章讲明白HearthBuddy卡组
一篇文章讲明白HearthBuddy卡组
158 0
|
6月前
|
设计模式 算法 Java
|
编解码 前端开发 程序员
为啥只跟着视频敲代码学不好编程?
为啥只跟着视频敲代码学不好编程?
418 1
|
移动开发 缓存 ARouter
没错,TheRouter 是我写的
大约在17年底到18年初的时候,我经常会讲一些当时做模块化开发的心得和踩坑历程
220 0