Java字符串


Java字符串

​ 在平时开发的过程中,字符串操作应该是最常见的行为。而在Java中,String类大概是我们使用的最频繁的一个类了。今天我们就来初步研究下String的实现。说起看源码,就本能的感到一种对高阶程序员的一种畏惧,但当你打开源码的时候你会发现,String类3000多行代码有很多都是注释,真正的代码没有想像的多,这个也侧面说明了注释在开发当中的重要性。

String类声明和属性

1
2
3
4
5
6
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
private int hash; // Default to 0
private static final long serialVersionUID = -6849794470754667710L;
private static final ObjectStreamField[] serialPersistentFields =new ObjectStreamField[0];

​ 我们可以看到String类首先是final修饰的,所以不允许被继承和修改。其次String类实现了Serializable、Comparable、CharSequence三个接口;Serializable接口使String可序列化,Comparable接口使String类能互相比较,CharSequence接口提供了length()、charAt(int index)、subSequence(int start,int end)方法。

​ 接下来是String类的四个属性,一个不可变的char数组用来存放字符串,一个int型的hash存放哈希值,serialVersionUID提供序列化ID,serialPersistentFields声明了一个可序列化的字段。从这里我们可以看出,String是对char数组的封装。

String构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
//无参构造方法
public String() {
this.value = "".value;
}

//用一个String对象初始化新建String对象
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}

//用一个char数组初始化String对象
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}

//用char数组的一部分初始化String对象
public String(char value[], int offset, int count) {//offset起始点,count长度
if (offset < 0) {//起始点小于0,抛出越界异常
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {//长度小于0,抛出越界异常
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {//count==0,起始点小于等于char数组长度,String为空
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {//起始点大于char数组长度减count,越界异常
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}

//Unicode码数组的一部分初始化String对象
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))//是否为BMP代码点(基于多语言面),U+0000~U+FFFF
continue;
else if (Character.isValidCodePoint(c))//是否为合法Unicode代码点,U+10000~U+10FFFF
n++;//遇到增补字符长度加一
else throw new IllegalArgumentException(Integer.toString(c));//非法参数异常
}

// Pass 2: Allocate and fill in char[]
final char[] v = new char[n];//用上面计算的长度来新建char数组

for (int i = offset, j = 0; i < end; i++, j++) {
int c = codePoints[i];
if (Character.isBmpCodePoint(c))
v[j] = (char)c;//BMP代码点直接转换成char
else
Character.toSurrogates(c, v, j++);//增补字符转换成两个char
}

this.value = v;
}

//用一个字节数组初始化String对象,offset指定开始偏移量,length指定bytes长度,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);
}

//这个构造方法跟上面那个几乎一样,除了指定编码方式用的Charset对象
public String(byte bytes[], int offset, int length, Charset charset) {
if (charset == null)
throw new NullPointerException("charset");
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(charset, bytes, offset, length);
}

//同上,将整个字节数组转化成String对象
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[], int offset, int length) {
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(bytes, offset, length);
}

//同上
public String(byte bytes[]) {
this(bytes, 0, bytes.length);
}

//将StringBuffer转成String对象
public String(StringBuffer buffer) {
synchronized(buffer) {//同步锁
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}

//将StringBuilder转成String对象
//StringBuffer和StringBuilder都继承自抽象类AbstractStringBuilder,它有个叫value的字符数组,这两个构造方法就是把这个字符数组转成String对象。
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}

每个构造方法都写了注释,在此就不赘述了。

String类常用方法

boolean equals(Object anObject)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public boolean equals(Object anObject) {
if (this == anObject) {//如果引用的是同一个对象,返回真
return true;
}
if (anObject instanceof String) {//如果不是String类型的数据,返回假
String anotherString = (String) anObject;
int n = value.length;
if (n == anotherString.value.length) {//如果char数组长度不相等,返回假
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对象中的value是否相等,判断流程如下:

  1. 判断引用是否相同。如果是,则为同一个String对象,肯定相等,若不同继续判断。
  2. 判断比较对象是否是String对象,若不是,返回false, 若是,则继续判断。
  3. 判断两个String对象维护的char数组长度是否相等,不想等则返回false,相等则继续判断。
  4. 从后往前单个字符比较,若都相等,则两个String相等,否则不想等。

int compareTo(String anotherString)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public int compareTo(String anotherString) {
int len1 = value.length;//自身对象字符串长度len1
int len2 = anotherString.value.length;//被比较对象字符串长度len2
int lim = Math.min(len1, len2);//取两个字符串长度的最小值lim
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
//从value的第一个字符开始到最小长度lim处为止,如果字符不相等,返回自身(对象不相等处字符减被比较对象不相等字符)
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
//如果前面都相等,则返回(自身长度-被比较对象长度)
return len1 - len2;
}

这个方法利用字符相减和长度相减巧妙的判断了三种情况。

int hashCode()

1
2
3
4
5
6
7
8
9
10
11
12
13
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {//如果hash没有被计算过,并且字符串不为空,则进行hashCode计算
char val[] = value;
//计算过程
//val[0]*31^(n-1) + val[1]*31^(n-2) + ... + val[n-1]
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;//hash赋值
}
return h;
}

String类重写了hashCode方法,采用了多项式计算得到hash值,但是两个hash相同的String也有可能不想等,这里采用这种方式只是为了尽可能减少碰撞,提高HashMap等的效率。

startsWith()/endsWith()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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);
}

public boolean endsWith(String suffix) {
return startsWith(suffix, value.length - suffix.value.length);
}

这几个方法通常用于判断一个字符串的开头或者结尾是否是特定格式的,比如找到一个doc文件或者判断url的协议是不是http。

String concat(String str)

1
2
3
4
5
6
7
8
9
10
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);
}

将一个字符串添加到当前字符串后,通过判断str时候为空串来决定时候新建对象。

String replace(char oldChar,char newChar)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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是替换字符,replace(String oldStr,String newStr)方法采用的正则表达式。

String trim()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

去掉字符串前后的空格。

String intern()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java&trade; Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();

intern()是一个native方法,上面给出了Jdk注释,大概意思就是如果常量池里面有当前字符串,返回该字符串的引用;没有的话就把当前字符串放进常量池,再返回该字符串的引用。关于intern()这个方法个人感觉用起来要慎重,因为一旦常量池里面的字符串达到一定规模之后性能会下降不少。由于没有接触过大规模数据的操作,等有机会接触了再详细说一说intern()。

总结

​ 从上文我们可以看出,String对象是不可变的。String类中每一个看起来会修改String值发方法,实际上都是创建了一个全新的String对象,以包含修改后的字符串内容,而最初的String对象则纹丝不动。


文章作者: Amos Liu
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Amos Liu !
 上一篇
编码和解码 编码和解码
编码和解码​ 平时开发的时候经常会遇到字符串编码和解码的问题,今天就初步探究下乱码问题,寻求解决乱码的方法。 概念首先来分析下Java中的编码问题,文本在存储设备中是以字节的方式存储的,以特定的编码方案讲编码映射到字节数组。从存储设备
2017-04-24
下一篇 
算法练习2 算法练习2
算法练习2Median of Two Sorted ArraysThere are two sorted arrays nums1 and nums2 of size m and n respectively. Find the media
2017-04-15
  目录