由面试题引发的对于String的思考

先看一些面试题,如下:

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()
Returns a canonical representation for the string object.

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 Stringobject is returned.

It follows that for any two strings s and ts.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这部分的简单总结和记录吧,当然还有其他很多内容,没有展开写。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值