关于string:初识String源码

50次阅读

共计 6573 个字符,预计需要花费 17 分钟才能阅读完成。

前言

最近打算开始来读一下 JDK 的局部源码,这次先从咱们平时用的最多的 String 类 (JDK1.8) 开始,本文次要会对以下几个办法的源码进行剖析:

  • equals
  • hashCode
  • equalsIgnoreCase
  • indexOf
  • startsWith
  • concat
  • substring
  • split
  • trim
  • compareTo

如果有不对的中央请多多指教,那么开始进入注释。

源码分析

首先看下 String 类实现了哪些接口

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

这个序列化接口没有任何办法和域,仅用于标识序列化的语意。

  • Comparable<String>

这个接口只有一个 compareTo(T 0)接口,用于对两个实例化对象比拟大小。

  • CharSequence

这个接口是一个只读的字符序列。包含 length(), charAt(int index), subSequence(int start, int end)这几个 API 接口,值得一提的是,StringBuffer 和 StringBuild 也是实现了该接口。

看一下两个次要变量:

/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0

能够看到,value[]是存储 String 的内容的,即当应用 String str = “abc”; 的时候,实质上,”abc” 是存储在一个 char 类型的数组中的。

hash 是 String 实例化的 hashcode 的一个缓存。因为 String 常常被用于比拟,比方在 HashMap 中。如果每次进行比拟都从新计算 hashcode 的值的话,那无疑是比拟麻烦的,而保留一个 hashcode 的缓存无疑能优化这样的操作。

留神:这边有一个须要留神的点就是能够看到 value 数组是用 final 润饰的,也就是说不能再去指向其它的数组,然而数组的内容是能够扭转的,之所以说 String 不可变是因为其提供的 API(比方 replace 等办法)都会给咱们返回一个新的 String 对象,并且咱们无奈去扭转数组的内容,这才是它不可变的起因。

equals

equals() 办法用于判断 Number 对象与办法的参数进是否相等

String 类重写了父类 Object 的 equals 办法,来看看源码实现:

  1. 首先会判断两个对象是否指向同一个地址,如果是的话则是同一个对象,间接返回 true
  2. 接着会应用 instanceof 判断指标对象是否是 String 类型或其子类的实例,如果不是的话则返回 false
  3. 接着会比拟两个 String 对象的 char 数组长度是否统一,如果不统一则返回 false
  4. 最初迭代顺次比拟两个 char 数组是否相等

hashCode

hashCode() 办法用于返回字符串的哈希码

Hash 算法就是一种将任意长度的消息压缩到某一固定长度的音讯摘要的函数。在 Java 中,所有的对象都有一个 int hashCode() 办法,用于返回 hash 码。

依据官网文档的定义:Object.hashCode() 函数用于这个函数用于将 一个对象转换为其十六进制的地址。依据定义,如果 2 个对象雷同,则其 hash 码也应该雷同。如果重写了 equals() 办法,则原 hashCode() 办法也一并生效,所以也必须重写 hashCode() 办法。

依照下面源码举例说明:

String msg = "abcd"; 
System.out.println(msg.hashCode());

此时 value = {‘a’,’b’,’c’,’d’}  因而 for 循环会执行 4 次

第一次:h = 31*0 + a = 97 
第二次:h = 31*97 + b = 3105 
第三次:h = 31*3105 + c = 96354 
第四次:h = 31*96354 + d = 2987074 

由以上代码计算能够算出 msg 的 hashcode = 2987074

在源码的 hashcode 的正文中还提供了一个多项式计算形式:

s[0]31^(n-1) + s[1]31^(n-2) + … + s[n-1]

另外,咱们能够看到,计算中应用了 31 这个质数作为权进行计算。能够尽可能保障数据分布更扩散

在《Effective Java》中有提及:

之所以抉择 31,是因为它是一个奇素数。如果乘数是偶数,并且乘法溢出的话,信息就会失落,因为与 2 相乘等价于移位运算。应用素数的益处并不显著,然而习惯上都应用素数来计算散列后果。31 有个很好的个性。即用移位和减法来代替乘法,能够失去更好的性能:31 * i == (i << 5) – i。古代的 VM 能够主动实现这种优化。

