Java SE 学习哈希表后对String的讨论
1.发现问题
我们在学习String后,知道它常用的构造方法分别为以下三种:
1.使用常量字符串直接构造,比如:
String s1 = "hello";2.通过new String对象构造,比如:
String s2 = new String("hello");3.使用字符数组构造,比如:
char[] ch = {'h','e','l','l','o'}; String s3 = new String(ch);
不仅如此,我们还知道String的字符串是储存在String类中的一个数组中,并且这个数组是私有的,无法直接对它进行操作,这也就是String的不可变性的由来。知道了String是怎么保存字符串之后,难免会产生一个疑问:String作为Java中经常使用的引用类型,创建String对象时,它是直接通过构造方法创建的,还是有别的方法。
请看下面的代码:
public class Test {public static void main(String[] args) {String s1 = "hello";String s2 = "hello";String s3 = new String("hello");String s4 = new String("hello");System.out.println(s1 == s2);System.out.println(s3 == s4);System.out.println(s1 == s3);}
}//运行结果:
true
false
false发现:s1和s2引用的是同一个对象,而s3和s4引用的不是同一个对象。为什么会造成这种原因呢?这就不得不引入一个新的概念了——“池”。
2.“池”是什么东西?
“池”(Pool)的核心逻辑就是提前准备好一批常用资源,谁要用就直接拿,用完了还回来,避免反复创建和销毁,就像生活里的 “共享工具站”,它是编程中的一种常见的, 重要的提升效率的方式, 有各种各样的池,比如说: "内存池", "线程池", "数据库连接池" ……
在Java程序中,类似于:1,2,3.14,"hello"等字面类型(指的是在代码中直接写出的、代表固定值的 “字面量”,它是数据的直接表示形式,无需计算或引用其他变量就能确定值)的常量经常频繁使用,为了使程序的运行速度更快、更节省内存,Java为8种基本数据类型和String类都提供了常量池。为了节省存储空间以及程序的运行效率,Java中引入了:
- Class文件常量池:每个.Java源文件编译后生成.Class文件中会保存当前类中的字面常量以及符号信息。
- 运行时常量池:在.Class文件被加载时,.Class文件中的常量池被加载到内存中称为运行时常量池,每个类都有一份运行时常量池。
- 字符串常量池:String 常量池是 JVM 内存中一块特殊的区域(在 JDK 7 及之后位于堆内存,之前位于方法区),专门用于存储字符串常量(即编译期确定的字符串,如直接写在代码中的
"abc")。
3.关于字符串常量池(StringTable)
在Java的JVM中字符串常量池是StringTable类,这个类实际是一个固定大小的哈希表(HashTable),在不同JDK版本下字符串常量池的位置以及默认大小是不同的,比如说:
| JDK版本 | 字符串常量池位置 | 大小设置 |
|---|---|---|
| Java6 | (方法区)永久代 | 固定大小:1009 |
| Java7 | 堆中 | 可设置,没有大小限制,默认大小:60013 |
| Java8 | 堆中 | 可设置,有范围限制,最小是1009 |
4.再次探讨创建String对象
开头,我们说Java中String对象的创建使用的构造方法通常有三种,但是它们真的都是构造方法么,其实第一种方式,即使用常量字符串直接构造,这种方式严格意义上不能算做一种构造方法,因为构造方法的调用必须通过 new 关键字触发。现在我们结合字符串常量池来分析一下三种构造方法的底层实现。在开始前。再次明确“池”的核心逻辑:
提前准备好一批常用资源,谁要用就直接拿,用完了还回来,避免反复创建和销毁。
1.使用常量字符串直接构造
当我们通过这种方式创建String对象时,比如说 String s1 = "abc",此时JVM会先去字符串常量池中查找是否有"abc"这个String对象,如果有,那么令s1直接指向这个已有的对象(不会创建新的String对象);如果没有,在常量池中创建String对象("abc"),再让s1引用它(仅创建一次),同时字符串常量池保存这个对象,方便之后复用。
注:在字符串常量池中创建String对象并不依赖于String类的构造方法,它依赖的是JVM的底层操作——直接在常量池区域分配内存并初始化(底层是数组)。同时得益于String的不可变性,使得字符串常量池可以实现“多个引用共用一个对象”!一旦字符串对象存入常量池,其内容就永远不会被修改,因此复用是安全的。画图举例如下:

2.通过new创建String对象
通过new创建String对象,这里主要讨论开头所说的常用方法的两种。
2.1 通过new String对象构造
当我们通过像:String s3 = new String("hello");这样的方式创建String对象时,创建的过程是这样的:JVM会先去字符串常量池查找是否存在“hello”这个String对象,如果没有就在字符串常量池中创建String对象("hello"),接着获取储存"hello"数组的引用;如果有,就直接获取"hello"数组的引用。接着按照构造方法String(String original)在堆中创建一个String对象,并且令这个String对象的value指向获取的"hello"的数组,这就实现了String对象的创建。
需要注意的是:局部变量s3引用的是堆中新建的String对象,而这个String对象的value指向的是字符串常量池中"hello"String对象的value所指向数组。
具体过程如图所示:

2.2 使用字符数组构造
对于使用字符数组构造的方式来创建String对象的方式,它与上述两种方式有明显的区别,它的具体过程是这样的:调用String(char[] value)构造方法,直接在堆中创建新对象,这个新对象的value指向的是传入数组value的副本,这种方式不主动关联常量池。图示如下:

假设说我们现在通过字符数组创建String对象了,那么也没有办法把这个String对象添加到字符串常量池方便我们以后使用呢?其实是有的!就是intern方法,intern 是一个native方法(Native方法指:底层使用C++实现的,因此看不到其实现的源代码),该方法的作用是手动将创建的String对象添加到常量池中。像这样使用:
public class Test {public static void main(String[] args) {char[] ch = {'a','b','c'};String s1 = new String(ch);s1.intern(); //将s1这个对象放入字符串常量池中String s2 = "abc"; //创建s2时,发现字符串常量池中有"abc"这个对象,//那么s2直接指向这个对像 System.out.println(s1 == s2);}
}//运行结果:
true根据运行结果,发现s1与s2引用的是同一个对象,说明s1放入字符串之后,创建s2时,在字符串常量池中查找到"abc"这个对象,那么直接令s2指向这个对象。
5.总结
字符串常量池遵循着“池”的核心逻辑:提前准备好一批常用资源,谁要用就直接拿,用完了还回来,避免反复创建和销毁。当我们创建一个String对象时,JVM会先去字符串常量池查找是否有创建String对象的字符串,如果有,那么直接令String引用指向字符串常量池中的对象或者令String引用的String对象的value字段指向字符串常量池中的对象的value所指向的数组即可;没有再考虑创建新的String对象。简单来说,字符串常量池主要实现了以下关键作用:
- 字符串对象的复用
- 提高代码的执行效率
保障字符串不可变性的安全复用。
跨类全局共享。字符串常量池是 JVM 级别的全局资源(全进程共享),不同类、不同方法中声明的相同字符串,都能复用同一个常量池对象,实现了字符串资源的全局优化。
到此,本篇文章结束,感谢您的阅读,如有不对,还请指出!谢谢!
