当前位置: 首页 > news >正文

【Java基础】字符串不可变性、string的intern原理

【Java基础】字符串不可变性、string的intern原理

  • 1、String是如何实现不可变的?
  • 2、String为什么设计成不可变的?
  • 3、字符串常量从哪来的?
  • 4、String中intern的原理是什么?
  • 5、intern的正确用法

1、String是如何实现不可变的?

string不可变的基本原理:

  1. String类被声明为final,这意味着它不能被继承。那么他里面的方法就是没办法被覆盖的。
  2. 用final修饰字符串内容的char[](从JDK 1.9开始,char[]变成了byte[]),由于该数组被声明为final,一旦数组被初始化,就不能再指向其他数组。
  3. String类没有提供用于修改字符串内容的公共方法。例如,没有提供用于追加、删除或修改字符的方法。如果需要对字符串进行修改,会创建一个新的String对象。
public final class Stringimplements java.io.Serializable, Comparable<String>, CharSequence {/** The value is used for character storage. */private final char value[];/** use serialVersionUID from JDK 1.0.2 for interoperability */private static final long serialVersionUID = -6849794470754667710L;public String substring(int beginIndex) {if (beginIndex < 0) {throw new StringIndexOutOfBoundsException(beginIndex);}int subLen = value.length - beginIndex;if (subLen < 0) {throw new StringIndexOutOfBoundsException(subLen);}return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);}public String concat(String str) {int otherLen = str.length();if (otherLen == 0) {return this;}int len = value.length;char buf[] = Arrays.copyOf(value, len + otherLen);str.getChars(buf, len);return new String(buf, true);}
}

2、String为什么设计成不可变的?

主要有以下4个原因:

  1. 缓存:字符串是使用最广泛的数据结构。大量的字符串的创建是非常耗费资源的,所以,Java提供了对字符串的缓存功能,可以大大的节省堆空间。通过字符串池,两个内容相同的字符串变量,可以从池中指向同一个字符串对象,从而节省了关键的内存资源。
  2. 安全性:字符串在Java应用程序中广泛用于存储敏感信息,如用户名、密码、连接url、网络连接等。JVM类加载器在加载类的时也广泛地使用它。如果是可变的,那么这个字符串内容就可能随时都被修改。那么这个字符串内容就完全不可信了。这样整个系统就没有安全性可言了。
  3. 线程安全:不可变会自动使字符串成为线程安全的,因为当从多个线程访问它们时,它们不会被更改。
  4. 可用于hashcode缓存:由于字符串对象被广泛地用作数据结构,它们也被广泛地用于哈希实现,如HashMap、HashTable、HashSet等。在对这些散列实现进行操作时,经常调用hashCode()方法。不可变性保证了字符串的值不会改变。因此,hashCode()方法在String类中被重写,以方便缓存,这样在第一次hashCode()调用期间计算和缓存散列,并从那时起返回相同的值。

3、字符串常量从哪来的?

字符串常量池的位置:

在JDK 1.6及之前的版本,字符串常量池通常被实现为方法区的一部分,即永久代(Permanent Generation),用于存储类信息、常量池、静态变量、即时编译器编译后的代码等数据。

从JDK 1.7开始,字符串常量池的实现方式发生了重大改变。字符串常量池不再位于永久代,而是直接存放在堆(Heap)中,与其他对象共享堆内存。

之所以要挪到堆内存中,主要原因是因为永久代的 GC 回收效率太低,只有在FullGC的时候才会被执行回收。但是Java中往往会有很多字符串也是朝生夕死的,将字符串常量池放到堆中,能够更高效及时地回收字符串内存

字符串常量从哪来?

1、字面量常量
在代码中直接使用双引号括起来的字符串字面值(如String s = “Hollis”)会被认为是常量,并且会在编译后进入class文件的常量池,并且在运行阶段,进入字符串常量池。这是最常见的字符串常量来源。

2、intern()方法
String类提供了一个intern()方法,用于将字符串对象手动添加到字符串常量池中。

如果字符串池中已经存在一个等于该字符串的对象,intern()方法会返回这个已存在的对象的引用。

如果字符串池中没有等于该字符串的对象,intern()方法会将该字符串添加到字符串池中,并返回对新添加的字符串对象的引用。

4、String中intern的原理是什么?

根据intern方法的原理,判断下第4行和第8行的结果,并分析原因。

