Java String 的 常量池 和 intern 方法 简析

简介: Java String 的 常量池 和 intern 方法 简析字符串常量池和 intern 方法先举个例子,我们创建 10000 个相同的 String,并且不使用常量池String[] list = new String[10000];for (int i = 0; i < 10000; i++) { list[i] = new String(new char[]{'a','b','c'});}结果是:每个 String 都用新开的对象,占用大量内存现在我们这样创建,增加一个 map ,key 和 value 内容一样,每次取的时候,先检查一下 map 里面有没

字符串常量池和 intern 方法

先举个例子,我们创建 10000 个相同的 String,并且不使用常量池

1
2
3
4
String[] list = new String[10000];
for (int i = 0; i < 10000; i++) {
    list[i] = new String(newchar[]{'a','b','c'});
}

结果是:每个 String 都用新开的对象,占用大量内存

现在我们这样创建,增加一个 map ,key 和 value 内容一样,每次取的时候,先检查一下 map 里面有没有,有就取 map 里面的,没有就放进去。即,创建 10000 个 String,使用自建常量池。

1
2
3
4
5
6
7
String[] list = new String[10000];
Map<String, String> pool = new HashMap<>();
for (int i = 0; i < 10000; i++) {
    String s = new String(newchar[]{'a','b','c'});
    pool.putIfAbsent(s, s); // 等价于 if (pool.get(s) == null) pool.put(s, s);
    list[i] = pool.get(s);
}

结果很明显,数组中每一个元素都指向堆中的同一个元素,其他新创建的 String 都会在下一次 GC 被清空。

其实,这个 map 就是字符串常量池。不过,JVM 把这个功能用 C++重新 实现了,存放在堆区。

那常量池这么好,要怎么使用 JVM 里面的常量池呢?

  1. 用双引号创建的 String ,自动使用常量池,比如 String a = "test";
  2. 使用 String 的 intern 方法,使用常量池,比如
1
2
String s = new String(new char[]{'a','b','c'});
String intern = s.intern(); // 类似于上面的pool.putIfAbsent(s, s) 和 pool.get(s)

关于 intern 方法,JDK 文档这样写:当调用 intern 方法时,如果常量池(内置在 JVM 中的)中已经包含相同的字符串,则返回池中的字符串。否则,将此 String 对象添加到池中,并返回对该 String 对象的引用。

我们再用 intern 写一个存 10000 个字符串的代码,使用 JVM 常量池,结果和例子中的第二个代码一致

1
2
3
4
String[] list = new String[10000];
for (int i = 0; i < 10000; i++) {
    list[i] = new String(newchar[]{'a','b','c'}).intern();
}

或者使用双引号创建字符串,自动使用 JVM 常量池,结果和例子中的上面的代码一致

1
2
3
4
String[] list = new String[10000];
for (int i = 0; i < 10000; i++) {
    list[i] = "abc";
}

intern 方法的小特性

其实 intern 的使用并不复杂,上面的例子已经讲得非常清晰。

不过,如果你要研究茴香豆的茴字有几种写法的话,那么这里有一个细节要注意:

  • 当堆区先创建了一个 String,并使用常量池,如果常量池中没有,常量池会直接把这个 刚刚在堆区创建的那个 String 作为 value

这个细节有点拗口,不过也非常好理解,我们搬回上面例子中的自建常量池:

1
2
3
4
5
6
7
String[] list = new String[10000];
Map<String, String> pool = new HashMap<>();
for (int i = 0; i < 10000; i++) {
    String s = new String(newchar[]{'a','b','c'});
    pool.putIfAbsent(s, s);
    list[i] = pool.get(s);
}

这个特性的意思就是说,常量池里面的 pool.get(s) 总是会返回第一个加进去的 s ,而不是别的。如果你走一下上面的代码,就会发现这个非常正常。

我们再复习一下使用常量池的两个方法:

  1. 用双引号创建的 String ,自动使用常量池
  2. 使用 String 的 intern 方法,使用常量池

那么,现在,我们祭出网上流传很广的题目:

1
2
3
4
5
6
7
8
9
10
11
publicstaticvoidmain(String[] args){
    String s = new String("1");
    s.intern();
    String s2 = "1";
    System.out.println(s == s2);
    String s3 = new String("1") + new String("1");
    s3.intern();
    String s4 = "11";
    System.out.println(s3 == s4);
}

你可以用我上面讲的内容试一下答案。

答案是输出 false true,我再讲一次原理:

  1. 第二行,先执行括号中的 “1” ,在堆区创建了一个 String,我假设他的地址为 100 ,由于是双引号创建,自动使用常量池,设置常量池中 “1” 的 value 为 堆区 100 号
  2. 第二行,new String(xxx),在堆区又创建了一个 String,假设地址为 101,暂时没有使用常量池
  3. 第三行,很显然,s.intern() 会返回 100,但是并没有用变量接住 (谁知道网上出题的那个人脑子怎么想的呢)
  4. 第四行,双引号创建,自动使用常量池,返回的地址是 100
  5. 所以 s2 是 101,s 是 100,答案已经很明显了
  6. 第七行,两个”1”结合,堆区创建了一个 String s3,假设地址为 102
  7. 第八行,s3.intern() ,使用常量池,但是常量池里面没有 “11”,所以设置常量池的 11 的 value 为 地址 102
  8. 第九行,双引号创建自动使用常量池,所以 s4 地址为 102

