来自三段代码的疑惑~

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

来自三段代码的疑惑~

几天前豆豆发现了发现的一段有意思的代码,主要是一开始没看明白,接着去找了洛洛,请教一下。你是不是很好奇是什么代码迷惑了豆豆的双眼?话不多说,我们先上代码。运行环境是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
目录
相关文章
|
8月前
|
自然语言处理 安全 测试技术
如何写出优秀的代码
如何写出优秀的代码
|
4月前
|
设计模式 算法 Java
|
8月前
|
关系型数据库 数据库连接 网络安全
细心慢慢来就可以
今天早上在床上刷抖音,偶然发现了阿里云云产品可以白嫖的广告(学生党,no money),抱着试一试的心态就搭建了一下,跟着老师的教程好好做就可以的
53 0
|
10月前
|
缓存 算法 安全
程序员写代码为什么要阅读源码?
阅读一篇技术文章,畅聊一个技术话题。本期文章推荐的是《Node 中的 AsyncLocalStorage 的前世今生和未来》,一起来聊聊开发者阅读源码的这件事。阅读源码的过程实质上是对软件构建技术和架构深度的一种持续学习和理解。阅读源码可以揭示代码的内在逻辑,可以对技术深度的理解,也能提高对技术的理解程度。然而,仅仅阅读源码并不能代替实践操作,因为通过实践,可以更加全面的理解代码的深度和进展。
103 1
|
Python Windows
你真的看得懂报错吗?
你真的看得懂报错吗?
274 0
你真的看得懂报错吗?
|
编解码 前端开发 程序员
为啥只跟着视频敲代码学不好编程?
为啥只跟着视频敲代码学不好编程?
304 1
|
设计模式 前端开发 安全
如何写出高质量的代码
如何写出高质量的代码
166 0
如何写出高质量的代码
|
安全 JavaScript 程序员
写好代码需要举一反三
如何让代码写的更安全高效一直是程序员的不懈追求!在解决问题的同时,简介清爽,逻辑严谨的代码会让程序员更带光环。 简书亦有简码
170 0
写好代码需要举一反三
|
Web App开发 Ubuntu JavaScript
我好像明白了什么.....
谷歌浏览器是最流行的网络浏览器,无论你是否喜欢使用它,Chrome都毋庸置疑提供良好的用户体验。 尽管它可用于Linux,但它并不是一个开源的网络浏览器。
我好像明白了什么.....
|
存储 IDE Java
如何写出优雅的代码
如何写出优雅的代码
197 0
如何写出优雅的代码