你真的理解Java String么?

Thursday, August 1, 2019

TOC

String类

java.lang.Stringfinal类,这意味着它内部的属性都被隐式地指定为final。对于final修饰的类,意味着它不可被继承。对于final修饰的属性,意味着不能修改.

final修饰的属性

如果属性是基础类型,则不能改值。如果属性是对象引用,则不能改变所引用的地址.

String内部共有2个重要属性:

// 实际保存字符串的值
private final char value[];
// hash值
private int hash; // Default to 0

char数组用于保存字符串内部的每个字符,字符都是以UTF-16编码保存的。char被修饰为private final,内部所有操作均不暴露char,这是JVM刻意要将数组设计成不可变的对象。了解了这一点,面对创建了几个对象的问题,就可以手到擒来了.

String a = "a"; // 创建了一个对象
String b = a + "b"; // 这里又创建一个新的对象,千万注意不是在原有对象基础上做修改

hash是字符串的hash值,通过对每个字符做数值计算得到。所以两个值相同的字符串的hash值肯定是一致的。

String对象的保存位置

运行时通过new String("value")来创建,那么该对象会被保存在堆内存中。如果使用引号"value"来创建,则会将对象保存在字符串常量池(以下简称常量池)中。常量池的存在可以使得相同值的字符串在内存中仅保留一份拷贝,可以节省空间。**它的底层类似于一个线程安全的HashMap,相同Hash值的字符串落到一个bucket中退化为链表保存。所以保存在常量池中的字符串如果distinct数量庞大,就需要考虑扩大池的容量。**JVM提供了-XX:StringTableSize=N来进行配置。

题外话

String str = “value”; 这种操作涉及到自动装箱,在JVM装箱的过程中做了:

  1. 检查常量池
  2. 往常量池写入新对象
  3. 返回常量池中的对象的引用.
String str1 = new String("a");
String str2 = new String("a");
System.out.println(str1 == str2);
// false 两次调用String构造方法均生成了一个新的对象保存在堆中,不是同一个

String str3 = "a";
String str4 = "a";
System.out.println(str3 == str4);
// true 通过自动装箱创建的两个对象,均指向常量池中的相同地址

String str5 = "aa";
String str6 = str3 + "a";
System.out.println(str5 == str6);
// false str5通过自动装箱创建,保存在常量池中。
//       str6是在运行时计算得到新对象,保存在堆中

但是要注意,在不同的JDK版本中,常量池的实现是不同的

  • JDK6 – 这个版本将常量池放在方法区中,由于方法区不能扩容,而且方法区gc也非常困难,很容易产生OOM
  • JDK7 – 将常量池放在堆内存,这样做可以使得字符串可以跟普通对象一样维护。

String#intern方法

有了前面的铺垫,intern()方法的引入就非常自然了。我们通过new String()构造的对象都是保存在堆中的,所以重复的值会创建冗余的对象占用内存。这时就可以通过调用intern()方法来强制写入常量池,如果池内有同值字符串对象,则直接返回引用,否则将新对象保存在池中。

高性能String Pool

结论说在前面,并不建议将业务构建在常量池之上。性能问题主要出在两个方面:

  1. 当distinct字符串数量太多,退化成链表的hashmap性能极差
  2. 并发情况下常量池的同步性能比较低(这个详情没有研究过,读者有兴趣可以深入研究原因)

所以如果确实需要使用字符串常量池,不妨通过ConcurrentHashMap这样的线程安全容器自己实现一套。