1. class 简介
String类代表多个单个字符的集合体,所有的字符串字面常量如”abc”等都是String类的实例,String对象所代表的字符串一旦创建其内部内容就不可改变,也正因为这一点,String是可以共享的。String类提供了很多操作字符串的实用方法,比如字符串比较,复制,连接,切割,截取,替换,匹配,变量字符串化,大小写变换等。但是不管如何操作,原字符串都是不会发生变化的。
2. class内部原理及特点
- String类内部其实就是一个final char[] value字符数组,一个起始位置指针offset和一个字符计数器count,final表明了String类的内部内容是不可改变的。
- String类的构造方法可以方便地将字符数组、字节数组、StringBuilder、StringBuffer转换成String类对象,对字符数组基本上就是使用Arrays.copyOf等方法进行复制,对于字节数组使用StringCoding类进行编码转换。
- String类的各种语义上会使字符串内容发生改变的方法(比如拼接、截取),每被调用一次内部最终都会生成一个新字符串,如果频繁地对String类对象进行操作的话,会导致堆中垃圾过多,可能会影响程序性能。
- 字符串字面常量使用享元模式存放在常量池中,只保留一份,两个相同的字符串字面常量的地址是相同的。
3. class源码细节分析
String类的大部分方法的实现细节都非常的简单(正则表达式跳过),同时也非常繁琐无味,一些核心特点都已经写在上面了,这里只记录一些让人困惑或者有意义的方法。
getChars
/*
这个包内方法我是看一次忘一次,经常忘记它的意思,每次都要看下它的内部实现,才知道是把String内部数组复制到传入的参数数组中。
感觉这种把目标当参数传入取值而不是把目标当返回值返回的写法太C语言化了。。。
*/
void getChars(char dst[], int dstBegin) {
System.arraycopy(value, offset, dst, dstBegin, count);
}
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > count) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
System.arraycopy(value, offset + srcBegin, dst, dstBegin,
srcEnd - srcBegin);
}
concat和+
使用String的concat方法拼接字符串是要比+拼接字符串要快的(当然和StringBuilder和StringBuffer没法比)。
个人分析如下:
/* 如果参数字符串长度为0,则返回自身。
拼接一次出现了两次new
*/
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
char buf[] = new char[count + otherLen];
getChars(0, count, buf, 0);//先把自身的字符数组放入前半部分
str.getChars(0, otherLen, buf, count);//再把参数字符数组放入后半部分
return new String(0, count + otherLen, buf);//新生成一个String
}
/* 而使用+拼接,本质上是使用了StringBuilder对象的append方法
以下为测试
*/
public static void main(String[] args) {
String str1 = "";
long start = System.currentTimeMillis();
for(int i=0; i<100000; i++){
//str1 = str1.concat("a");
str1 += "a";
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
/* 以下是javap -c反编译出的字节码 */
public static void main(java.lang.String[]);
Code:
0: ldc #16 // String
2: astore_1
3: invokestatic #18 // Method java/lang/System.currentTimeMillis:()J
6: lstore_2
7: iconst_0
8: istore 4
10: goto 36
13: new #24 // class java/lang/StringBuilder
16: dup
17: aload_1
18: invokestatic #26 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
21: invokespecial #32 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
24: ldc #35 // String a
26: invokevirtual #37 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
29: invokevirtual #41 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
32: astore_1
33: iinc 4, 1
36: iload 4
38: ldc #45 // int 100000
40: if_icmplt 13
43: invokestatic #18 // Method java/lang/System.currentTimeMillis:()J
46: lstore 4
48: getstatic #46 // Field java/lang/System.out:Ljava/io/PrintStream;
51: lload 4
53: lload_2
54: lsub
55: invokevirtual #50 // Method java/io/PrintStream.println:(J)V
58: return
/*
首先行号13,生成了一个StringBuilder对象;
行号18,调用了String.valueOf方法,也会生成新的对象
行号26,使用append方法进行拼接,可能会生成新的字符数组(扩容时)
行号29,StringBuilder.toString,会生成新的String对象
*/
那么结论就是concat方法拼接一次会new两次,而+拼接一次会new三次或四次,自然+效率更低。
注意了,+拼接效率低是发生在循环中使用+拼接的情况,如果是下面这样:
String str = "select xxx,xxx,xxx,xxx,xxx,xxx,xx "
+ "from xxtable1 inner join xxtable2 "
+ "on xxtable1.id = xxtable2.id "
+ "where xx = xxx";
像这样连续使用+拼接,如果去反编译观察的话,相当于
str= StringBuilder("xx").append("xx").append("xx").append("xx")......append("xx").toString();
这样和StringBuilder有什么两样?实际上+拼接编译器还会优化的更好,只有在循环中不要使用+连续拼接,把+当成换行拼接来用是没有任何问题的,可以大胆放心的使用。
toString、String.valueOf和+”“
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
/*
本质上还是使用了toString,但是使用String.valueOf如果对象为null并不会抛出异常;
而直接toString的话,如果是null对象,则会抛出空指针异常;
*/
intern
/*
一个String对象str调用intern方法,会从常量池中返回一个内容和str相同的String对象,其地址在常量池中;
如果池中不存在和str内容相同的String对象,则会在池中新生成一个内容和str相同的String对象,再返回这个对象,其地址也是在常量池中的。
*/
public native String intern();
比如:
public static void main(String[] args) {
String str1 = new String("abc");
String str2 = "abc";
System.out.println(str1 == str2); //str1在堆中,str2在池中,地址肯定不相等
str1.intern();
System.out.println(str1 == str2); //虽然str1调用了intern(),但是返回值丢弃了,str1没变化,所以结果和上面一样
System.out.println(str1.intern() == str2); //左右两边都是池中地址
}
结果:
false
false
true
4. 总结
String类的各种字符串操作方法实现起来都很简单,实在是没有必要浪费时间把那些方法都分析个里里外外清清楚楚;String类使用频率极高,但是由于内容是不可变的,它并不适合进行频繁的拼接操作。