Yancey

人生,或是一次勇敢的探险,或是,虚掷光阴。

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

Java中,除了8种基本类型,最长用的应该就是String类了。那么我们来看看JDK中的源码是怎么建造String、StringBuffer、StrinBuilder一系列类的。


 java.lang.String

在JAVA里,String类是一个非常特殊的类,我们来看一下它是怎么来表示一个字符串的。首先来看一下它的比较重要的几个属性,源码如下:

/** The value is used for character storage. */
private final char value[];

/** The offset is the first index of the storage that is used. */
private final int offset;

/** The count is the number of characters in the String. */
private final int count;

是的,其实String是用一个char数组来储存的,offset表示char[]的起始位置。count表示字符串的长度,通过字符串的length()方法只有一行return count;就看得出来。

  这里的属性都是final的,这就是String值为什么不能被改变【可不是因为final class String的final呦】。这么做的好处是String对象可以在不同用户间共用,本身是线程安全的。

来看一个构造方法:

 1 public String(String original) {
 2     int size = original.count;
 3     char[] originalValue = original.value;
 4     char[] v;
 5     if (originalValue.length > size) {
 6         // 如果传入的original的存储char[]的长度大于要构造的字符串的长度,就copy original有效的那部分。
 7         int off = original.offset;
 8         v = Arrays.copyOfRange(originalValue, off, off+size);
 9     } else {
10         // 因为传入的original的存储char[]的长度等于要构造的字符串的长度,所以无需copy
11         v = originalValue;
12     }
13     this.offset = 0;
14     this.count = size;
15     this.value = v;
16 }

   先说点题外的,笔者我第一次看到第2行的时候感到很诧异,String类的count属性不是private的吗。后来经过自己实验并反复回忆初学JAVA时老师对private的描述,private修饰的属性是只有在类的内部时才能被访问,于是恍然大悟。重新认识了private关键字,也是个收获。

  再来说这个构造方法,大概翻译了下JDK的注释。这里有两点,一是第8行的数组复制,里面是最终用的是System.arraycopy()方法,这个方法在JAVA中复制数组是最快的【这是个native方法,至于底层怎么实现就不太了解,猜测是通过内存里的地址吧】。再就是第11行的v = originalValue;,就是说,用于构造String的字符串,和构造出来的新的字符串,value属性是指向同一个char[]的【其实这也无所谓,因为String.value是final的】。

  关于字符集的构造方法这里就不说了,转换来转换去好烦。

  下面来看几个Sring类常用的方法的实现:

public boolean equals(Object anObject)

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = count;
        if (n == anotherString.count) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = offset;
            int j = anotherString.offset;
            while (n-- != 0) {
                if (v1[i++] != v2[j++])
                return false;
            }
            return true;
        }
    }
    return false;
}
View Code

先比较hashCode,然后再遍历对比两个String的value值,没什么好说的。

public boolean startsWith(String prefix, int toffset)

public boolean startsWith(String prefix, int toffset) {
    char ta[] = value;
    int to = offset + toffset;
    char pa[] = prefix.value;
    int po = prefix.offset;
    int pc = prefix.count;
    // Note: toffset might be near -1>>>1.
    if ((toffset < 0) || (toffset > count - pc)) {
        return false;
    }
    while (--pc >= 0) {
        if (ta[to++] != pa[po++]) {
            return false;
        }
    }
    return true;
}
View Code

也很简单,遍历字符串的value值。不过注释很有意思:-1>>>1。【-1的无符号右移1位,就是整数最大值,你懂得

public int indexOf(String str)

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];
    //遍历source时,如果剩下的字符比target还少,就没必要继续遍历了,就是这个max的作用。
    int max = sourceOffset + (sourceCount - targetCount);

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

        /* 匹配整个target是否完全匹配,如果不完全匹配并且source字符串还没遍历完就继续外层的for循环 */
        if (i <= max) {
            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;
}
View Code

我们在使用这个方法时会有一个陷阱,就是sourceString.indexOf(""),传入空字符串会返回0。

public String substring(int beginIndex, int endIndex)

public String substring(int beginIndex, int endIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    if (endIndex > count) {
        throw new StringIndexOutOfBoundsException(endIndex);
    }
    if (beginIndex > endIndex) {
        throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
    }
    return ((beginIndex == 0) && (endIndex == count)) ? this :
        new String(offset + beginIndex, endIndex - beginIndex, value);
}
View Code