public static void main(String[] args) {String s1 = new String("a"); // ①s1.intern(); // ②String s2 = "a";// ③System.out.println(s1 == s2); // ④   falseString s3 = new String("a") + new String("a");// ⑤s3.intern();// ⑥String s4 = "aa";// ⑦System.out.println(s3 == s4);// ⑧    true}

这个类被编译后,Class常量池中应该有"a"和"aa"这两个字符串,这两个字符串最终会进到字符串池。但是,字面量"a"在代码①这一行,就会被存入字符串池,而字面量"aa"则是在代码⑦这一行才会存入字符串池

以上代码的执行过程:
第①行,new 一个 String 对象,并让 s1指向他。
第②行,对 s1执行 intern,但是因为"a"这个字符串已经在字符串池中,所以会直接返回原来的引用,但是并没有赋值给任何一个变量
第③行,s2指向常量池中的"a";

所以,s1和 s2并不相等!

第⑤行,new 一个 String 对象,并让 s3 指向他。
第⑥行,对 s3 执行 intern,但是目前字符串池中还没有"aa"这个字符串,于是会把<s3指向的String对象的引用>放入<字符串常量池>
第⑦行,因为"aa"这个字符串已经在字符串池中,所以会直接返回原来的引用,并赋值给 s4;

所以,s3和 s4 相等!

5、intern的正确用法

String s3 = new String("Hello").intern();

如果真的理解了第4部分intern的原理,就可以发现以上代码中的intern是多余的,因为字面量常量"Hello"已经在字符串常量中了。

如果在字符串拼接中,有一个参数是非字面量,而是一个变量的话,整个拼接操作会被编译成StringBuilder.append,这种情况编译器是无法知道其确定值的。只有在运行期才能确定。

那么,有了这个特性了,intern就有用武之地了。那就是很多时候,我们在程序中得到的字符串是只有在运行期才能确定的,在编译期是无法确定的,那么也就没办法在编译期被加入到常量池中

这时候,对于那种可能经常使用的字符串,使用intern进行定义,每次JVM运行到这段代码的时候,就会直接把常量池中该字面值的引用返回,这样就可以减少大量字符串对象的创建了

参考链接

  1. https://www.yuque.com/hollis666/wk6won/ik9x1gx4zddllhhg
  2. https://www.yuque.com/hollis666/wk6won/hhkgh2nsrlnf2g0g
  3. https://www.yuque.com/hollis666/wk6won/koc3uykar8eg3oxt
  4. https://www.yuque.com/hollis666/wk6won/yr32wu44yxt5l8nh
  5. https://www.yuque.com/hollis666/wk6won/em12e4rgw6suv75o
http://www.lryc.cn/news/615085.html

相关文章:

  • C++11 ---- 线程库
  • 3.2Vue Router路由导航
  • B.10.01.3-性能优化实战:从JVM到数据库的全链路优化
  • 区块链密码学简介
  • (LeetCode 每日一题) 231. 2 的幂 (位运算)
  • 基于clodop和Chrome原生打印的标签实现方法与性能对比
  • 通过 SCP 和 LXD 配置迁移 CUDA 环境至共享(笔记)
  • 数据标准化与归一化的区别与应用场景
  • FAN5622SX 四通道六通道电流吸收线性LED驱动器,单线数字接口 数字式调光, 2.7 → 5.5 V 直流直流输入, 30mA输出FAN5622S
  • C++ unordered_map 和 unordered_set 的使用
  • 新手向:Python开发简易待办事项应用
  • 【JS-8-Json】深入理解JSON语法及Java中的JSON操作
  • Visual Studio Code (v1.103) 中 GitHub Copilot 最新更新!
  • [TryHackMe]Challenges---Game Zone游戏区
  • 避不开的数据拷贝(2)
  • 第二十天:数论度量
  • 【面试场景题】通过LinkedHashMap来实现LRU与LFU
  • C++隐式转换的魔法与陷阱:explicit关键字的救赎
  • 软件工程总体设计:从抽象到具体的系统构建之道
  • Python基础教程(六)条件判断:引爆思维Python条件判断的九层境界
  • 轻量化阅读应用实践:21MB无广告电子书阅读器测评
  • MySQL(188)如何使用MySQL的慢查询工具?
  • Spring Boot 2 集成 Redis 集群详解
  • 聊聊经常用的微服务
  • MBR分区nvme固态硬盘安装win7--非UEFI启动和GPT分区
  • day30-HTTP
  • 大语言模型提示工程与应用:LLMs文本生成与数据标注实践
  • 在Docker中下载RabbitMQ(详细讲解参数)
  • docker基础前置
  • STM32H503不同GPIO速度配置(HAL库)对应的最高速度