JAVA源码之JDK(三)——String、StringBuffer、StrinBuilder

时间:2024-11-22 14:35:49

Java中,除了8种基本类型,最长用的应该就是String类了。那么我们来看看JDK中的源码是怎么建造String、StringBuffer、StrinBuilder一系列类的。


java.lang.String

在JAVA里,String类是一个非常特殊的类,我们来看一下它是怎么来表示一个字符串的。首先来看一下它的比较重要的几个属性,源码如下:

/** The value is used for character storage. */
private final char value[]; /** The offset is the first index of the storage that is used. */
private final int offset; /** The count is the number of characters in the String. */
private final int count;

是的,其实String是用一个char数组来储存的,offset表示char[]的起始位置。count表示字符串的长度,通过字符串的length()方法只有一行return count;就看得出来。

  这里的属性都是final的,这就是String值为什么不能被改变【可不是因为final class String的final呦】。这么做的好处是String对象可以在不同用户间共用,本身是线程安全的。

来看一个构造方法:

 public String(String original) {
int size = original.count;
char[] originalValue = original.value;
char[] v;
if (originalValue.length > size) {
// 如果传入的original的存储char[]的长度大于要构造的字符串的长度,就copy original有效的那部分。
int off = original.offset;
v = Arrays.copyOfRange(originalValue, off, off+size);
} else {
// 因为传入的original的存储char[]的长度等于要构造的字符串的长度,所以无需copy
v = originalValue;
}
this.offset = 0;
this.count = size;
this.value = v;
}

  先说点题外的,笔者我第一次看到第2行的时候感到很诧异,String类的count属性不是private的吗。后来经过自己实验并反复回忆初学JAVA时老师对private的描述,private修饰的属性是只有在类的内部时才能被访问,于是恍然大悟。重新认识了private关键字,也是个收获。

  再来说这个构造方法,大概翻译了下JDK的注释。这里有两点,一是第8行的数组复制,里面是最终用的是System.arraycopy()方法,这个方法在JAVA中复制数组是最快的【这是个native方法,至于底层怎么实现就不太了解,猜测是通过内存里的地址吧】。再就是第11行的v = originalValue;,就是说,用于构造String的字符串,和构造出来的新的字符串,value属性是指向同一个char[]的【其实这也无所谓,因为String.value是final的】。

  关于字符集的构造方法这里就不说了,转换来转换去好烦。

  下面来看几个Sring类常用的方法的实现:

public boolean equals(Object anObject)

public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = count;
if (n == anotherString.count) {
char v1[] = value;
char v2[] = anotherString.value;
int i = offset;
int j = anotherString.offset;
while (n-- != 0) {
if (v1[i++] != v2[j++])
return false;
}
return true;
}
}
return false;
}

先比较hashCode,然后再遍历对比两个String的value值,没什么好说的。

public boolean startsWith(String prefix, int toffset)

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;
}

也很简单,遍历字符串的value值。不过注释很有意思:-1>>>1。【-1的无符号右移1位,就是整数最大值,你懂得

public int indexOf(String str)

static int indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex) {
if (fromIndex >= sourceCount) {
return (targetCount == 0 ? sourceCount : -1);
}
if (fromIndex < 0) {
fromIndex = 0;
}
if (targetCount == 0) {
return fromIndex;
} char first = target[targetOffset];
//遍历source时,如果剩下的字符比target还少,就没必要继续遍历了,就是这个max的作用。
int max = sourceOffset + (sourceCount - targetCount); for (int i = sourceOffset + fromIndex; i <= max; i++) {
/* 找到匹配到的第一个字符 */
if (source[i] != first) {
while (++i <= max && source[i] != first);
} /* 匹配整个target是否完全匹配,如果不完全匹配并且source字符串还没遍历完就继续外层的for循环 */
if (i <= max) {
int j = i + 1;
int end = j + targetCount - 1;
for (int k = targetOffset + 1; j < end && source[j] ==
target[k]; j++, k++); if (j == end) {
/* Found whole string. */
return i - sourceOffset;
}
}
}
return -1;
}

我们在使用这个方法时会有一个陷阱,就是sourceString.indexOf(""),传入空字符串会返回0。