/** Cache the hash code for the string */
private int hash; // Default to 0

而且如下面所示,当计算完之后会用一个变量 hash 把哈希值保存起来,下一次再获取的时候就不必换从新计算了,正是因为String 的不可变性保障了 hash 值的惟一。

equalsIgnoreCase

equalsIgnoreCase() 办法用于将字符串与指定的对象比拟,不思考大小写

接下来来看看源码实现:

来看看外围办法

置信看了上图的介绍就能看懂了,这里就不多说了。

indexOf

查找指定字符或字符串在字符串中第一次呈现中央的索引,未找到的状况返回 -1

String str = "wugui";
System.out.println(str.indexOf("g"));

输入后果:2

public int indexOf(String str) {return indexOf(str, 0);
}

public int indexOf(String str, int fromIndex) {return indexOf(value, 0, value.length,str.value, 0, str.value.length, fromIndex);
}

接下来是咱们的外围办法,先看下各个参数的介绍

/*
 * @param   source       被搜寻的字符
 * @param   sourceOffset 原字符串偏移量
 * @param   sourceCount  原字符串大小
 * @param   target       要搜寻的字符
 * @param   targetOffset 指标字符串偏移量
 * @param   targetCount  指标字符串大小
 * @param   fromIndex    开始搜寻的地位
*/
static int indexOf(char[] source, int sourceOffset, int sourceCount,
        char[] target, int targetOffset, int targetCount,
        int fromIndex) {......}

上面是代码的逻辑步骤

indexOf 的源码外面我认为 边界条件 是写的比拟好的

咱们这里假如

String str = "wugui";
str.indexOf("ug");

在上图第 2 步,计算出 max 作为上面循环的边界条件

// 找到第一个匹配的字符索引
if (source[i] != first) {while (++i <= max && source[i] != first);
}

咱们计算出 max=3,也就是说咱们在应用迭代搜寻第一个字符的时候只须要遍历到索引为 3 的地位,就能够了,因为索引第 4 位也就是最初一位 'i',就是匹配到了第一个字符也是无意义的,因为咱们要搜寻的指标自字符是 2 位字符,同第 5 步计算出 end 作为边界条件也是同样的情理。

有了 indexOf 办法之后,那有些办法就能够借用它来实现了,比方 contains 办法,源码如下:

public boolean contains(CharSequence s) {return indexOf(s.toString()) > -1;
}

只须要调用依据 indexOf 的返回值来判断是否蕴含指标字符串就能够了。

startsWith

startsWith() 办法用于查看字符串是否是以指定子字符串结尾,如果是则返回 True,否则返回 False

String str = "wugui";
System.out.println(str.startsWith("wu"));

输入后果:true

public boolean startsWith(String prefix) {return startsWith(prefix, 0);
}

public boolean startsWith(String prefix, int toffset) {......}

既然有了 startsWith 办法,那么 endsWith 就很容易实现了,如下:

只有批改一下参数,设置偏移量就能够了。

concat

用于将指定的字符串参数连贯到字符串上

String str1 = "wu";
String str2 = "gui";
System.out.println(str1.concat(str2));

输入后果:wugui

能够看到是应用了 Arrays.copyOf 办法来生成新数组

char buf[] = Arrays.copyOf(value, len + otherLen);

咱们来看看其实现:

能够看到次要应用 system.arraycopy 办法,点进去看一下实现:

如果看不到的话咱们这里举个例子:

比方:咱们有一个数组数据

byte[] srcBytes =  new byte[]{2,4,0,0,0,0,0,10,15,50};// 原数组
byte[] destBytes = new byte[5]; // 指标数组

咱们应用 System.arraycopy 进行复制

System.arrayCopy(srcBytes,0,destBytes ,0,5)

下面这段代码就是 : 创立一个一维空数组, 数组的总长度为 12 位, 而后将 srcBytes 源数组中 从 0 位 到 第 5 位之间的数值 copy 到 destBytes 指标数组中, 在指标数组的第 0 位开始搁置,
那么这行代码的运行成果应该是 2,4,0,0,0,

调用完 Arrays.copy 返回新数组办法后,会调用 str.getChars(buf, len)来拼接字符串,咱们看下其实现:

