String类常用方法源码分析

来源:互联网 发布:tcp网络编程java 编辑:程序博客网 时间:2024/06/05 20:47

环境:JDK8

主要分析String类的一些常用的方法源码。

String

先看String类的定义:

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

可以看到String类被final修饰,因此不能被继承。String类还实现了序列化接口Serializable、可比较的接口Comparable并指定范型为String,该接口必须要实现int compareTo(T o)   方法。最后还实现了字符序列CharSequence接口,该接口也有些常用的方法,如charAt(int index)    、length()、toString()    等


构造字符串

String类的无参构造函数:

  1. /**
  2.     * Initializes a newly created {@code String} object so that it represents
  3.     * an empty character sequence.  Note that use of this constructor is
  4.     * unnecessary since Strings are immutable.
  5.     */
  6.    public String() {
  7.        this.value = "".value;
  8.    }

其中value定义:

  1. private final char[] value

该构造函数创建了一个空的字符串并存在字符数组value中。


再看一个有参的构造函数:

  1. public String(char value[]) {
  2.        this.value = Arrays.copyOf(value, value.length);
  3. }

该构造函数指定一个字符数组来创建一个字符序列。是通过Arrays的copyOf方法将字符数组拷贝到当前数组。

这样当修改字符数组的子串时,不会影响新字符数组。


经过以上分析可以看出,下面两个语句是等价的,因为String类底层使用char[]数组来存储字符序列。

  1. char data[] = {'a', 'b', 'c'};
  2. String str = new String(data);


使用字节数组构造一个String

在Java中,String实例中保存有一个char[]字符数组,char[]字符数组是以unicode码来存储的,String 和 char 为内存形式,byte是网络传输或存储的序列化形式。所以在很多传输和存储的过程中需要将byte[]数组和String进行相互转化。所以,String提供了一系列重载的构造方法来将一个字符数组转化成String,提到byte[]和String之间的相互转换就不得不关注编码问题。

String(byte[] bytes, Charset charset) 是指通过charset来解码指定的byte数组,将其解码成unicode的char[]数组,够造成新的String。

  • 这里的bytes字节流是使用charset进行编码的,想要将他转换成unicode的char[]数组,而又保证不出现乱码,那就要指定其解码方式。

如果我们在使用byte[]构造String的时候,使用的是下面这四种构造方法(带有charsetName或者charset参数)的一种的话,那么就会使用StringCoding.decode方法进行解码,使用的解码的字符集就是我们指定的charsetName或者charset。 我们在使用byte[]构造String的时候,如果没有指明解码使用的字符集的话,那么StringCoding的decode方法首先调用系统的默认编码格式,如果没有指定编码格式则默认使用ISO-8859-1编码格式进行编码操作。主要体现代码如下:

  1. static byte[] encode(String charsetName, char[] ca, int off, int len)
  2.        throws UnsupportedEncodingException
  3.    {
  4.        StringEncoder se = deref(encoder);
  5.        String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;
  6.        if ((se == null) || !(csn.equals(se.requestedCharsetName())
  7.                              || csn.equals(se.charsetName()))) {
  8.            se = null;
  9.            try {
  10.                Charset cs = lookupCharset(csn);
  11.                if (cs != null)
  12.                    se = new StringEncoder(cs, csn);
  13.            } catch (IllegalCharsetNameException x) {}
  14.            if (se == null)
  15.                throw new UnsupportedEncodingException (csn);
  16.            set(encoder, se);
  17.        }
  18.        return se.encode(ca, off, len);
  19. }

上面是编码清单,下面是解码清单:

  1. static char[] decode(String charsetName, byte[] ba, int off, int len)
  2.        throws UnsupportedEncodingException
  3.    {
  4.        StringDecoder sd = deref(decoder);
  5.        String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;
  6.        if ((sd == null) || !(csn.equals(sd.requestedCharsetName())
  7.                              || csn.equals(sd.charsetName()))) {
  8.            sd = null;
  9.            try {
  10.                Charset cs = lookupCharset(csn);
  11.                if (cs != null)
  12.                    sd = new StringDecoder(cs, csn);
  13.            } catch (IllegalCharsetNameException x) {}
  14.            if (sd == null)
  15.                throw new UnsupportedEncodingException(csn);
  16.            set(decoder, sd);
  17.        }
  18.        return sd.decode(ba, off, len);
  19. }

charAt

再看charAt(int index)方法源码:

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

该方法返回字符序列中下标为index的字符。并且index的范围:(0,value.length].

concat

再看publicString concat(String str)

  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.    }

该方法先判断传递进来的参数字符串长度是否为0,如果是就返回当前字符串。

否则使用Arrays类的静态方法copyOf(char[] original, int newLength)

拷贝当前字符数组到新数组,指定长度为当前字符串长度加上参数字符串长度,然后通过getChars方法将value字符数组拷贝到buf字符数组,这点可以从getChars方法的实现中看到:

  1. void getChars(char dst[], int dstBegin) {
  2.        System.arraycopy(value, 0, dst, dstBegin, value.length);
  3. }

