[JavaSE] 认识String类(JVM源码展示、StringTable、字符串常量池、哈希表内存图解)奉劝那些想深入理解String的各位家人,呕心沥血,袒露心声,掏心掏肺的内存图一定要看看
目录
往期回顾,专栏一览
🔴 认识 String 类
🔵 引入思考
🔵 创建字符串
💠 经典错误
🔵 字符串比较相等
💠 扩展
💠 什么是池?
💠 什么是哈希表?
🔵 内存布局分析
💠 JVM源码分析
💠 例1
💠 例2
💠 例3
💠 例4
💠 例5
💠 例6
💠 例7
🔵 测试代码一览
往期回顾,专栏一览
🍉 JavaSE 🍋 AWT 🍑 数据结构 🍅 C1进阶之路 🍒 每日一练 🌽 代码报错 🍈 活动
🍹欢迎各路大佬来到 Nick 主页指点
☀️本期文章将学习 [JavaSE] 认识String类(JVM源码展示、StringTable、字符串常量池、哈希表内存图解),我是博主Nick。✨
✨我的博客主页:Nick_Bears 🌹꧔ꦿ
🌹꧔ꦿ博文内容如对您有所帮助,还请给个点赞 + 关注 + 收藏✨
🔴 认识 String 类
🔵 引入思考
🔶 字符串:在C语言里面 是没有字符串这种数据类型的!在 C++/Java 中有这种数据类型,叫 String。
什么叫字符串? 使用双引号,“abcdef” “a”。
什么叫字符? ‘a’ 孙‘, ‘abc’ 这是错误的。
🔶 注意问题:在 Java 中没有所谓的字符串以 \0 一说。
🔶 查看API文档简单认识 String
🔶 查看源码认识 String
🔶 我们看看发现了什么?
value[] 是什么鬼?查阅资料后发现原来是char[]。
数组也没开辟空间...空间去哪儿了?(后面讨论)。
hash 是啥?不懂!反正我知道默认是0。
🔵 创建字符串
🔷 一共有三种方式
public class Demo { public static void main(String[] args) { //方式1 String str = "Nick很宠粉的"; //方式二(调用构造方法进行构造对象) String str2 = new String("Nick很宠粉的"); //方式三 char[] chars = {'N','i','c','k','很','宠','粉','的'}; String str3 = new String(chars); System.out.println(str); System.out.println(str2); System.out.println(str3); }}//运行结果Nick很宠粉的Nick很宠粉的Nick很宠粉的
🔻 注意事项:
"Nick" 这样的字符串字面值常量, 类型也是 String。
String 也是引用类型。String str = "Nick很宠粉的"; 这样的代码内存布局如下
String str1 = "Nick"; String str2 = str1;
思考:既然 str1 和 str2 都指向 ”Nick“,是否可以做到 str1 修改”Nick“->"Nick很宠粉的"
答:做不到!!
public static void main(String[] args) { String str1 = "Nick"; String str2 = str1; System.out.println(str1); System.out.println(str2); str1 = "Nick很宠粉的"; //这里是修改指向,不是字面值 System.out.println("================"); System.out.println(str1); System.out.println(str2); }
💠 经典错误
public static void main(String[] args) { String str = "abc"; char[] arr = {'n','i','c','k'}; func(str,arr); System.out.println(str); System.out.println(Arrays.toString(arr)); } public static void func(String s,char[] arr){ s ="Nick"; arr[0] = 'N'; }//结果abc[N, i, c, k]
答:不是说传引用就能改变实参的值,你要看这个引用干啥了,虽然我们传的都是引用,但是 str 传过去只是修改了引用的指向;
草图(大概是对的,后面究细节)
🔵 字符串比较相等
🔺 须知:等号比较的是地址
💠 扩展
1、概念
Class 文件常量池:int a = 10;
运行时常量池: 当程序把编译好的字节码文件,加载到 JVM 中后,会生成一个运行时常量池[方法区],实际上是 Class 文件常量池。
字符串常量池:主要存放字符串常量-> 字符串常量池,本质上是一个哈希表。StringTable,双引号 引起来的字符串常量才会放进去。JDK1.8开始,放在了堆里面。
💠 什么是池?
数据库连接池
线程池
.......
意义:提高效率。
🍋 例子1:现实生活中有一种女神, 称为 "绿茶", 在和高富帅谈着对象的同时, 还可能和别的屌丝搞暧昧. 这时候这个屌丝被 称为 "备胎"。那么为啥要有备胎? 因为一旦和高富帅分手了, 就可以立刻找备胎接盘, 这样 效率比较高。如果这个女神, 同时在和很多个屌丝搞暧昧, 那么这些备胎就称为 备胎池。
🍋 例子2:你和女朋友嗑瓜子,要是没有池,你就要用嘴把瓜子嗑开才能喂女朋友吃,但是你提早把一包瓜子全部嗑好了放在了盘子里,你的池就建好了,你女朋友直接拿盘子里的瓜子即可,提高了嗑瓜子效率。
💠 什么是哈希表?
🔶 描述和组织数据的一种方式
例子:假如我有12 45 2 7 15 92
问题:如何去查找?
顺序查找 O(n)
排序+二分查找
💠 哈希表怎么做的?
🔶 存储数据的时候,会根据一个映射关系进行存储,如何映射?需要设计一个函数(哈希函数)
假如自己设计的函数是:key % len
💬 那么,当哈希表无限大的时候,还不是会达到O(n)么?
🔸 一般性我们认为哈希表是一个常量,不会无限大的,速度存储很快,甚至快到O(1),接下来跟着Nick来分析一下内存布局吧!
🔵 内存布局分析
💠 JVM源码分析
🔶 那么StringTable 在哪里?我们就要去看底层 JVM 的源码了
🔶 首先我们看到我们在放一个东西时候就会new 一个 StringTable(),它就是一个字符串的哈希表。
🔶 首先把你要放的东西hash一下,得到hashValue得到一个位置,然后在look-up下面去找,如果是空则new_entry,不为空则返回。
🔶 这是look-up函数,数组下面是链表,开始遍历,如果哈希相同了就返回value值,没有就是null;
💠 例1
public static void main(String[] args) { String str1 = "Nick"; String str2 = new String("Nick"); System.out.println(str1==str2); }//结果false
🔷 分析:我们的“Nick”在底层是如何存储的?
由 引入思考 的图知道,“Nick”对象有 两个引用 ,一个 value ,一个 hash , value 是一个数组,因此会以数组的方式存储,数组的地址等于 val 的地址,val 与 hash 合为一个字符串对象
接着我就会把val与字符串常量池进行判断,利用哈希函数,类似于上面说的 key % len
若哈希表中没有,我就在当中开辟一块空间,这块空间的第一个节点包括哈希值,String对象地址值,和后继next。
原引用 str1 指向“Nick”对象地址,现在哈希表节点也指向这个“Nick”对象地址,因此成功将“Nick”,加入了哈希表中。
现在 str2 指向new 出来的一块空间,val=“Nick”,那么我就利用哈希格式先去找有没有这个数组(内部比较数组对应位数),结果肯定是找到了,那么我就将val的引用指向原先 str1 把“Nick”转换为数组的地址上。
但是结果肯定是false,因为 str1 它创建了一个对象,可是 str2 创建了两个,如图所示 0x8899 和 0x1122 ,尽管最终指向val相同,但是==比较的是地址。我们可以思考,如果如下面代码所示,返回一定是true。
💠 例2
public static void main(String[] args) { String str1 = "Nick"; String str2 = "Nick"; System.out.println(str1==str2); }//结果true
💠 例3
public static void main(String[] args) { String str1 = "Nick"; String str2 = "Ni"+"ck"; // 此时它俩都是常量,在编译的时候就已经确定好了,是"Nick" System.out.println(str1==str2); }//结果true
🔷 如何去证明?我们反编译字节码文件,发现第二个 str 是 Nick
💠 例4
public static void main(String[] args) { String str1 = "Nick"; String str3 = "Ni"; String str4 = str3+"ck"; //此时str3是一个变量,编译的时候不知道是啥 System.out.println(str1 == str4); }//结果false
🔷 我们翻看字节码文件发现编译器不知道 str4 是啥,重新new了一个,也就是说str3和“ck”拼接后的结果Nick不会出现在常量池里面,而是会new一个对象在堆里面
💠 例5
public static void main(String[] args) { String str1 = "11"; String str2 = new String("1")+new String("1"); System.out.println(str1==str2); }//结果false
🔷 我先编译一下,看字节码文件,在分析
🔷 我们发现:
new了一个StringBuilder对象
通过两个new 出来的String 对象拼接产生的新的StringBuilder
最后调用了toString方法
🔷 toString方法有没有重新new对象呢?我们翻找StringBuilder的源码看看
🔷 答案显而易见,是有的!
万事俱备,直接上图分析
前面入池11的情况见上(这里省略)
我将两个新new出来的“1”都指向数组中的“1”
因为+(拼接),所以我又新new出来了一个拼接出来的空间(StringBuilder)
但是我们的拼接出来的“11”不会入池,StringBuilder中的val指向11
但是我们是要返回String,因此调用toString方法,查看源码发现又new了一个对象
这个对象的val也指向11,因此我们的str2指向这个对象的地址,也就是图中的的0x999
不过这个val仍然没有入池
💠 例6
public static void main(String[] args) { String str2 = new String("1")+new String("1"); String str1 = "11"; System.out.println(str1==str2); }//不同说结果肯定是一样的false
稍作修改:
public static void main(String[] args) { String str2 = new String("1")+new String("1"); str2.intern(); // 手动入池 String str1 = "11"; System.out.println(str1==str2);}//结果true
神奇的事情发生了,0x999(toString后的那个对象),手动入池以后,哈希表里就有了11
当我str1=11时,在表中检索到了,因此str1直接指向入池的对象0x999,这是str1和str2就指向同一个地址了,返回true
💠 例7
public static void main(String[] args) { String str1 = "11"; String str2 = new String("1")+new String("1"); str2.intern(); // 手动入池->当字符串常量池中没有才会入池 System.out.println(str1==str2); }//结果false
如果我如上这样换一下呢?我们可以分析先入池了str1的11
str2想要也把11入池,不可以,因为池中已经有了,这种情况手动入池失败了
🔵 测试代码一览
import java.util.Arrays;public class Demo { public static void main(String[] args) { String str1 = "11"; String str2 = new String("1")+new String("1"); str2.intern(); // 手动入池->当字符串常量池中没有才会入池 System.out.println(str1==str2); } public static void main9(String[] args) { String str2 = new String("1")+new String("1"); str2.intern(); // 手动入池 String str1 = "11"; System.out.println(str1==str2); } public static void main8(String[] args) { String str1 = "11"; String str2 = new String("1")+new String("1"); System.out.println(str1==str2); } public static void main7(String[] args) { String str1 = "Nick"; String str3 = "Ni"; String str4 = str3+"ck"; //此时str3是一个变量,编译的时候不知道是啥 System.out.println(str1 == str4); } public static void main6(String[] args) { String str1 = "Nick"; String str2 = "Ni"+"ck"; // 此时它俩都是常量,在编译的时候就已经确定好了,是"hello" str2.intern(); System.out.println(str1==str2); } public static void main5(String[] args) { String str1 = "Nick"; String str2 = "Nick"; System.out.println(str1==str2); } public static void main4(String[] args) { String str1 = "Nick"; String str2 = new String("Nick"); System.out.println(str1==str2); } public static void main3(String[] args) { String str = "abc"; char[] arr = {'n','i','c','k'}; func(str,arr); System.out.println(str); System.out.println(Arrays.toString(arr)); } public static void func(String s,char[] arr){ s ="Nick"; arr[0] = 'N'; } public static void main2(String[] args) { String str1 = "Nick"; String str2 = str1; System.out.println(str1); System.out.println(str2); str1 = "Nick很宠粉的"; //这里是修改指向,不是字面值 System.out.println("================"); System.out.println(str1); System.out.println(str2); } public static void main1(String[] args) { //方式1 String str = "Nick很宠粉的"; //方式二(调用构造方法进行构造对象) String str2 = new String("Nick很宠粉的"); //方式三 char[] chars = {'N','i','c','k','很','宠','粉','的'}; String str3 = new String(chars); System.out.println(str); System.out.println(str2); System.out.println(str3); }}
☀️ 以上是Nick花了一个上午的时间剖析的内部原理,图片纯手绘,创作不易,希望大家留下宝贵的三连。