源码阅读-java基础-java.lang.String
1、概述
String这个类可以说是我们使用最为频繁的类之一了,它在java的数据类型里面有很重要的地位,今天我们来看看它的源码,以及分析一下它的不可变性。
2、概念解释
2.1、底层结构
以下是String的部分源码。我们可以看到它是一个用final修饰的常量类,不能被任何类所继承,而且一旦一个String对象被创建, 包含在这个对象中的字符序列是不可改变的, 包括该类后续的所有方法都是不能修改该对象的,直至该对象被销毁,这是我们需要特别注意的(该类的一些方法看似改变了字符串,其实内部都是创建一个新的字符串)。接着实现了 Serializable接口,这是一个序列化标志接口,还实现了 Comparable 接口,用于比较两个字符串的大小(按顺序比较单个字符的ASCII码),最后实现了 CharSequence 接口,表示是一个有序字符的集合。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
private int hash;
private static final long serialVersionUID = -6849794470754667710L;
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
......
}
2.2、CharSequence
2.3、几个数组复制方法
阅读String的源码,你会看到
System.arraycopy、Arrays.copyOf、Arrays.copyOfRange
这几个方法经常出现,这三个方法操作value(即 存储String的字符数组),所以我们有必要看看这三个方法是干嘛的。这三个方法都是复制数组用的,只是前者是基础方法;而后两者是稍微高级的方法,底层调用了System.arraycopy方法。
System.arraycopy方法是一个静态的,本地方法,所以我们一般是看不到它的实现过程的。虽然它的参数是Object,但是java的数组没有一个比较抽象的公共的“父类”或者“接口”,所以这里的Object src,Object dest可以理解为数组,但是呢,我是没想通,既然这个方法是用于数组的,为啥不写成Object[] src,Object[] dest呢,这样意思不是更加明显吗?有知道的同学可以在评论区留言一下。
/**
* src:源数组
* srcPos:源数组的起始索引
* dest:目标数组
* destPos:目标数组的起始索引
* length:复制的长度
*/
public static native void arraycopy(Object src,int srcPos,Object dest,int destPos,int length);
2.4、String类型真的不可变吗
3、常用方法
3.1、int length():获取长度
返回String的长度,它是
java.lang.CharSequence
接口定义的方法,只不过String对其进行了重写,源码里面是没有@Override
注解的,我为了看得清楚,增加了这个注解。我们看到它返回的实际上就是字符数组的长度。
@Override
public int length() {
return value.length;
}
3.2、char charAt(int index):返回数组某个索引处的的字符
如果索引超限,则抛出索引越界异常,底层操作的还是字符数组。它也是
java.lang.CharSequence
的方法。
@Override
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
3.3、boolean isEmpty():字符串是否为空
我们看到操作的还是value数组。
public boolean isEmpty() {
return value.length == 0;
}
3.4、boolean equals(Object anObject):比较内容是否一样
我们看到,首先比较了指针是否指向同一个地址,然后比较了类型,最后比较了内容(在比较内容的时候,为了减少比较的次数,使用了i++,n--的思想)。整体的思想值得借鉴。另外,这里即便参数为null,程序也能正常运行。
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;
}
3.5、int compareTo(String anotherString)
这是实现Comparable接口必须要实现的方法。
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;
}
3.6、int hashCode():获取哈希值
这里看到String的hashCode()实现还是很简单的,真正的代码就三行,但是有个奇怪的数字——31,为啥是31呢?
- 31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一。选择数字31是因为它是一个奇质数,如果选择一个偶数会在乘法运算中产生溢出,导致数值信息丢失,因为乘二相当于移位运算。选择质数的优势并不是特别的明显,但这是一个传统。同时,数字31有一个很好的特性,即乘法运算可以被移位和减法运算取代,来获取更好的性能。
- 31可以被 JVM 优化,
31 * i = (i << 5) - i
。
具体的可以看看这篇博文:为什么 String hashCode 方法选择数字31作为乘子,我感觉这位大佬的这篇博文的质量非常高,是带着学术的精神去研究这个问题,值得膜拜!
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;
}
3.7、String substring:字符串截取
字符串的截取。其中方法的参数是“起始索引”,截取的长度是
value.length - beginIndex
,如果起始索引超限,则抛出索引越界异常!但是呢,值得注意的是,String的几乎所有方法,都不会操作原来的value数组,而是复制一份出来,再进行操作。
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 substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
3.8、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);
}
3.9、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;
}
3.10、boolean contains(CharSequense s):包含
public boolean contains(CharSequence s) {
return indexOf(s.toString()) > -1;
}
3.11、String 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;
}
3.12、String toString():经常用,就是返回字符串本身
public String toString() {
return this;
}
3.13、boolean startsWith():是否以某个字符串开头
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;
}
public boolean startsWith(String prefix) {
return startsWith(prefix, 0);
}
3.14、boolean endsWith():是否以某个字符串结尾
public boolean endsWith(String suffix) {
return startsWith(suffix, value.length - suffix.value.length);
}
4、构造方法
public String() {
this.value = "".value;
}
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
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);
}
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;
}
public String(byte bytes[], String charsetName)
throws UnsupportedEncodingException {
this(bytes, 0, bytes.length, charsetName);
}
public String(byte bytes[], Charset charset) {
this(bytes, 0, bytes.length, charset);
}
public String(byte bytes[]) {
this(bytes, 0, bytes.length);
}
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());
}
String(char[] value, boolean share) {
// assert share : "unshared not supported";
this.value = value;
}
5、本地方法
5.1、String intern()
6、其他方法
6.1、indexOf系列
//todo
6.2、valueOf系列
//todo
7、未解决问题
- System.arraycopy(Object src,int srcPos,Object dest,int destPos,int length),既然是Jdk提供的一个只用于数组复制的方法,为啥表示数组的参数类型是Object,表示成Object[]不是更一目了然吗?