可以看到,连接字符串操作实际是字符串的拷贝。最后,返回连接成功后的字符串。

最后是一个特殊的私有包范围类型的构造方法,String除了提供了很多公有的供程序员使用的构造方法以外,还提供了一个包范围类型的构造方法(Jdk 8),我们看一下他是怎么样的:

  1. String(char[] value, boolean share) {
  2.        // assert share : "unshared not supported";
  3.        this.value = value;
  4. }

从代码中我们可以看出,该方法和 String(char[] value)有两点区别:

  1. 第一个,该方法多了一个参数: boolean share,其实这个参数在方法体中根本没被使用,也给了注释,目前不支持使用false,只使用true。那么可以断定,加入这个share的只是为了区分于String(char[] value)方法,不加这个参数就没办法定义这个函数,只有参数不能才能进行重载。

  2. 第二个区别就是具体的方法实现不同。

我们前面提到过,String(char[] value)方法在创建String的时候会用到Arrays的copyOf方法将value中的内容逐一复制到String当中,而这个String(char[] value, boolean share)方法则是直接将value的引用赋值给String的value。

那么也就是说,这个方法构造出来的String和参数传过来的char[] value共享同一个数组。 那么,为什么Java会提供这样一个方法呢?

 首先,我们分析一下使用该构造函数的好处:

  1. 首先,性能好,这个很简单,一个是直接给数组赋值(相当于直接将String的value的指针指向char[]数组),一个是逐一拷贝。当然是直接赋值快了。

  2. 其次,共享内部数组节约内存

