String可以说是java编程中最常用的对象。用了这么久,大家对它的印象有多深呢?下面我就谈谈我自己对它的一些认知吧。
不可变对象
我们可以看到,String被声明成了final,这样我们可以得到String是不可继承的对象,也是线程安全的对象。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
}
concat
接下来我们看String的concat方法。由于String是不可变的,所以当我们连接一个字符串时,它会合并2哥字符串的value数组,然后用这个数组创建一个新的字符串。
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);
}
“+”连接符的处理
Java 语言提供对字符串串联符号(”+”)以及将其他对象转换为字符串的特殊支持。字符串串联是通过 StringBuilder(或 StringBuffer)类及其 append 方法实现的。
String str1 = "aabc0";
String str2 = "aa" + "bc"+"0";
System.out.println(str1+" "+str2);
System.out.println(str1==str2);
for (int i = 0; i < 1; i++) {
String str3 = "aa" + "bc"+i;
System.out.println(str1+" "+str3);
System.out.println(str1==str3);
}
打印结果:
aabc0 aabc0
true
aabc0 aabc0
false
如果在编译期间就能确定字符串,编译器就会优化String,合并到一起。如上例所示str1==str2为true,表明为同一字符串。
intern浅析
对于字符串常量池,看了网上的很多文章,大部分人貌似都不对。现在在此在总结一遍。
String str1 = new StringBuilder("chaofan").append("wei").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
打印结果:
jdk6 下false false
jdk7 下true false
产生的差异在于在jdk1.6中 intern 方法会把首次遇到的字符串实例复制到永久待(常量池)中,并返回此引用;但在jdk1.7中,只是会把首次遇到的字符串实例的引用添加到常量池中(没有复制),并返回此引用。
所以在jdk1.7中执行上面代码,str1返回true是引用他们指向的都是str1对象(堆中)(池中不存在,返回原引用),而str2返回false是因为池中已经存在”java”了(关键词),所以返回的池的对象,因此不相等。
相信很多 JAVA 程序员都做做类似 String s = new String(“abc”)这个语句创建了几个对象的题目。 这种题目主要就是为了考察程序员对字符串对象的常量池掌握与否。上述的语句中是创建了2个对象,第一个对象是”abc”字符串存储在常量池中,第二个对象在JAVA Heap中的 String 对象。接下来我们分析另一端代码。
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
打印结果是
- jdk6 下
false false
- jdk7 下
false true
为什么会这样呢?美团网给出了很好的解释?我觉得上面的说法比美团网的解释更好,大家可以参考http://tech.meituan.com/in_depth_understanding_string_intern.html。
最根本的原因应该是:
jdk7 版本对 intern 操作和常量池都做了一定的修改。主要包括2点:
- 将String常量池 从 Perm 区移动到了 Java Heap区
- String#intern 方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。
intern需要注意的地方
String的String Pool是一个固定大小的Hashtable,默认值大小长度是1009,如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降(因为要一个一个找)。
在 jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。在jdk7中,StringTable的长度可以通过一个参数指定:
- -XX:StringTableSize=99991
例如:在fastjson 中对所有的 json 的 key 使用了 intern 方法,缓存到了字符串常量池中,这样每次读取的时候就会非常快,大大减少时间和空间。而且 json 的 key 通常都是不变的。这个地方没有考虑到大量的 json key 如果是变化的,那就会给字符串常量池带来很大的负担。
这个问题 fastjson 在1.1.24版本中已经将这个漏洞修复了。程序加入了一个最大的缓存大小,超过这个大小后就不会再往字符串常量池中放了。
更多详情请参考http://tech.meituan.com/in_depth_understanding_string_intern.html
subString内存泄露
首先我们看subString方法。
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);
}
在这个方法中,新建了一个String对象。
在jdk1.7,1.8中,如下:
public String(char value[], int offset, int count) {
....
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
在jdk1.6中
String(int offset, int count, char value[]) {
this.value = value;
this.offset = offset;
this.count = count;
}
我们可以看到,1.6中还是引用了原来的字符串,存在内存泄露问题。1.7及以上使用了数组复制的方法创建了一个新的数组,不存在内存泄露。
其他
字符串分割
在jdk1.6中:优先考虑使用indexOf方法,最后才使用spilt方法。
在jdk1.8中:优先考虑spilt方法,最后是indexOf方法。
为什么?我们对下面代码进行测试:
StringBuffer sb = new StringBuffer();
int i = 1000;
for (int i1 = i; i1 > 0; i1--) {
sb.append(i1).append(";");
}
String old = sb.toString();
int k = 10000;
long l = System.currentTimeMillis();
for (int k1 = k; k1 > 0; k1--) {
old.split(";");
}
System.out.println(System.currentTimeMillis() - l);
long l1 = System.currentTimeMillis();
String tmp = old;
for (int k1 = k; k1 > 0; k1--) {
while (true) {
int j = tmp.indexOf(";");
if (j < 0) {
break;
}
tmp.substring(0, j);
tmp = tmp.substring(j + 1);
}
tmp = old;
}
System.out.println(System.currentTimeMillis() - l1);
结果如下:
- jdk1.6中
1073 295
- jdk1.8中
606 7177
对比1073与606,在1.8中由于取消了offset和count2个字段,所以spilt的性能有了较大的提升。
对比295与7177,在1.6中,由于substring方法不会产生新字符串,所以速度更快。在1.8中每次spilt都会产生新的字符串,造成了性能瓶颈。
startWith替代
我们看看下面一段代码:
String ss = "abcdefgg";
int i = 10000000;
long l = System.currentTimeMillis();
for (int i1 = i; i1 > 0; i1--) {
boolean a = ss.startsWith("abc");
}
System.out.println(System.currentTimeMillis() - l);
long l1 = System.currentTimeMillis();
for (int i1 = i; i1 > 0; i1--) {
boolean c = ss.charAt(0)=='a'&&ss.charAt(1)=='b'&&ss.charAt(2)=='c';
}
System.out.println(System.currentTimeMillis() - l1);
打印结果如下:
- 在jdk1.6中
109 26
- 在jdk1.8中
22 21
为什么会出现这样的结果呢,我们分析代码,发现
//1.6
public boolean startsWith(String prefix, int toffset) {
char ta[] = value;
int to = offset + toffset;
char pa[] = prefix.value;
int po = prefix.offset;
int pc = prefix.count;
// Note: toffset might be near -1>>>1.
if ((toffset < 0) || (toffset > count - pc)) {
return false;
}
while (--pc >= 0) {
if (ta[to++] != pa[po++]) {
return false;
}
}
return true;
}
//1.8
public boolean startsWith(String prefix, int toffset) {
char ta[] = value;
int to = toffset;
char pa[] = prefix.value;
int po = 0;
int pc = prefix.value.length;
// Note: toffset might be near -1>>>1.
if ((toffset < 0) || (toffset > value.length - pc)) {
return false;
}
while (--pc >= 0) {
if (ta[to++] != pa[po++]) {
return false;
}
}
return true;
}
在1.6中第四行,int to = offset + toffset;
进行了整形相加操作,这里影响了方法的执行时间。1.7及以后去掉了offset和count,性能有了极大的提升。