public String substring(int beginIndex, int endIndex)

public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > count) {
throw new StringIndexOutOfBoundsException(endIndex);
}
if (beginIndex > endIndex) {
throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
}
return ((beginIndex == 0) && (endIndex == count)) ? this :
new String(offset + beginIndex, endIndex - beginIndex, value);
}

很简单,构造一个新的字符串。

  总之,这些常用的方法也不过是对char[]翻来覆去的操作。但提到String类就不得不提String Pool,这是JVM为了提高效率的一个缓存机制【至于JVM的这个原理,研究层次就更深入了,我想我以后会去了解吧】。目前要搞清楚String对象什么时候会在String Pool中建立,什么时候在Heap中建立,并将引用指向哪个,这个也是非常重要的。

String Pool(字符串池)

在创建字符串时,JVM的管理有如下特点:

  • 每当用任意方式创建一个String对象时,JVM都会String Pool中查找是否存在内容相同的字符串对象,如果不存在,则在池中创建一个字符串。
  • 在使用new关键字或包含变量的字符串拼接时,一定会在Heap中建立这个对象,并将引用指向这个对象。【同时仍然会维护String Pool】
  • 使用等号直接指定,或纯字符串拼接时,只会操作String Pool,如果有这个字符串则直接将引用指向,如果没有则创建后指向。
String temp = "ab";
String str1 = new String("abcd");
String str2 = "abcd";
String str3 = "ab" + "cd";
String str4 = temp + "cd"; System.out.println(str1 == str2);//false
System.out.println(str2 == str3);//true
System.out.println(str3 == str4);//false

有兴趣的可以自己试试,这里列举几个例子。

public native String intern();

  最后来说intern()。这是个native方法,它的执行结果是将字符串的引用指向String Pool中的对象。这样Heap中的对象就可以被回收掉了。

java.lang.StringBuffer

  我们经常需要String的变化,但String却是不可变的,那么只好串接后生成一个新的String对象来满足变化的需求。如果频繁的进行字符串拼接就会产生出大量的对象,这样很耗性能。所以这时,轮到StringBuffer来登场了。顾名思义,这是一个字符串缓冲区,可以随意变化。StringBuffer 上的主要操作是 append 和 insert 方法。

public synchronized StringBuffer append(String str)

 public synchronized StringBuffer append(String str) {
super.append(str);//父类方法如下
return this;
}
 public AbstractStringBuilder append(String str) {
if (str == null) str = "null";
int len = str.length();
if (len == 0) return this;
int newCount = count + len;
if (newCount > value.length)
expandCapacity(newCount);//这个扩容的方法如下
str.getChars(0, len, value, count);
count = newCount;
return this;
}
 void expandCapacity(int minimumCapacity) {
int newCapacity = (value.length + 1) * 2;
if (newCapacity < 0) {
newCapacity = Integer.MAX_VALUE;
} else if (minimumCapacity > newCapacity) {
newCapacity = minimumCapacity;
}
value = Arrays.copyOf(value, newCapacity);
}

  从上面我们可以看出,一是如果拼接字符串时,变量为NULL,则按"null"来处理;二则是那个扩容。我们在构造一个StringBuffer时,如果不设置一个长度,则JDK会默认长度为16,如果我们最终拼接的字符串的长度远远超出这个长度,则StringBuffer会频繁的做扩容操作,所以,在new StringBuffer(),传入一个预估的长度参数是一个好习惯。

java.lang.StringBuilder

这个类与StringBuffer的区别就是synchronized,提高效率嘛,都懂得。笔者这里测试了一下,大概也就差1倍的速度。测试代码如下:

StringBuffer sb1 = new StringBuffer(1024);
StringBuilder sb2 = new StringBuilder(1024);
for(int i = 0; i < 10000000; i++){
//sb1.append("abc");
sb2.append("abc");
if((i & 1023) == 0){
//sb1 = new StringBuffer(1024);
sb2 = new StringBuilder(1024);
}
}

测试结果为:

StringBuffer平均耗时399ms

StringBuilder平均耗时254ms


好了,本次的学习就到这里吧。

学习是件快乐而又有成就感的事。