从Java的字符串池、常量池理解String的intern()
前言
逛知乎遇到一个刚学Java就会接触的字符串比较问题:

通常,根据"==比较的是地址,equals比较的是值"介个定理就能得到结果。但是String有些特殊,通过new String(string)生成的两个同值的字符串地址就不相等,用其他方式来生成的两个同值字符串地址就相等。
代码如下:
// 第一种方式创建字符串,字面量赋值
String str1 = "abc";
String str2 = "abc";
// 第二种方式创建字符串
String str3 = new String("xyz");
String str4 = new String("xyz");
System.out.println(str1 == str2); //true
System.out.println(str3 == str4); //false
同样是创建字符串,两对等值的字符串进行为什么结果不一样,这就涉及到了常量池和堆。
第一种方式创建的字符串,会将"abc"这个字面量放到了常量池中,然后str1和str2都指向常量池中的"abc",所以两个变量地址相同;第二种方式创建的字符串,是先在常量池中放入"xyz",然后通过构造函数将常量池中的"xyz"拷贝一份到堆中生成新的String,和常量池中的"xyx"就没有了关系,所以两个变量指向的是堆中两个不同的变量,所以两个变量地址不同。
那intern()又是啥?和常量池之间又有什么联系?
常量池
常量池是存放字面量、符号引用或直接引用的地方。而常量池又分为class常量池和运行时常量池。
class常量池
class常量池是存放编译期类中的字面量和符号引用。上面的字符串"abc"就是字面量;符号引用就是类和接口的完全限定名,字段的名称和描述符,方法的名称和描述符。
如图:

图中的就是new String(String)这个方法在常量池中的名称和描述符,即符号引用。
运行时常量池
我们平时说的常量池指的就是运行时常量池。在类加载的解析阶段,会将class常量池载入内存中(JDK1.7之前位于方法区,现在位于Heap中),并且将符号引用解析成直接引用,即根据对方法/类的描述信息指向内存中对应的方法/类。运行时常量池具有动态性,可以在运行期添加新的变量进入常量池。
intern()
先看一下intern()这个方法的描述:
用二级英文水平翻译一波,大意就是一个string调用intern()的时候,如果池中有和这个字符串值相等的字符串对象,就会将字符串池中的字符串对象返回;如果没有,就将这个字符串添加进去,并返回这个字符串的引用。字符串池由String类私有维护。
这里又引入了字符串池这个概念。
字符串池
字符串池存放的是常量池中字符串对象的引用,而不是字符串对象。通过第一种字面量赋值法创建的字符串会放在常量池中,字符串池就会存储这个字符串对象的引用,当再次在常量池创建字符串时,会先从字符串池查看是否有此字符串的等值引用,如果有的话,直接指向此引用对应的对象。
而第二种方式创建的字符串,会在字符串池中查找是否有与构造参数等值的字符串,以此决定是否需要在常量池新建字符串,然后拷贝常量池中字符串在Heap创建一个新的字符串。

如图,在堆中会在常量池中创建一个名为original的新字符串,然后拷贝并在堆中生成一个新字符串。注释中也提到,除非你需要一个字符串的显式副本,否则不需要使用这个构造函数,因为字符串是不可变的。
这里使用intern()测试一下字符串池:
public static void main(String[] args) {
//第一部分 测试
String str1 = "abc";
String str2 = new String("abc");
System.out.println(str1.intern() == str1); //true
System.out.println(str1.intern() == str2); //false
System.out.println(str1.intern() == str2.intern()); //true
//第二部分 测试通过char[]创建字符串后,引用是否会进入字符串池
String str3 = new String(new char[]{'g', 'h'});
String str4 = "gh";
System.out.println(str3.intern() == str3); //false
System.out.println(str3.intern() == str4); //true
//第三部分 测试char[]创建的字符串调用intern()后引用是否进入字符串池
String str3 = new String(new char[]{'g', 'h'});
str3.intern();
String str4 = "gh";
System.out.println(str3.intern() == str3); //true
System.out.println(str3.intern() == str4); //true
}
以上三部分代码是独立测试。
第一部分:str1在常量池创建了abc,并将引用放入字符串池,str2拷贝常量池中的abc并在堆中创建新字符串。intern()从字符串池中获取的是常量池中str1的abc引用。
第二部分:str3通过char[]在堆中创建了字符串,不是在常量池,所以gh的引用不会自动放入字符串池。str4在常量池创建了gh,所以字符串池中保存了str4的gh引用。intern()从字符串池中获取的是常量池中str4的gh引用。
第三部分:str3通过char[]在堆中创建了字符串,不是在常量池,所以gh的引用不会自动放入字符串池,但是它调用intern()手动将str3的gh的引用添加到了字符串池中。当str4使用字面量赋值创建时,查询到字符串池中有gh的引用,str4就指向了str3的gh引用。intern()从字符串池中获取的是堆中str3的gh引用。
从上面的代码中也得出结论:intern()可以将堆中创建的且字符串池没有等值引用的字符串引用放入字符串池。
同时,这也能说明String为什么不可变这个问题。
因为这样可以保证多个引用可以同时指向字符串池中的同一个对象。如果字符串是可变的,其中的一个引用操作改变了对象的值,对其他引用会有影响,这样显然是不可以的。
言归正传
回到知乎上的问题。在常量池创建了"string"并将其引用放入字符串池,str1调用intern()返回的是常量池中的引用,而str1指向的是堆中的引用,所以输出为false。
而StringBuilder的toString()是通过char[]创建字符串:

在堆中创建了abcdef之后,str2调用intern()将堆中引用放入字符串池并返回此引用,与str2指向堆中同一个字符串对象,所以输出为true。
结语
Java中有时候很小的问题也会发散出很多知识点,不论是底层还是JVM的理论学习,结合应用案例会理解的更加深刻。就像文中提到的常量池就是class文件结构和类加载理论学习的一部分。

从Java的字符串池、常量池理解String的intern()的更多相关文章
- 从字符串到常量池,一文看懂String类设计
从一道面试题开始 看到这个标题,你肯定以为我又要讲这道面试题了 // 这行代码创建了几个对象? String s3 = new String("1"); 是的,没错,我确实要从这里 ...
- Java提高篇之常量池
一.相关概念 1. 什么是常量 用final修饰的成员变量表示常量,值一旦给定就无法改变! final修饰的变量有三种:静态变量.实例变量和局部变量,分别表示三种类型的常量. 2. Class文件中的 ...
- Java基础系列2:深入理解String类
Java基础系列2:深入理解String类 String是Java中最为常用的数据类型之一,也是面试中比较常被问到的基础知识点,本篇就聊聊Java中的String.主要包括如下的五个内容: Strin ...
- 【JVM】Java 8 中的常量池、字符串池、包装类对象池
1 - 引言 2 - 常量池 2.1 你真的懂 Java的“字面量”和“常量”吗? 2.2 常量和静态/运行时常量池有什么关系?什么是常量池? 2.3 字节码下的常量池以及常量池的加载机制 2.4 是 ...
- Java中几种常量池的区分
转载自:https://tangxman.github.io/2015/07/27/the-difference-of-java-string-pool/ 在java的内存分配中,经常听到很多关于常量 ...
- java虚拟(一)--java内存区域和常量池概念
一.java运行时数据区 也可以称为java内存区域,和java内存模型不是一回事,不要弄混,这里基于jdk1.8之前 1.1.方法区 线程共享,类装载过程中产生的java.lang.Class对象保 ...
- Java内存中的常量池
1,java内存模型简介 <深入理解java虚拟机>里将java内存分为如下五个模块: 堆-堆是所有线程共享的,主要用来存储对象. 其中,堆可分为:新生代和老年代两块区域.使用NewRat ...
- String字符串针对常量池的优化
String对象是java语言中重要的数据类型,但是不是基本数据类型.相对于c语言的char java做了一些封装和延伸. 针对常量池的优化:当两个String拥有相同的值时,它们只引用常量池中的同一 ...
- Java基础二:常量池
目录: 自动装箱与拆箱 常量池 ==与equals()区别 1. 自动装箱与拆箱 Java是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成 ...
随机推荐
- Codeforces Edu Round 50 A-D
A. Function Height 由于只能提升\(x\)为奇数的点,每个三角形的底一定为\(2\), 则要求我们求: \(2 * (h_1 + h_2 + - + h_n) / 2 = k\),使 ...
- c++笔试题3
一.[阿里C++面试题]1.如何初始化一个指针数组.答案: 错题解析:首先明确一个概念,就是指向数组的指针,和存放指针的数组. 指向数组的指针:char (*array)[5];含义是一个指向存放5个 ...
- Linux相关介绍和安装
目录 前言 第一章 linux介绍 1.1 linux简介 第二章 Linux起源 2.1 Unix的历史 2.2 Unix操作系统的革命 2.3 Linux系统的诞生 2.4 Linux系统的发展史 ...
- JVM虚拟机(二):字节码执行引擎
运行时栈帧结构 栈帧是用于支持虚拟机进行方法调用和方法执行背后的数据结构,它也是虚拟机运行时数据区中的虚拟机栈的栈元素.栈帧存储了方法的局部变量表.操作数栈.动态链接.和方法返回地址等信息. ...
- js下 Day10、尺寸位置属性
一.元素尺寸信息 元素.offsetWidth: 元素的外宽高 width + padding + border 元素.offsetHeight: 元素的外宽高 height + padding + ...
- Pyhton3 文件拷贝
目录 Pyhton3 文件拷贝 shutil.copy()简单使用说明 Pyhton3 文件拷贝 导入模块shutil,使用模块中的shutil.copy()函数进行文件拷贝 shutil.cop ...
- mini-web框架-WSGI-mini-web框架-多进程,面向对象的服务器(5.1.1)
@ 目录 1.说明 2.代码 关于作者 1.说明 使用多进程 积极主动python多进程是复制资源,线程是共享变量 所以这个的socket要关两次,因为复制文件的时候,是把文件的fd给复制过去(fil ...
- Web服务器-并发服务器-多进程(3.4.1)
@ 目录 1.优化分析 2.代码 3. 关于作者 1.优化分析 在单进程的时候,相当于 是来一个客户,派一个人去服务一下 效率低,现在使用多进程来服务 假设场景 100个人同时访问页面 单进程:一次处 ...
- 基于SpringBoot+Mybatis+MySQL5.7的轻语音乐网
一个基于SpringBoot+Mybatis+MySQL5.7的轻语音乐网站项目 1.主要用到的技术: 使用maven进行项目构建 使用Springboot+Mybatis搭建整个系统 使用ajax连 ...
- spring整合sharding-jdbc实现分库分表
1.创建两个库,每个库创建两个分表t_order_1,t_order_2 DROP TABLE IF EXISTS `t_order_1`; CREATE TABLE `t_order_1` ( `i ...