引言

经过原理篇三篇文章的讲解,相信你已经对java中的字符串有了一个比较深入的理解了,从这篇文章开始,我会开始讲解String整个类层次结构的源码,我们先从String的几个重要方法开始。

类的定义

  1. public final class String
  2. implements java.io.Serializable, Comparable<String>, CharSequence {

String实现了三个接口,分别是java.io.Serializable、Comparable和CharSequence。Serializable意味着String对象可以被序列化,Comparable接口意味着String对象之间有固定的比较方式,这个需要实现compareTo方法,CharSequence是java.lang包中的一个接口,由于CharSequence接口是很多跟字符串相关类的顶层接口,我们这里先介绍一下这个接口。

CharSequence接口

CharSequence是很多字符串类的顶层父类,包括java.lang包下的String、AbstractStringBuilder、StringBuilder和StringBuffer以及java.nio包下面的CharBuffer、DirectCharBuffer*和HeapCharBuffer等。
这个接口其实是比较简单的,只有几个我们需要关注的方法。
我们需要根据源码中的注释来大概了解一下这个接口的用途:

  1. /**
  2. * A <tt>CharSequence</tt> is a readable sequence of <code>char</code> values. This
  3. * interface provides uniform, read-only access to many different kinds of
  4. * <code>char</code> sequences.
  5. * A <code>char</code> value represents a character in the <i>Basic
  6. * Multilingual Plane (BMP)</i> or a surrogate. Refer to <a
  7. * href="Character.html#unicode">Unicode Character Representation</a> for details.
  8. */

大概意思就是:CharSequence代表一个可读的char序列。这个接口提供了到不同char序列的统一的、只读的访问,注意是只读的访问,后面我们讲到AbstractStringBuilder抽象类时,会看到它通过实现CharSequence和Appendable,实现了对字符序列可读和可写的操作。
CharSequence有三个方法需要我们关注:

  1. /**
  2. * Returns the length of this character sequence. The length is the number
  3. * of 16-bit <code>char</code>s in the sequence.
  4. *
  5. * @return the number of <code>char</code>s in this sequence
  6. */
  7. int length();

length方法返回字符序列的长度。

  1. char charAt(int index);

charAt方法返回指定索引位置上的字符。

  1. CharSequence subSequence(int start, int end);

subSequence方法返回从start开始到end结束的子字符序列。
下面我们会看到链接

字符串的比较和相等性判定

来看compareTo方法的实现:

  1. public int compareTo(String anotherString) {
  2. int len1 = value.length;
  3. int len2 = anotherString.value.length;
  4. int lim = Math.min(len1, len2);
  5. char v1[] = value;
  6. char v2[] = anotherString.value;
  7. int k = 0;
  8. while (k < lim) {
  9. char c1 = v1[k];
  10. char c2 = v2[k];
  11. if (c1 != c2) {
  12. return c1 - c2;
  13. }
  14. k++;
  15. }
  16. return len1 - len2;
  17. }

字符串是按照字典序来比较的。字典序的意思是:如果两个字符串不相同,那么可能是它们在某些位置上有不同的字符,也可能是它们的长度不同,或者两种情况都有。如果它们在一个或多个索引位置上的字符不同,取这些索引中的最小值为k,那么在k这个位置上字符值小的那个字符串,就是在字典序上先于另一个字符串。如果当前字符串在字典序上先于参数字符串,那么就会返回负数,也就是当前字符串小于参数字符串,如果当前字符串在字典序上比参数字符串靠后,就返回正数,也就是当前字符串大于参数字符串,如果在字典序上相同,就返回零,也就是当前字符串与参数字符串相同。
从代码我们还可以看出来,返回的值是比较到的索引(比较到某个索引位置说明之前的索引位置上,两个字符串的字符值是一样的)位置上,两个字符串的字符值(字符值实际上是一个char数字值)的差值,如果两个字符串每个合法位置上的字符值都相同,就比较长度,返回长度的差值,长度小的字符串小于长度大的字符串。
下面是几个字符串比较的示例:

  1. System.out.println("a".compareTo("d"));

结果为-3,这两个字符串都只有一个字符,比较这两个字符值即可。

  1. System.out.println("ad".compareTo("ac"));

输出为1,在第二个位置上,两个字符串的字符不同,返回的是d和c的差值。

  1. System.out.println("ad".compareTo("adad"));

输出为-2,ad的长度为2,在前两个位置上,两个字符串都是一样的,就会比较长度,返回长度的差值为-2。
String类重写了equals方法,我们看一下equals方法的实现:

  1. public boolean equals(Object anObject) {
  2. if (this == anObject) {
  3. return true;
  4. }
  5. if (anObject instanceof String) {
  6. String anotherString = (String)anObject;
  7. int n = value.length;
  8. if (n == anotherString.value.length) {
  9. char v1[] = value;
  10. char v2[] = anotherString.value;
  11. int i = 0;
  12. while (n-- != 0) {
  13. if (v1[i] != v2[i])
  14. return false;
  15. i++;
  16. }
  17. return true;
  18. }
  19. }
  20. return false;
  21. }

只有在参数不是null并且参数是String类型并且参数的字符序列与当前对象的字符序列相同时,才会返回true。这个也符合我们对String的equals方法的认知,比较的是字符序列内容而不是对象引用。

重要方法

startsWith和endsWith方法

  1. public boolean startsWith(String prefix, int toffset) {
  2. char ta[] = value;
  3. int to = toffset;
  4. char pa[] = prefix.value;
  5. int po = 0;
  6. int pc = prefix.value.length;
  7. // Note: toffset might be near -1>>>1.
  8. if ((toffset < 0) || (toffset > value.length - pc)) {
  9. return false;
  10. }
  11. while (--pc >= 0) {
  12. if (ta[to++] != pa[po++]) {
  13. return false;
  14. }
  15. }
  16. return true;
  17. }

startsWith方法校验当前字符串从toffset开始的子串是否以prefix开头。这个逻辑很简单,如果toffset小于零或者toffset的值大于当前字符串的长度减去参数字符串的长度(例如当前字符串长度为10,参数字符串长度为6,我肯定不能从当前字符串的第5个开始查,必须保证开始的位置之后的字符串长度不能小于参数字符串的长度)。之后,对当前字符串从tofset开始,参数字符串从零开始按顺序比较字符值,有一个不相等即返回false。
我们常用的startWith(String prefix)是调用了这个方法:

  1. public boolean startsWith(String prefix) {
  2. return startsWith(prefix, 0);
  3. }

endsWith方法的实现很巧妙,直接调用的startsWith方法:

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

可以思考一下,判断abdoopcdd是否以cdd结束可以转换为判断abdcdd是否从第流个字符开始是否以cdd开头,所以,只要调用startWith方法,将toffset这个参数设置为两个字符串长度的差即可。

hashCode方法

参照String不变性中的讲解

indexOf方法

  1. /**
  2. * Code shared by String and StringBuffer to do searches. The
  3. * source is the character array being searched, and the target
  4. * is the string being searched for.
  5. *
  6. * @param source the characters being searched.
  7. * @param sourceOffset offset of the source string.
  8. * @param sourceCount count of the source string.
  9. * @param target the characters being searched for.
  10. * @param targetOffset offset of the target string.
  11. * @param targetCount count of the target string.
  12. * @param fromIndex the index to begin searching from.
  13. */
  14. static int indexOf(char[] source, int sourceOffset, int sourceCount,
  15. char[] target, int targetOffset, int targetCount,
  16. int fromIndex) {
  17. if (fromIndex >= sourceCount) {
  18. return (targetCount == 0 ? sourceCount : -1);
  19. }
  20. if (fromIndex < 0) {
  21. fromIndex = 0;
  22. }
  23. if (targetCount == 0) {
  24. return fromIndex;
  25. }
  26. //记录要查找的第一个字符
  27. char first = target[targetOffset];
  28. //最多需要查找到的原字符串的索引位置 如果到这个位置还没有找到 肯定就没有了
  29. //例如sourOffset=5 souceCount=10 targetCount=3 那么最多查找到原字符串的第12个位置
  30. //也就是保证后面至少有3个字符
  31. int max = sourceOffset + (sourceCount - targetCount);
  32. for (int i = sourceOffset + fromIndex; i <= max; i++) {
  33. /* Look for first character. */
  34. //先查找第一个字符 在查找的过程中i需要增加
  35. if (source[i] != first) {
  36. while (++i <= max && source[i] != first);
  37. }
  38. /* Found first character, now look at the rest of v2 */
  39. if (i <= max) {
  40. //找到了第一个字符 如果从这个字符开始的数量为targetCount的
  41. //每个字符都与target对应相等 说明找到整个符合的字符串
  42. int j = i + 1;
  43. int end = j + targetCount - 1;
  44. for (int k = targetOffset + 1; j < end && source[j]
  45. == target[k]; j++, k++);
  46. if (j == end) {
  47. /* Found whole string. */
  48. return i - sourceOffset;
  49. }
  50. }
  51. }
  52. return -1;
  53. }

这个方法是静态方法,会被String本身和StringBuffer使用。
String的contains方法实际上是调用了indexOf方法,只要indexOf方法返回值不是-1,就证明包含参数字符串。

join方法

  1. public static String join(CharSequence delimiter, CharSequence... elements) {
  2. Objects.requireNonNull(delimiter);
  3. Objects.requireNonNull(elements);
  4. // Number of elements not likely worth Arrays.stream overhead.
  5. StringJoiner joiner = new StringJoiner(delimiter);
  6. for (CharSequence cs: elements) {
  7. joiner.add(cs);
  8. }
  9. return joiner.toString();
  10. }

join方法将elements里面的每个字符串以delimiter为分隔符连接,例如

  1. String message = String.join("-", "Java", "is", "cool");

返回的message是”Java-is-cool”。它是通过StringJoiner来实现的,StringJoiner的分析可以参考这篇文章,这里不再赘述。
还有另外一个join方法,可以对一个可迭代的容器中的元素进行这样的操作:

  1. public static String join(CharSequence delimiter,
  2. Iterable<? extends CharSequence> elements) {
  3. Objects.requireNonNull(delimiter);
  4. Objects.requireNonNull(elements);
  5. StringJoiner joiner = new StringJoiner(delimiter);
  6. for (CharSequence cs: elements) {
  7. joiner.add(cs);
  8. }
  9. return joiner.toString();
  10. }

例如,可以这样使用:

  1. List<String> strings = new LinkedList<>();
  2. strings.add("Java");
  3. strings.add("is");
  4. strings.add("cool");
  5. String message = String.join(" ", strings);

所以我们可以用join方法进行字符串的拼接,通过StringJoiner的源码,我们应该知道它是使用StringBuilder来进行字符串拼接的,不用每次都创建新的字符串对象。
String还提供了另外一个进行字符串拼接的方法:

concat方法

  1. public String concat(String str) {
  2. int otherLen = str.length();
  3. if (otherLen == 0) {
  4. return this;
  5. }
  6. int len = value.length;
  7. char buf[] = Arrays.copyOf(value, len + otherLen);
  8. str.getChars(buf, len);
  9. return new String(buf, true);
  10. }

从代码可以看出来,concat方法实现拼接会创建新的字符串,所以我们循环调用concat方法会创建很多的String对象。

trim方法

  1. public String trim() {
  2. int len = value.length;
  3. int st = 0;
  4. char[] val = value; /* avoid getfield opcode */
  5. while ((st < len) && (val[st] <= ' ')) {
  6. st++;
  7. }
  8. while ((st < len) && (val[len - 1] <= ' ')) {
  9. len--;
  10. }
  11. return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
  12. }

trim方法去掉一个字符串的开头和结尾的空格。
我举几个trim的例子:
(1)如果当前字符串是一个空字符串,或者当前字符串的第一个和最后一个字符的值都大于” “的值也就是说没有开头和结尾的空字符串,就返回当前字符串。

  1. public static void main(String[] args) {
  2. String emptyString = "";
  3. System.out.println(emptyString.trim()==emptyString);
  4. String notToTrim = new String("a d d d");
  5. String trim = notToTrim.trim();
  6. System.out.println(trim);
  7. System.out.println(notToTrim==trim);
  8. }

输出结果为:

  1. true
  2. a d d d
  3. true

说明返回的就是当前字符串。
(2)如果当前字符串的每个字符的值都大于” “的值,也就是说整个字符串都是空格没有其他字符,就返回空字符串。

  1. public static void main(String[] args) {
  2. String emptyString = " ";
  3. System.out.println(emptyString.trim()+"end");
  4. }

为了便于观察,我在最后输出了end,最后输出的就是end。
(3)否则,取字符串中第一个不为” “的字符索引为k,最后一个不为” “的字符的索引为m,返回当前字符串的(k
,m+1)之间的子串,通过调用this.substring(k,m+1)来实现;

  1. public static void main(String[] args) {
  2. String emptyString = " a dd d d ";
  3. System.out.println(emptyString.trim()+"end");
  4. }

输出的是:starta dd d dend
可以看到开头和结尾的连续空格都被去掉了,中间的没有去掉。

CharSequence接口方法的实现

String作为CharSequence接口的实现类,对接口中的抽象方法进行了实现:

  1. public int length() {
  2. return value.length;
  3. }
  1. public char charAt(int index) {
  2. if ((index < 0) || (index >= value.length)) {
  3. throw new StringIndexOutOfBoundsException(index);
  4. }
  5. return value[index];
  6. }
  1. public CharSequence subSequence(int beginIndex, int endIndex) {
  2. return this.substring(beginIndex, endIndex);
  3. }

都比较简单,就不再一一介绍了。