先看一些面试题,如下:
String s1 = "aba";
String s2 = new String("aba");
System.out.println(s1==s2);
String a = "aaa";
String b = "a" + "aa";
System.out.println(a == b);
String s = new String("aaa");
s.intern();
String ss = "aaa";
System.out.println(s == ss);
String s = new String("aaa") + new String("bbb");
s.intern();
String ss = "aaabbb";
System.out.println(s == ss);
这些题目相信大家也不陌生,你知道输出的是什么吗?还有很多类似的题目不一一举例了。
说实话,当初是看intern方法的作用,看着看着,又研究了String源码,然后牵扯出一大堆东西,研究了一个星期。
下面先说String。
看过某宝典,里面有道题:String和StringBuilder的区别,答案说String是不可变的,StringBuilder是可变的,那么为什么String是不可变的?大家都知道String是final类,final修饰的类是不能被继承的,难道因此就是不可变的?当然不是,查看String源码,看到有个char数组的value属性,由此知道String其实就是对char[]的一种封装,这个value也是final,因此是不可变的,那么String真的就不可变吗,有没有什么办法改变呢?当然有,利用反射,我们就能改变String的内容,但不推荐这么做,也没有什么意义,这也不是我们要讨论的主要内容,或许以后再写篇来研究下也未尝不可。
String类里提供的方法也非常多,我们不一一讨论,重点来说下intern方法。
intern方法的作用,看看API中的描述:
public String intern()
A pool of strings, initially empty, is maintained privately by the class String
.
When the intern method is invoked, if the pool already contains a string equal to this String
object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String
object is added to the pool and a reference to this String
object is returned.
It follows that for any two strings s
and t
, s.intern() == t.intern()
is true
if and only if s.equals(t)
is true
.
All literal strings and string-valued constant expressions are interned. String literals are defined in section 3.10.5 of the The Java™ Language Specification.
Returns:
a string that has the same contents as this string, but is guaranteed to be from a pool of unique strings.
大致意思是:
一个初始时为空的字符串池,它由类 String 私有地维护。
当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并且返回此 String 对象的引用。
它遵循对于任何两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。
所有字面值字符串和字符串赋值常量表达式都是内部的。
返回:
一个字符串,内容与此字符串相同,但它保证来自字符串池中。
在研究过程中,发现jdk1.6和jdk1.7不同,原来常量池的位置发生的变化,而这又得从jvm开始说起了。这里主要说下jvm内存结构。
jvm运行时数据区我们可以分为2类:一类是数据共享的,一类是线程私有的。
内存主要分为6部分,分别是pc寄存器,java虚拟机栈,java堆,方法区,运行时常量池和本地方法栈。其中java堆和方法区是线程共享的,其他的是线程私有的。当然还有一种直接内存的说法,它不是运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但这部分也频繁使用,也容易出现内存溢出。
我们看到内存结构中有一个常量池的区域,它是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。Java语言并不要求常量一定只有编译期才能产生,运行期间也可以将新的常量放入池中,而用的比较多的就是String类的intern()方法。扯了这么多,终于回到正题了。
在jdk1.6之前,这个运行时常量池是堆外的一个空间,而jdk1.7将这部分移到堆中,变成元数据区。这就导致了intern()方法的在不同版本中的表现不同了。
简单的说,jdk1.6,intern()方法是在常量池中寻找是否有相同的字符串,如果有则返回,如果没有,就复制一个,然后返回池中的字符串;而jdk1.7,则是寻找是否有相同的字符串,如果有则返回,到这里与jdk1.6是相同的,不同的在于,如果没有,则不是复制一个,而是将地址加入到元数据区,然后返回。
现在再来看看开篇最后一段代码,在jdk1.6的运行结果是 false,在jdk1.7的运行结果是true。
解释下(假设内存中不存在要创建的字符串,同时不考虑编译期优化、逃逸等):
String s = new String("aaa") + new String("bbb");
运行上面这句,在常量池中创建"aaa"、"bbb",在堆中创建2个String对象,而这里使用了操作符“+”,这个操作符在java中有重载(java不像c++可以让程序员重载操作符),我这里直接给出答案,不再具体分析,有兴趣的朋友可以上网搜索。这句话实际是在内存中创建了StringBuilder来完成连接的,所以内存中应该还有一个StringBuilder对象,然后s指向的是StringBuilder的toString方法创建的String对象,其值为“aaabbb”,而到此为止这个字符串在常量池中是不存在的,接着运行第二句:
s.intern();
先查找常量池中是否有“aaabbb”,发现没有,那么如果是jdk1.6,则把“aaabbb”复制到常量池中,然后返回池中的地址;如果是jdk1.7,则把堆中的“aaabbb”的地址(也就是s所指向的地址)加到常量池中(元数据区),然后返回这个地址。
再看第三句:
String ss = "aaabbb";
由于经过第二句代码,常量池中已经存在“aaabbb”,那么直接返回该地址,对于jdk1.6,返回的就是复制后的,对于jdk1.7,返回的就是加入的地址。
所以,随后判断s与ss的地址,在jdk1.6中是false,jdk1.7是true。
在jdk1.7中String的变化还是比较多的,不仅上述变化,switch结构中也可以使用String了,并且修复了内存溢出问题,具体的就是原来String都是通过offset和count来返回一个新的值,这样会导致如果一个很大的char数组,实际只使用小部分数据,如果一直创建这样的数据,而由于这个char[]数组一直被使用,无法被gc回收,最后就会内存溢出。jdk1.7去掉了offset和count属性,所有返回的String都是重新复制的一个数组,虽然性能上有一点影响,但解决了内存溢出的bug,也算时间换空间吧。当然为了效率还是有提供一个构造器:
String(char[] value, boolean share) {
// assert share : "unshared not supported";
this.value = value;
}
不过这个构造器只能包内使用,不允许外部使用,这样就防止了上述内存溢出问题了。
以上算是对这个星期研究String这部分的简单总结和记录吧,当然还有其他很多内容,没有展开写。