String是最常用的类之一,也许就是太常见了,因此导致经常忽略它存在,即使在观察堆得时候发现String类以及char[]所占的内存比较多的时候,也潜意识的跳过了它们。其实,String类以及char[]也还是有优化的空间的。
String的源码提供了非常丰富的方法,最多的就是对char[]数组的处理。对数组的处理,几乎就是复制数组和转码的各种组合。本来觉得看下去以及没啥惊喜了,指到发现以下这个方法:
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java™ Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();
这一个看似普通的本地方法, 返回的是该Sting对象所指向的常量在常量池的地址。这里就不得不分析以下复杂的String对象关系了。也是导致为啥我们经常约定判断String对象是否相等时,不是用的“==”而是用equals()方法呢?是不是所有的时候2个String对象一定是不相等的呢?要说明这个问题,理解intern()方法就非常重要了。
intern()被调用的时候,首先是查看常量池中是否存在等于该String对象value的字符串,如果存在,则返回该字符串的地址,如果不存在就将该String对象的作为新的常量,那么新常量的地址和String对象的地址就是同一个了。这里有个前提就是在JDK1.7之后,常量池并入Java堆中。因此,上述的场景是处于JDK1.7之后的环境,我的验证环境为JDK1.8。
public class App {
public static void main(String[] args) {
String s1 = new String("aaa");
String s2 = s1.intern();
System.out.println(s1 == s2);
}
}
根据上面对于intern()描述,第一印象结果应该是true,但是实际结果却是false。这个就很奇怪了,此时参考美团技术团队的文章终于找到了原因(https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html)。其实,
String s1 = new String("aaa");
中引入"aaa"作为参数的时候,因此在后面创建s1对象时,在常量池中时有了"aaa"常量,此时调用intern()返回的地址和s1不一致的。为了验证这个观点,写了以下代码:
public class App {
public static void main(String[] args) {
char[] chars = new char[]{'a', 'a', 'a'};
String s1 = new String(chars);
String s2 = s1.intern();
System.out.println(s1 == s2);
}
}
此时返回的结果就是true。
那么这个函数有什么用呢?其实就可以考虑用来优化Java堆。创建一个100万大小的数组,随机插入数字String,观察堆的情况和运行时间,记录3次运行数据。
public class App {
private static long MAX = 100 * 10000;
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
List<String> strArr = new ArrayList<>();
Random random = new Random();
for (long i = 0; i < MAX; i++) {
strArr.add(String.valueOf(random.nextInt() % (100 * 10)));
// strArr.add(String.valueOf(random.nextInt() % (100 * 10)).intern());
}
long endTime = System.currentTimeMillis();
System.out.println("Loop is end, Spending time is " + (endTime - startTime) / 1000.0 + "s");
}
}
序号 | 数量 | 大小 | 运行时间 |
1 | 1,005,258 | 24,126kb | 0.151s |
2 | 1,005,246 | 24,126kb | 0.164s |
3 | 1,005,245 | 24,125kb | 0.197s |
平均值 | 1,005,250 | 24,126kb | 0.171s |
序号 | 数量 | 大小 | 运行时间 |
1 | 45,077 | 1,081kb | 0.215s |
2 | 45,195 | 1,084kb | 0.209s |
3 | 45,077 | 1,081kb | 0.204s |
平均值 | 45,116 | 1082kb | 0.209s |
从上面的数据对比可以得知,合理使用intern()确实有降低堆大小的作用,但是也会带来额外的系统开销。因此intern()需要根据场景进行使用,不可滥用。