String的对象是不可变的,如下:
String str = "abc";
str = "def";
在上述代码中,实际上分别创建了abc和def两个字符串,str只是由指向abc的引用变为指向def的引用。
我们来看下Java中String类的源代码
/** The value is used for character storage. */
private final char value[];
我们可以看到,其实String类在Java内部是以final修饰的字符数组形式存储的。这也就表明了,String只会初始化一次,并且不可被继承。
我们通过源代码再来看下对String进行分割,合并等操作后String是否会发生变化。
//分割字符串
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);
}
//合并字符串
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中是不允许程序员对操作符重载的,但是Java自身对+操作符进行了重载。
在String的操作中,+操作符表示字符串连接。
String str = "java";
String str2 = "hello " + str;
System.out.println(str2);
console:
hello java
我们反编译上述代码
0: ldc #16 // String java
2: astore_1
3: new #18 // class java/lang/StringBuilder
6: dup
7: ldc #20 // String hello
9: invokespecial #22 // Method java/lang/StringBuilder."":(Ljava/lang/String;)V
12: aload_1
13: invokevirtual #25 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
16: invokevirtual #29 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
我只截取了反编译后的部分内容。
可以看出在Java的内部实现中,+操作是以StringBuilder的形式实现的。
注意:
其实这个过程就是:
在String使用+连接字符串的时候,创建了很多String对象,这无疑会影响到效率。
使用StringBuilder/StringBuffer的意义无非就是为了提高操作字符串的效率。
我们先看下源码中关于这两个类的声明。
public final class StringBuffer extends AbstractStringBuilder implements java.io.Serializable, CharSequence{}
public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence{}
都是继承自抽象类AbstractStringBuilder,都熟悉点了Serializable和CharSequence接口。
其实不仅仅是类的声明,StringBuilder/StringBuffer其实是在功能上是完全相同的,只是StringBuffer中的方法大多被synchronized修饰,因此是线程安全的,而其实不仅仅是类的声明,StringBuilder不是线程安全的。
我们使用《Thinking in Java》中的例子来探究下StringBuiler是否真的能提升效率。
我们先提供两个方法
public String stringBuilderAppend(String[] strArray) {
StringBuilder stringBuilder = new StringBuilder();
for(int i = 0; i < strArray.length; i++) {
stringBuilder.append(strArray[i]);
}
return stringBuilder.toString();
}
public String stringAppend(String[] strArray) {
String str = "";
for(int i = 0; i < strArray.length; i++) {
str += strArray[i];
}
return str;
}
我们反编译后上述代码
public java.lang.String stringBuilderAppend(java.lang.String[]);
Code:
10: goto 24
13: aload_2
14: aload_1
15: iload_3
16: aaload
17: invokevirtual #19 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: pop
21: iinc 3, 1
24: iload_3
25: aload_1
26: arraylength
27: if_icmplt 13
public java.lang.String stringAppend(java.lang.String[]);
Code:
5: goto 32
8: new #16 // class java/lang/StringBuilder
11: dup
12: aload_2
13: invokestatic #37 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
16: invokespecial #43 // Method java/lang/StringBuilder."":(Ljava/lang/String;)V
19: aload_1
20: iload_3
21: aaload
22: invokevirtual #19 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
25: invokevirtual #23 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
28: astore_2
29: iinc 3, 1
32: iload_3
33: aload_1
34: arraylength
35: if_icmplt 8
}
我们只保留两个方法中的循环部分。
注意:
我们明白了这两项以后就可以看出,String使用+连接字符串的时候,每次都会创建StringBuilder对象,而这肯定是会影响执行效率的。
最后,我想说一些关于StringBuilder和StringBuffer的实现原理的内容。
我们来看它们的父类AbstractStringBuilder的源代码(只截取一部分)。
/**
* The value is used for character storage.
*/
char[] value;
/**
* The count is the number of characters used.
*/
int count;
/**
* This no-arg constructor is necessary for serialization of subclasses.
*/
AbstractStringBuilder() {
}
/**
* Creates an AbstractStringBuilder of the specified capacity.
*/
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
/**
* Returns the length (character count).
*
* @return the length of the sequence of characters currently
* represented by this object
*/
@Override
public int length() {
return count;
}
我们可以看到,AbstractStringBuilder内部也是以字符数组实现的。注意count是指实际长度,而capacity指的是容量,注意它们的区别。
/**
* Ensures that the capacity is at least equal to the specified minimum.
* If the current capacity is less than the argument, then a new internal
* array is allocated with greater capacity. The new capacity is the
* larger of:
*
* - The {@code minimumCapacity} argument.
*
- Twice the old capacity, plus {@code 2}.
*
* If the {@code minimumCapacity} argument is nonpositive, this
* method takes no action and simply returns.
* Note that subsequent operations on this object can reduce the
* actual capacity below that requested here.
*
* @param minimumCapacity the minimum desired capacity.
*/
public void ensureCapacity(int minimumCapacity) {
if (minimumCapacity > 0)
ensureCapacityInternal(minimumCapacity);
}
/**
* For positive values of {@code minimumCapacity}, this method
* behaves like {@code ensureCapacity}, however it is never
* synchronized.
* If {@code minimumCapacity} is non positive due to numeric
* overflow, this method throws {@code OutOfMemoryError}.
*/
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
以上是扩容的两个方法,原理很简单,大家可以自己思考下。还有个trimToSize()方法,是缩减容量的。
至于StringBuilder和StringBuffer所实现的方法,在这里不多说,大家可以去看API进行学习。
之前我在【CoreJava】equals()和==的比较中提到过String的存放位置,但只是稍微讲了下关于两种创建方式存储位置的不同。
我们使用两种种方式创建字符串
String str1 = "abc";
String str2 = new String("abc");
我们反编译上面的代码
0: ldc #16 // String abc
2: astore_1
3: new #18 // class java/lang/String
6: dup
7: ldc #16 // String abc
9: invokespecial #20 // Method java/lang/String."":(Ljava/lang/String;)V
12: astore_2
13: return
可以看出两种方式都是首先从字符创常量池中寻找abc,但是它们的区别在哪呢?
这就是我们为什么推荐使用
String str = "abc";
这种形式创建字符串了。
字符串的内容也基本结束了,《Thinking in Java》中还补充了正则表达式和格式化输出的一些内容。正则表达式可以作为一整篇文章来讲,而且我自己目前也不太熟练,所以就不在这多说了。格式化输出呢,我用的比较少,平时用的也不多,所以也没有怎么研究过。毕竟脑子有限,还是要有一些侧重的。