很简单,构造一个新的字符串。

  总之,这些常用的方法也不过是对char[]翻来覆去的操作。但提到String类就不得不提String Pool,这是JVM为了提高效率的一个缓存机制【至于JVM的这个原理,研究层次就更深入了,我想我以后会去了解吧】。目前要搞清楚String对象什么时候会在String Pool中建立,什么时候在Heap中建立,并将引用指向哪个,这个也是非常重要的。

String Pool(字符串池)

在创建字符串时,JVM的管理有如下特点:

  • 每当用任意方式创建一个String对象时,JVM都会String Pool中查找是否存在内容相同的字符串对象,如果不存在,则在池中创建一个字符串。
  • 在使用new关键字或包含变量的字符串拼接时,一定会在Heap中建立这个对象,并将引用指向这个对象。【同时仍然会维护String Pool】
  • 使用等号直接指定,或纯字符串拼接时,只会操作String Pool,如果有这个字符串则直接将引用指向,如果没有则创建后指向。
String temp = "ab";
String str1 = new String("abcd");
String str2 = "abcd";
String str3 = "ab" + "cd";
String str4 = temp + "cd";

System.out.println(str1 == str2);//false
System.out.println(str2 == str3);//true
System.out.println(str3 == str4);//false
有兴趣的可以自己试试,这里列举几个例子。

public native String intern();

  最后来说intern()。这是个native方法,它的执行结果是将字符串的引用指向String Pool中的对象。这样Heap中的对象就可以被回收掉了。

java.lang.StringBuffer

  我们经常需要String的变化,但String却是不可变的,那么只好串接后生成一个新的String对象来满足变化的需求。如果频繁的进行字符串拼接就会产生出大量的对象,这样很耗性能。所以这时,轮到StringBuffer来登场了。顾名思义,这是一个字符串缓冲区,可以随意变化。StringBuffer 上的主要操作是 append 和 insert 方法。

public synchronized StringBuffer append(String str) 

1 public synchronized StringBuffer append(String str) {
2     super.append(str);//父类方法如下
3     return this;
4 }
 1 public AbstractStringBuilder append(String str) {
 2     if (str == null) str = "null";
 3         int len = str.length();
 4     if (len == 0) return this;
 5     int newCount = count + len;
 6     if (newCount > value.length)
 7         expandCapacity(newCount);//这个扩容的方法如下
 8     str.getChars(0, len, value, count);
 9     count = newCount;
10     return this;
11 }
1 void expandCapacity(int minimumCapacity) {
2     int newCapacity = (value.length + 1) * 2;
3     if (newCapacity < 0) {
4         newCapacity = Integer.MAX_VALUE;
5     } else if (minimumCapacity > newCapacity) {
6         newCapacity = minimumCapacity;
7     }
8     value = Arrays.copyOf(value, newCapacity);
9 }

  从上面我们可以看出,一是如果拼接字符串时,变量为NULL,则按"null"来处理;二则是那个扩容。我们在构造一个StringBuffer时,如果不设置一个长度,则JDK会默认长度为16,如果我们最终拼接的字符串的长度远远超出这个长度,则StringBuffer会频繁的做扩容操作,所以,在new StringBuffer(),传入一个预估的长度参数是一个好习惯。

java.lang.StringBuilder

这个类与StringBuffer的区别就是synchronized,提高效率嘛,都懂得。笔者这里测试了一下,大概也就差1倍的速度。测试代码如下:

StringBuffer sb1 = new StringBuffer(1024);
StringBuilder sb2 = new StringBuilder(1024);
for(int i = 0; i < 10000000; i++){
    //sb1.append("abc");
    sb2.append("abc");
    if((i & 1023) == 0){
        //sb1 = new StringBuffer(1024);
        sb2 = new StringBuilder(1024);
    }
}
View Code

测试结果为:

StringBuffer平均耗时399ms

StringBuilder平均耗时254ms


好了,本次的学习就到这里吧。

学习是件快乐而又有成就感的事。

 

posted on 2013-12-24 21:01  Yancey.Han  阅读(1645)  评论(1编辑  收藏  举报