String类源码解析
1. 体系结构
首先看下源码:
final class String implements java.io.Serializable, Comparable<String>, CharSequence
String类是一个final类,因此是不可变的、线程安全的,并实现了Serializable、Comparable和CharSequence接口,String 类是日常开发中使用最频繁的类之一,同时也是非常重要的一个类,因此很有必要针对String类的进一步的理解和分析,而不能仅仅停留在会用的地步
2. 属性
private final char value[];
private int hash; // Default to 0
String类中用一个不可变的char数组来存放字符串,一个int型的变量hash用来存放计算后的哈希值
3. 构造方法
String类有十几个构造方法,我们重点解析下其中的几个
3.1 无惨构造函数
/**
* Initializes a newly created {@code String} object so that it represents
* an empty character sequence. Note that use of this constructor is
* unnecessary since Strings are immutable.
*/
public String() {
this.value = "".value;
}
3.2 String(String original)构造函数
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
这个构造函数接收一个字符串对象orginal作为参数,并用它初始化一个新创建的字符串对象,使其表示一个与参数相同的字符序列;换句话说,新创建的字符串是该参数字符串的副本。除非需要original的显式副本,否则不要使用此构造函数
3.3 String(char value[])
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
该构造函数接受一个字符数组value作为初始值来构造一个新的字符串,以表示字符数组参数中当前包含的字符序列。字符数组value的内容已被复制到字符串对象中,因此后续对字符数组的修改不会影响新创建的字符串
3.4 String(char value[], int offset, int count)
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
该构造函数会分配一个新的字符串,初始值取自字符数组value,offset参数是子数组第一个字符的索引,count参数指定子数组的长度。当count=0且offset<=value.length时,会返回一个空的字符串。
3.5 String(int[] codePoints, int offset, int count)
public String(int[] codePoints, int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= codePoints.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > codePoints.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
final int end = offset + count;
// Pass 1: Compute precise size of char[]
int n = count;
for (int i = offset; i < end; i++) {
int c = codePoints[i];
if (Character.isBmpCodePoint(c))
continue;
else if (Character.isValidCodePoint(c))
n++;
else throw new IllegalArgumentException(Integer.toString(c));
}
// Pass 2: Allocate and fill in char[]
final char[] v = new char[n];
for (int i = offset, j = 0; i < end; i++, j++) {
int c = codePoints[i];
if (Character.isBmpCodePoint(c))
v[j] = (char)c;
else
Character.toSurrogates(c, v, j++);
}
this.value = v;
}
该构造函数从代码点数组构造字符串:
先对offset、count等做判断,看是否超出界限,然后计算字符数组大的精确大小,最后将代码点数组的内容拷贝到数组v中并返回(这里涉及到字符编码的知识,会在Character源码解析中详细叙述)。
3.6 String(byte bytes[], int offset, int length, String charsetName)
public String(byte bytes[], int offset, int length, String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null)
throw new NullPointerException("charsetName");
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(charsetName, bytes, offset, length);
}
这两个构造函数使用指定的字符集解码字节数组,构造一个新的字符串。解码的字符集可以使用字符集名指定或者直接将字符集传入。方法中用到了两个函数checkBounds和StringCoding.decode,其中checkBounds函数,源码如下:
private static void checkBounds(byte[] bytes, int offset, int length) {
if (length < 0)
throw new StringIndexOutOfBoundsException(length);
if (offset < 0)
throw new StringIndexOutOfBoundsException(offset);
if (offset > bytes.length - length)
throw new StringIndexOutOfBoundsException(offset + length);
}
这个方法只是单纯的进行边界检查,length、offset不能小于零,而且offset+lenght不能超出字节数组的长度。
decode源码如下:
static char[] decode(String charsetName, byte[] ba, int off, int len)
throws UnsupportedEncodingException
{
StringDecoder sd = deref(decoder);
String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;
if ((sd == null) || !(csn.equals(sd.requestedCharsetName())
|| csn.equals(sd.charsetName()))) {
sd = null;
try {
Charset cs = lookupCharset(csn);
if (cs != null)
sd = new StringDecoder(cs, csn);
} catch (IllegalCharsetNameException x) {}
if (sd == null)
throw new UnsupportedEncodingException(csn);
set(decoder, sd);
}
return sd.decode(ba, off, len);
}
此方法主要是利用参数中的编码类型先进行解码,然后构造对应的字符串,就不过多解析了。
3.7 String(StringBuffer buffer) 和 String(StringBuilder builder)两个构造方法
public String(StringBuffer buffer) {
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
除了前面所示的,可以从字符串、字符数组、代码点数组、字节数组构造字符串外,也可以使用StringBuffer和StringBuilder构造字符串,注意:使用StringBuffer构造字符串时内部使用了同步代码块
其他几个构造方法也都很简单,也不解析了
4. 常用方法
String类的常用方法都比较简单,里面会经常用到一个native方法,
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
4.1 boolean equals(Object anObject)
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
equals方法经常用得到,它用来判断两个对象从实际意义上是否相等,String对象判断规则:
1. 内存地址相同,则为真。
2. 如果对象类型不是String类型,则为假。否则继续判断。
3. 如果对象长度不相等,则为假。否则继续判断。
4. 从后往前,判断String类中char数组value的单个字符是否相等,有不相等则为假。如果一直相等直到第一个数,则返回真。
由此可以看出,如果对两个超长的字符串进行比较还是非常费时间的。
4.2 int compareTo(String anotherString)
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
这个方法写的很巧妙,先从0开始判断字符大小。如果两个对象能比较字符的地方比较完了还相等,就直接返回自身长度减被比较对象长度,如果两个字符串长度相等,则返回的是0,巧妙地判断了三种情况。
4.3 int hashCode()
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
String类重写了hashCode方法,Object中的hashCode方法是一个Native调用。String类的hash采用多项式计算得来,我们完全可以通过不相同的字符串得出同样的hash,所以两个String对象的hashCode相同,并不代表两个String是一样的。
4.4 String substring(int beginIndex)
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);
}
这个方法是用来截取字符串的,若beginIndex的值为0则直接返回当前字符串,否则会构造一个新的字符串并返回
4.5 String replace(char oldChar, char newChar)
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
这个方法也有讨巧的地方,例如最开始先找出旧值出现的位置,这样节省了一部分对比的时间。replace(String oldStr,String newStr)方法通过正则表达式来判断。
4.6 String concat(String str)
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);
}
concat方法也是经常用的方法之一,它先判断被添加字符串是否为空来决定要不要创建新的对象。
4.7 trim()
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
while ((st < len) && (val[st] <= ' ')) {
st++;
}
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
4.8 intern()
public native String intern();
intern方法是Native调用,它的作用是在方法区中的常量池里通过equals方法寻找等值的对象,如果没有找到则在常量池中开辟一片空间存放字符串并返回该对应String的引用,否则直接返回常量池中已存在String对象的引用
5. 为何String要设计成final类
字符串常量池的需要
字符串常量池(String pool, String intern pool, String保留池) 是Java方法区中一个特殊的存储区域, 当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。
如下面的代码所示,将会在堆内存中只创建一个实际String对象,代码如下:
String s1 = "abcd";
String s2 = "abcd";
假若字符串对象允许改变,那么将会导致各种逻辑错误,比如改变一个对象会影响到另一个独立对象. 严格来说,这种常量池的思想,是一种优化手段.
String s1= "ab" + "cd";
String s2= "abc" + "d";
也许这个问题违反新手的直觉, 但是考虑到现代编译器会进行常规的优化, 所以他们都会指向常量池中的同一个对象. 或者,你可以用 jd-gui 之类的工具查看一下编译后的class文件.
允许String对象缓存HashCode
Java中String对象的哈希码被频繁地使用, 比如在hashMap 等容器中。
字符串不变性保证了hash码的唯一性,因此可以放心地进行缓存.这也是一种性能优化手段,意味着不必每次都去计算新的哈希码.
安全性
String被许多的Java类(库)用来当做参数,例如 网络连接地址URL,文件路径path,还有反射机制所需要的String参数等, 假若String不是固定不变的,将会引起各种安全隐患。
线程安全
因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。
总体来说, String不可变的原因包括 设计考虑,效率优化问题,以及安全性这三大方面.