好,再来第二题

1
2
3
4
5
6
7
8
9
10
11
publicstaticvoidmain(String[] args){
    String s = new String("1");
    String s2 = "1";
    s.intern();
    System.out.println(s == s2);
    String s3 = new String("1") + new String("1");
    String s4 = "11";
    s3.intern();
    System.out.println(s3 == s4);
}

答案是 false, false,我就不再细讲了,你可以自己推导一遍。如果还是不懂再重新看下上面的文章。

不适合用 intern 方法的情况

由于 JVM 里面的 C++写的 的 HashMap 设计并不像 JDK 的 HashMap 这么科学(超过链表负载链表转红黑树)。所以,如果你有几千万个不同的 String 要使用 intern 丢进常量池的话,那么,查找起来会非常慢。而且常量池也会变得非常大,所以,不建议丢太多不同的 String 进常量池

那硬是要丢可不可以呢,也是可以的,你可以扩大 JVM 的 -XX:StringTableSize 参数(jdk8 中默认为 60013),这个参数类似 JDK HashMap 的 initialCapacity。 但是这样子会占用更多的内存和 CPU。

相关文章
|
8月前
|
编解码 Java 开发者
Java String类的关键方法总结
以上总结了Java `String` 类最常见和重要功能性方法。每种操作都对应着日常编程任务,并且理解每种操作如何影响及处理 `Strings` 对于任何使用 Java 的开发者来说都至关重要。
465 5
|
10月前
|
存储 SQL 缓存
Java字符串处理:String、StringBuilder与StringBuffer
本文深入解析Java中String、StringBuilder和StringBuffer的核心区别与使用场景。涵盖字符串不可变性、常量池、intern方法、可变字符串构建器的扩容机制及线程安全实现。通过性能测试对比三者差异,并提供最佳实践与高频面试问题解析,助你掌握Java字符串处理精髓。
|
11月前
|
自然语言处理 Java Apache
在Java中将String字符串转换为算术表达式并计算
具体的实现逻辑需要填写在 `Tokenizer`和 `ExpressionParser`类中,这里只提供了大概的框架。在实际实现时 `Tokenizer`应该提供分词逻辑,把输入的字符串转换成Token序列。而 `ExpressionParser`应当通过递归下降的方式依次解析
493 14
|
缓存 安全 Java
《从头开始学java,一天一个知识点》之:字符串处理:String类的核心API
🌱 **《字符串处理:String类的核心API》一分钟速通!** 本文快速介绍Java中String类的3个高频API:`substring`、`indexOf`和`split`,并通过代码示例展示其用法。重点提示:`substring`的结束索引不包含该位置,`split`支持正则表达式。进一步探讨了String不可变性的高效设计原理及企业级编码规范,如避免使用`new String()`、拼接时使用`StringBuilder`等。最后通过互动解密游戏帮助读者巩固知识。 (上一篇:《多维数组与常见操作》 | 下一篇预告:《输入与输出:Scanner与System类》)
390 11
课时14:Java数据类型划分(初见String类)
课时14介绍Java数据类型,重点初见String类。通过三个范例讲解:观察String型变量、&quot;+&quot;操作符的使用问题及转义字符的应用。String不是基本数据类型而是引用类型,但使用方式类似基本类型。课程涵盖字符串连接、数学运算与字符串混合使用时的注意事项以及常用转义字符的用法。
431 9
|
存储 JavaScript Java
Java 中的 String Pool 简介
本文介绍了 Java 中 String 对象及其存储机制 String Pool 的基本概念,包括字符串引用、构造方法中的内存分配、字符串文字与对象的区别、手工引用、垃圾清理、性能优化,以及 Java 9 中的压缩字符串特性。文章详细解析了 String 对象的初始化、内存使用及优化方法,帮助开发者更好地理解和使用 Java 中的字符串。
280 2
Java 中的 String Pool 简介
|
Java
JAVA方法的定义
JAVA方法的定义
322 0
|
存储 JSON Java
《从头开始学java,一天一个知识点》之:方法定义与参数传递机制
**你是否也经历过这些崩溃瞬间?** - 看了三天教程,连`i++`和`++i`的区别都说不清 - 面试时被追问&quot;`a==b`和`equals()`的区别&quot;,大脑突然空白 - 写出的代码总是莫名报NPE,却不知道问题出在哪个运算符 🚀 这个系列就是为你打造的Java「速效救心丸」!我们承诺:每天1分钟,地铁通勤、午休间隙即可完成学习;直击痛点,只讲高频考点和实际开发中的「坑位」;拒绝臃肿,没有冗长概念堆砌,每篇都有可运行的代码标本。上篇:《输入与输出:Scanner与System类》 | 下篇剧透:《方法重载与可变参数》。
379 25
|
安全 Java 编译器
杭州 【Java基础知识 11】java泛型方法的定义和使用(学习+改进+自己理解,想法) (借鉴-侵-删)
杭州 【Java基础知识 11】java泛型方法的定义和使用(学习+改进+自己理解,想法) (借鉴-侵-删)
234 1