引言
经过原理篇三篇文章的讲解,相信你已经对java中的字符串有了一个比较深入的理解了,从这篇文章开始,我会开始讲解String整个类层次结构的源码,我们先从String的几个重要方法开始。
类的定义
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
String实现了三个接口,分别是java.io.Serializable、Comparable
CharSequence接口
CharSequence是很多字符串类的顶层父类,包括java.lang包下的String、AbstractStringBuilder、StringBuilder和StringBuffer以及java.nio包下面的CharBuffer、DirectCharBuffer*和HeapCharBuffer等。
这个接口其实是比较简单的,只有几个我们需要关注的方法。
我们需要根据源码中的注释来大概了解一下这个接口的用途:
/**
* A <tt>CharSequence</tt> is a readable sequence of <code>char</code> values. This
* interface provides uniform, read-only access to many different kinds of
* <code>char</code> sequences.
* A <code>char</code> value represents a character in the <i>Basic
* Multilingual Plane (BMP)</i> or a surrogate. Refer to <a
* href="Character.html#unicode">Unicode Character Representation</a> for details.
*/
大概意思就是:CharSequence代表一个可读的char序列。这个接口提供了到不同char序列的统一的、只读的访问,注意是只读的访问,后面我们讲到AbstractStringBuilder抽象类时,会看到它通过实现CharSequence和Appendable,实现了对字符序列可读和可写的操作。
CharSequence有三个方法需要我们关注:
/**
* Returns the length of this character sequence. The length is the number
* of 16-bit <code>char</code>s in the sequence.
*
* @return the number of <code>char</code>s in this sequence
*/
int length();
length方法返回字符序列的长度。
char charAt(int index);
charAt方法返回指定索引位置上的字符。
CharSequence subSequence(int start, int end);
subSequence方法返回从start开始到end结束的子字符序列。
下面我们会看到链接。
字符串的比较和相等性判定
来看compareTo方法的实现:
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;
}
字符串是按照字典序来比较的。字典序的意思是:如果两个字符串不相同,那么可能是它们在某些位置上有不同的字符,也可能是它们的长度不同,或者两种情况都有。如果它们在一个或多个索引位置上的字符不同,取这些索引中的最小值为k,那么在k这个位置上字符值小的那个字符串,就是在字典序上先于另一个字符串。如果当前字符串在字典序上先于参数字符串,那么就会返回负数,也就是当前字符串小于参数字符串,如果当前字符串在字典序上比参数字符串靠后,就返回正数,也就是当前字符串大于参数字符串,如果在字典序上相同,就返回零,也就是当前字符串与参数字符串相同。
从代码我们还可以看出来,返回的值是比较到的索引(比较到某个索引位置说明之前的索引位置上,两个字符串的字符值是一样的)位置上,两个字符串的字符值(字符值实际上是一个char数字值)的差值,如果两个字符串每个合法位置上的字符值都相同,就比较长度,返回长度的差值,长度小的字符串小于长度大的字符串。
下面是几个字符串比较的示例:
System.out.println("a".compareTo("d"));
结果为-3,这两个字符串都只有一个字符,比较这两个字符值即可。
System.out.println("ad".compareTo("ac"));
输出为1,在第二个位置上,两个字符串的字符不同,返回的是d和c的差值。
System.out.println("ad".compareTo("adad"));
输出为-2,ad的长度为2,在前两个位置上,两个字符串都是一样的,就会比较长度,返回长度的差值为-2。
String类重写了equals方法,我们看一下equals方法的实现:
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;
}
只有在参数不是null并且参数是String类型并且参数的字符序列与当前对象的字符序列相同时,才会返回true。这个也符合我们对String的equals方法的认知,比较的是字符序列内容而不是对象引用。
重要方法
startsWith和endsWith方法
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;
}
startsWith方法校验当前字符串从toffset开始的子串是否以prefix开头。这个逻辑很简单,如果toffset小于零或者toffset的值大于当前字符串的长度减去参数字符串的长度(例如当前字符串长度为10,参数字符串长度为6,我肯定不能从当前字符串的第5个开始查,必须保证开始的位置之后的字符串长度不能小于参数字符串的长度)。之后,对当前字符串从tofset开始,参数字符串从零开始按顺序比较字符值,有一个不相等即返回false。
我们常用的startWith(String prefix)是调用了这个方法:
public boolean startsWith(String prefix) {
return startsWith(prefix, 0);
}
endsWith方法的实现很巧妙,直接调用的startsWith方法:
public boolean endsWith(String suffix) {
return startsWith(suffix, value.length - suffix.value.length);
}
可以思考一下,判断abdoopcdd是否以cdd结束可以转换为判断abdcdd是否从第流个字符开始是否以cdd开头,所以,只要调用startWith方法,将toffset这个参数设置为两个字符串长度的差即可。
hashCode方法
参照String不变性中的讲解
indexOf方法
/**
* Code shared by String and StringBuffer to do searches. The
* source is the character array being searched, and the target
* is the string being searched for.
*
* @param source the characters being searched.
* @param sourceOffset offset of the source string.
* @param sourceCount count of the source string.
* @param target the characters being searched for.
* @param targetOffset offset of the target string.
* @param targetCount count of the target string.
* @param fromIndex the index to begin searching from.
*/
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];
//最多需要查找到的原字符串的索引位置 如果到这个位置还没有找到 肯定就没有了
//例如sourOffset=5 souceCount=10 targetCount=3 那么最多查找到原字符串的第12个位置
//也就是保证后面至少有3个字符
int max = sourceOffset + (sourceCount - targetCount);
for (int i = sourceOffset + fromIndex; i <= max; i++) {
/* Look for first character. */
//先查找第一个字符 在查找的过程中i需要增加
if (source[i] != first) {
while (++i <= max && source[i] != first);
}
/* Found first character, now look at the rest of v2 */
if (i <= max) {
//找到了第一个字符 如果从这个字符开始的数量为targetCount的
//每个字符都与target对应相等 说明找到整个符合的字符串
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;
}
这个方法是静态方法,会被String本身和StringBuffer使用。
String的contains方法实际上是调用了indexOf方法,只要indexOf方法返回值不是-1,就证明包含参数字符串。
join方法
public static String join(CharSequence delimiter, CharSequence... elements) {
Objects.requireNonNull(delimiter);
Objects.requireNonNull(elements);
// Number of elements not likely worth Arrays.stream overhead.
StringJoiner joiner = new StringJoiner(delimiter);
for (CharSequence cs: elements) {
joiner.add(cs);
}
return joiner.toString();
}
join方法将elements里面的每个字符串以delimiter为分隔符连接,例如
String message = String.join("-", "Java", "is", "cool");
返回的message是”Java-is-cool”。它是通过StringJoiner来实现的,StringJoiner的分析可以参考这篇文章,这里不再赘述。
还有另外一个join方法,可以对一个可迭代的容器中的元素进行这样的操作:
public static String join(CharSequence delimiter,
Iterable<? extends CharSequence> elements) {
Objects.requireNonNull(delimiter);
Objects.requireNonNull(elements);
StringJoiner joiner = new StringJoiner(delimiter);
for (CharSequence cs: elements) {
joiner.add(cs);
}
return joiner.toString();
}
例如,可以这样使用:
List<String> strings = new LinkedList<>();
strings.add("Java");
strings.add("is");
strings.add("cool");
String message = String.join(" ", strings);
所以我们可以用join方法进行字符串的拼接,通过StringJoiner的源码,我们应该知道它是使用StringBuilder来进行字符串拼接的,不用每次都创建新的字符串对象。
String还提供了另外一个进行字符串拼接的方法:
concat方法
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方法实现拼接会创建新的字符串,所以我们循环调用concat方法会创建很多的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;
}
trim方法去掉一个字符串的开头和结尾的空格。
我举几个trim的例子:
(1)如果当前字符串是一个空字符串,或者当前字符串的第一个和最后一个字符的值都大于” “的值也就是说没有开头和结尾的空字符串,就返回当前字符串。
public static void main(String[] args) {
String emptyString = "";
System.out.println(emptyString.trim()==emptyString);
String notToTrim = new String("a d d d");
String trim = notToTrim.trim();
System.out.println(trim);
System.out.println(notToTrim==trim);
}
输出结果为:
true
a d d d
true
说明返回的就是当前字符串。
(2)如果当前字符串的每个字符的值都大于” “的值,也就是说整个字符串都是空格没有其他字符,就返回空字符串。
public static void main(String[] args) {
String emptyString = " ";
System.out.println(emptyString.trim()+"end");
}
为了便于观察,我在最后输出了end,最后输出的就是end。
(3)否则,取字符串中第一个不为” “的字符索引为k,最后一个不为” “的字符的索引为m,返回当前字符串的(k
,m+1)之间的子串,通过调用this.substring(k,m+1)来实现;
public static void main(String[] args) {
String emptyString = " a dd d d ";
System.out.println(emptyString.trim()+"end");
}
输出的是:starta dd d dend
可以看到开头和结尾的连续空格都被去掉了,中间的没有去掉。
CharSequence接口方法的实现
String作为CharSequence接口的实现类,对接口中的抽象方法进行了实现:
public int length() {
return value.length;
}
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
public CharSequence subSequence(int beginIndex, int endIndex) {
return this.substring(beginIndex, endIndex);
}
都比较简单,就不再一一介绍了。