但是,该方法之所以设置为包范围,是因为一旦该方法设置为公有,在外面可以访问的话,那就破坏了字符串的不可变性。例如如下YY情形:

  1. char[] arr = new char[] {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'};
  2. String s = new String(0, arr.length, arr); // "hello world"
  3. arr[0] = 'a'; // replace the first character with 'a'
  4. System.out.println(s); // aello world

如果构造方法没有对arr进行拷贝,那么其他人就可以在字符串外部修改该数组,由于它们引用的是同一个数组,因此对arr的修改就相当于修改了字符串。

所以,从安全性角度考虑,他也是安全的。对于调用他的方法来说,由于无论是原字符串还是新字符串,其value数组本身都是String对象的私有属性,从外部是无法访问的,因此对两个字符串来说都很安全。

在Jdk7 开始就有很多String里面的方法都使用这种“性能好的、节约内存的、安全”的构造函数。比如:substring、replace、concat、valueOf等方法(实际上他们使用的是public String(char[], int, int)方法,原理和本方法相同,已经被本方法取代)。

但是在Jdk 7中,substring已经不再使用这种“优秀”的方法了,为什么呢? 虽然这种方法有很多优点,但是他有一个致命的缺点,对于sun公司的程序员来说是一个零容忍的bug,那就是他很有可能造成内存泄露。 

看一个例子,假设一个方法从某个地方(文件、数据库或网络)取得了一个很长的字符串,然后对其进行解析并提取其中的一小段内容,这种情况经常发生在网页抓取或进行日志分析的时候。下面是示例代码:

  1. String aLongString = "...a very long string...";
  2. String aPart = data.substring(20, 40);
  3. return aPart;

在这里aLongString只是临时的,真正有用的是aPart,其长度只有20个字符,但是它的内部数组却是从aLongString那里共享的,因此虽然aLongString本身可以被回收,但它的内部数组却不能(如下图)。这就导致了内存泄漏。如果一个程序中这种情况经常发生有可能会导致严重的后果,如内存溢出,或性能下降。



下面贴下jdk6的substring源码:

  1. public String More ...substring(int beginIndex, int endIndex) {
  2.        if (beginIndex < 0) {
  3.            throw new StringIndexOutOfBoundsException(beginIndex);
  4.        }
  5.        if (endIndex > count) {
  6.            throw new StringIndexOutOfBoundsException(endIndex);
  7.        }
  8.       if (beginIndex > endIndex) {
  9.          throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
  10.       }
  11.        return ((beginIndex == 0) && (endIndex == count)) ? this :
  12.            new String(offset + beginIndex, endIndex - beginIndex, value);
  13.  }

最后返回调用

  1. String(int offset, int count, char value[]) {
  2.         this.value = value;
  3.         this.offset = offset;
  4.        this.count = count;
  5. }


下面阅读一下jdk8中的substring方法和jdk6做个比较

substring

substring有两个重载方法:

  1. public String substring(int beginIndex) {
  2.        if (beginIndex < 0) {
  3.            throw new StringIndexOutOfBoundsException(beginIndex);
  4.        }
  5.        int subLen = value.length - beginIndex;
  6.        if (subLen < 0) {
  7.            throw new StringIndexOutOfBoundsException(subLen);
  8.        }
  9.        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
  10. }
  1. public String substring(int beginIndex, int endIndex) {
  2.        if (beginIndex < 0) {
  3.            throw new StringIndexOutOfBoundsException(beginIndex);
  4.        }
  5.        if (endIndex > value.length) {
  6.            throw new StringIndexOutOfBoundsException(endIndex);
  7.        }
  8.        int subLen = endIndex - beginIndex;
  9.        if (subLen < 0) {
  10.            throw new StringIndexOutOfBoundsException(subLen);
  11.        }
  12.        return ((beginIndex == 0) && (endIndex == value.length)) ? this
  13.                : new String(value, beginIndex, subLen);
  14. }

这两个重载方法都是先计算要截取的子串长度,判断边界最后返回调用new String(value, beginIndex, subLen)方法,我们来看一下这个方法:

  1. public String(char value[], int offset, int count) {
  2.        if (offset < 0) {
  3.            throw new StringIndexOutOfBoundsException(offset);
  4.        }
  5.        if (count <= 0) {
  6.            if (count < 0) {
  7.                throw new StringIndexOutOfBoundsException(count);
  8.            }
  9.            if (offset <= value.length) {
  10.                this.value = "".value;
  11.                return;
  12.            }
  13.        }
  14.        // Note: offset or count might be near -1>>>1.
  15.        if (offset > value.length - count) {
  16.            throw new StringIndexOutOfBoundsException(offset + count);
  17.        }
  18.        this.value = Arrays.copyOfRange(value, offset, offset+count);
  19. }

offset指第一个匹配的字符序列的索引,count指子串的长度。

最终该子串会被拷贝到字符数组value中,并且后续的字符数组的修改并不影响新创建的字符串。

可以看到JDK6后substring方法底层是字符串的拷贝而不是数组引用。

新的实现虽然损失了性能,而且浪费了一些存储空间,但却保证了字符串的内部数组可以和字符串对象一起被回收,从而防止发生内存泄漏,因此新的substring比原来的更健壮。

contains

再来看public boolean contains(CharSequence s):

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

该直接调用indexOf(String)方法:

  1. public int indexOf(String str) {
  2.        return indexOf(str, 0);
  3. }

indexOf方法中又调用indexOf(String,int)方法,在该方法中又返回调用静态方法

static int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex):

  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      要被搜索的字符串,即源字符串
  7.     * @param   sourceOffset 源字符串的偏移
  8.     * @param   sourceCount  源字符串的长度
  9.     * @param   target       要在这个字符串中搜索,即目标字符串
  10.     * @param   targetOffset 目标字符串偏移.
  11.     * @param   targetCount  目标字符串长度.
  12.     * @param   fromIndex    开始搜索的索引.
  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.        char first = target[targetOffset];
  27.        int max = sourceOffset + (sourceCount - targetCount);
  28.        for (int i = sourceOffset + fromIndex; i <= max; i++) {
  29.            /* Look for first character. */
  30.            if (source[i] != first) {
  31.                while (++i <= max && source[i] != first);
  32.            }
  33.            /* Found first character, now look at the rest of v2 */
  34.            if (i <= max) {
  35.                int j = i + 1;
  36.                int end = j + targetCount - 1;
  37.                for (int k = targetOffset + 1; j < end && source[j]
  38.                        == target[k]; j++, k++);
  39.                if (j == end) {
  40.                    /* Found whole string. */
  41.                    return i - sourceOffset;
  42.                }
  43.            }
  44.        }
  45.        return -1;
  46. }

首先判断开始索引如果大于源字符串则返回,若目标字符串长度为0返回源字符串长度,否则返回-1.

然后迭代查找字符,若全部源字符串都找到则返回第一个匹配的索引,否则返回-1.

所以在public boolean contains(CharSequence s)方法中,若indexOf方法返回-1则返回false,否则返回true。

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. }

该方法首先判断this == anObject ?,也就是说判断要比较的对象和当前对象是不是同一个对象,如果是直接返回true,如不是再继续比较,然后在判断anObject是不是String类型的,如果不是,直接返回false,如果是再继续比较,到了能终于比较字符数组的时候,他还是先比较了两个数组的长度,不一样直接返回false,一样再逐一比较值。


join

最后阅读下jdk8新加入的String类的静态方法join,这个方法是通过分隔符delimiter来构造字符的:

  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. }

StringJoiner 类也是jdk1.8开始加入的通过分隔符或前缀或后缀来构造字符串的,底层是字符序列的拷贝。


这个方法要求参数是都不能为空的,否则回报空指针异常,requireNonNull方法源码可以说明这点:

  1. public static <T> T requireNonNull(T obj) {
  2.        if (obj == null)
  3.            throw new NullPointerException();
  4.        return obj;
  5. }

该方法是jdk1.7开始加入的操作对象的工具类Objects包含的方法。

例子:

  1. String message = String.join("-", "Java", "is", "cool");
  2. // message returned is: "Java-is-cool"


如有疏漏请指出,谢谢!



部分内容及图片参考:

http://www.hollischuang.com/archives/99

原创粉丝点击