能够看到其实也是调用了 System.arraycopy 来实现,这里不再细说。

最初一步就是把新数组赋值给value

return new String(buf, true);

substring

提取字符串中介于两个指定下标之间的字符

String str = "wugui";
System.out.println(str.substring(1, 3));// 包含索引 1 不包含索引 3 

输入后果:ug

来看看 new String(value, beginIndex, subLen) 的实现

看看 Arrays.copyOfRange 是如何实现的:

能够看到其实还是应用的 System.arraycopy 来实现,下面曾经介绍过了,这里不再细说。

split

依据匹配给定的正则表达式来拆分字符串

先来看看用法:

public String[] split(String regex, int limit)

第一个参数 regex 示意正则表达式,第二个参数 limit 是宰割的子字符串个数

String str = "a:b:c:d";
String[] split = str.split(":");

当没有传 limit 参数默认调用的是split(String regex, 0)

下面的输入为:[a, b, c, d]

如果把 limit 参数换成 2 那么输入后果变成:[a, b:c:d],能够看出 limit 意味着宰割后的子字符串个数。

看看整个源码:

 public String[] split(String regex, int limit) {
        /* fastpath if the regex is a
         (1)one-char String and this character is not one of the
            RegEx's meta characters".$|()[{^?*+\\", or
         (2)two-char String and the first char is the backslash and
            the second is not the ascii digit or ascii letter.
         */
        char ch = 0;
        // 如果 regex 只有一位,且不为列出的特殊字符;// 如果 regex 有两位,第一位为转义字符且第二位不是数字或字母 
        // 第三个是和编码无关,就是不属于 utf-16 之间的字符
        if (((regex.value.length == 1 &&
             ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
             (regex.length() == 2 &&
              regex.charAt(0) == '\\' &&
              (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
              ((ch-'a')|('z'-ch)) < 0 &&
              ((ch-'A')|('Z'-ch)) < 0)) &&
            (ch < Character.MIN_HIGH_SURROGATE ||
             ch > Character.MAX_LOW_SURROGATE))
        {
            int off = 0;
            int next = 0;
            boolean limited = limit > 0;
            ArrayList<String> list = new ArrayList<>();
            while ((next = indexOf(ch, off)) != -1) {if (!limited || list.size() < limit - 1) {list.add(substring(off, next));
                    off = next + 1;
                } else {    // last one
                    //assert (list.size() == limit - 1);
                    list.add(substring(off, value.length));
                    off = value.length;
                    break;
                }
            }
            // If no match was found, return this
            if (off == 0)
                return new String[]{this};

            // Add remaining segment
            if (!limited || list.size() < limit)
                list.add(substring(off, value.length));

            // Construct result
            int resultSize = list.size();
            if (limit == 0) {while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {resultSize--;}
            }
            String[] result = new String[resultSize];
            return list.subList(0, resultSize).toArray(result);
        }
        return Pattern.compile(regex).split(this, limit);
    }

接下来咱们一步步来剖析:

能够看到有三个条件:

  1. 如果 regex 只有一位,且不为列出的特殊字符
  2. 如果 regex 有两位,第一位为转义字符且第二位不是数字或字母
  3. 第三个是和编码无关,就是不属于 utf-16 之间的字符

只有满足下面三个条件能力进入下一步:

第一次宰割时,应用 offnextoff指向每次宰割的起始地位,next指向分隔符的下标,实现一次宰割后更新 off 的值,当 list 的大小等于 limit-1 时,间接增加剩下子字符串,具体看下源码:

最初就是对子字符串进行解决:

集体感觉这部分源码还是比拟难的,有趣味的同学能够再去钻研一下。

trim

删除字符串的头尾空白符

String str = "wugui";
System.out.println(str.trim());

输入:wugui

这部分还是比较简单的,这里不再细说。

compareTo

比拟两个字符

String a = "a";
String b = "b";
System.out.println(a.compareTo(b));

输入:-1

看看源码:

总结

无关 String 的源码临时剖析到这里,其它的源码感兴趣的小伙伴能够按本人去钻研一下,接下来可能会得写几篇文章来介绍一下 Java 中的包装类,敬请期待!

正文完
 0