【Java基础】字符串不可变性、string的intern原理
【Java基础】字符串不可变性、string的intern原理
- 1、String是如何实现不可变的?
- 2、String为什么设计成不可变的?
- 3、字符串常量从哪来的?
- 4、String中intern的原理是什么?
- 5、intern的正确用法
1、String是如何实现不可变的?
string不可变的基本原理:
- String类被声明为final,这意味着它不能被继承。那么他里面的方法就是没办法被覆盖的。
- 用final修饰字符串内容的char[](从JDK 1.9开始,char[]变成了byte[]),由于该数组被声明为final,一旦数组被初始化,就不能再指向其他数组。
- 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个原因:
- 缓存:字符串是使用最广泛的数据结构。大量的字符串的创建是非常耗费资源的,所以,Java提供了对字符串的缓存功能,可以大大的节省堆空间。通过字符串池,两个内容相同的字符串变量,可以从池中指向同一个字符串对象,从而节省了关键的内存资源。
- 安全性:字符串在Java应用程序中广泛用于存储敏感信息,如用户名、密码、连接url、网络连接等。JVM类加载器在加载类的时也广泛地使用它。如果是可变的,那么这个字符串内容就可能随时都被修改。那么这个字符串内容就完全不可信了。这样整个系统就没有安全性可言了。
- 线程安全:不可变会自动使字符串成为线程安全的,因为当从多个线程访问它们时,它们不会被更改。
- 可用于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运行到这段代码的时候,就会直接把常量池中该字面值的引用返回,这样就可以减少大量字符串对象的创建了。
参考链接
- https://www.yuque.com/hollis666/wk6won/ik9x1gx4zddllhhg
- https://www.yuque.com/hollis666/wk6won/hhkgh2nsrlnf2g0g
- https://www.yuque.com/hollis666/wk6won/koc3uykar8eg3oxt
- https://www.yuque.com/hollis666/wk6won/yr32wu44yxt5l8nh
- https://www.yuque.com/hollis666/wk6won/em12e4rgw6suv75o