阅读Java的String类与StringBuffer类源程序

来源:互联网 发布:php禁止查看源代码 编辑:程序博客网 时间:2024/05/18 08:15

注:读的是比较早的一个版本。status updated to 1.4

关于char和byte

在Sting类中第一个引起我注意的点是publicString(byte[] ascii, int hibyte, int offset, int count)函数。这个函数将一个byte[]数组的字符串,每个byte前面加上8bits的hibyte前缀,形成一个新的String,而String中存储用的是char[]数组。也就是说,Java中的char是两个字节的。

可以猜测,char这么设计是为了支持unicode等两个字节的码,也就是说一个char可以是一个汉字。经实际测试,让a为‘你’,发现确实如此。

虽然是两个字节,却不能存下两个普通的英文字母。经测试,让b为‘ni’,出现提示 “Invalid character constant”。也就是说如果放英文字母的话,char里也只能放一个。

关于编码

前面所说的public String(byte[] ascii, inthibyte, int offset, int count),及其简略版publicString(byte[] ascii, int hibyte)是已经deprecated的方法。我认为原因是:一般人们没有必要去手动设置高8位的值,而希望采用更智能的方式——输入编码格式。

因此,推荐使用的是方法public String(byte[] data, intoffset, int count, String encoding),及其简略版publicString(byte[] data, String encoding)。在这个函数的内部,是使用CharsetDecoder来实现的。

查文档,charset大概有如下几种。

Charset

描述

US-ASCII

7 位 ASCII 字符,也叫作 ISO646-US、Unicode 字符集的基本拉丁块

ISO-8859-1  

ISO 拉丁字母表 No.1,也叫作 ISO-LATIN-1

UTF-8

8 位 UCS 转换格式

UTF-16BE

16 位 UCS 转换格式,Big Endian(最低地址存放高位字节)字节顺序

UTF-16LE

16 位 UCS 转换格式,Little-endian(最高地址存放低位字节)字节顺序

UTF-16

16 位 UCS 转换格式,字节顺序由可选的字节顺序标记来标识

 

方法public String(byte[] data, int offset,int count)和简略版public String(byte[] data)是在没有encoding的情况下将byte生成String。事实上使用的encoding是环境默认的编码方式,通过System.getProperty("file.encoding")获得。

如果将char[]转成byte[]没成功,会进入安全模式('safe' encoding)。每个char如果高的那个字节全为0的话,也就是ASCII字符,直接将char转成byte。如果高位不全为0的话,转成字符 ‘?’,这或许就是我们在看一些网站的时候中文乱码为 ‘?’的原因吧。更常见的

关于大小写

在equalsIgnoreCase方法中,判断不考虑大小写下两个字母是否相等时,用的判断条件是(c1 != c2 && Character.toUpperCase(c1) !=Character.toUpperCase(c2) && Character.toLowerCase(c1) != Character.toLowerCase(c2))。

其实后两个判断就够用了,为什么要累赘的c1!=c2的比较呢?注释中说的很明白:Note that checking c1 != c2 is redundant, but avoids method calls。因为一般来说还是大小写一样的情况比较多,先判断c1!=c2可以尽可能避免调用toUpperCase等方法,从而提高效率。这我是可以理解并认同的。

但是——难道前两个条件还不够吗?把c1和c2都转成了UpperCase,难道还不足以判别是否相等吗?为什么还要写第三条判断呢?

另外,在转化大小写的时候对于土耳其语做了特殊判断。一般来说,字符“i”(\u0069) 是字符“I”(\u0049)的小写版本。 但是,土耳其语中有“带点的 I”字符“İ”(\u0130),该字符是“i”的大写版本。 土耳其语还包括小写的“不带点的 i”字符“ı”(\u0131),该字符的大写形式才是“I”。 (参考http://msdn.microsoft.com/zh-cn/library/azure/dd465121.aspx?cs-save-lang=1&cs-lang=vb

关于ensureCapacity

StringBuffer中,最值得一看的就是ensureCapacity方法了。

ensureCapacity_unsynchronized方法代码如下:

 

  private void ensureCapacity_unsynchronized(int minimumCapacity)  {    if (shared || minimumCapacity > value.length)      {        int max = (minimumCapacity > value.length                   ? value.length * 2 + 2                   : value.length);        minimumCapacity = (minimumCapacity < max ? max : minimumCapacity);        char[] nb = new char[minimumCapacity];        VMSystem.arraycopy(value, 0, nb, 0, count);        value = nb;        shared = false;      }  }

先抛开share不看,要做的主要事情是确保大小。如果设定值minimumCapacity比原来的length小(或相等)的话就用原来的。如果大的话,就用length * 2 + 2和minimumCapacity中比较大的那个。新建一个这么大的char[]数组,把原来的值拷贝进去,把StringBuffer的value换成这个数组。

为什么不直接采用minimumCapacity,而还要考虑length * 2 + 2呢?我想这是种用空间换时间的策略。如果只是吧空间升级成了minimumCapacity,下次可能还要升级。而使用length*2+2的话,下次别增长太多是不会超的。

每次扩张到两倍,这是有道理的。以前数据结构讲hash的时候提到过类似的思想,如果一个哈希表满了,就开一个两倍的再复制进去,这样的扩展策略均摊复杂度是O(n)的,还可以接受。至于加2嘛,应该是避免append时新加的String差不多和原来的Stirng差不多长(多一两个字符)出现的麻烦吧。

再说这个share的问题。如果之前这个StringBuffer已经是share的话,一旦调用这个方法就会变成非share的。我推测,这么做应该是基于这个考虑——

被share的StringBuffer有改动有可能导致一些指向它的String变化。这是不允许的。因此,在StringBuffer进行一些会改变内容的操作时(setCharAt),会调用方法ensureCapacity_unsynchronized。这不是真的为了确保空间,而是要与share的String分开来。

其他小细节

l  很多时候函数重载不用重新写代码,只要把某些参数值补上,调用另一个函数即可。这个实践符合Don’t repeat yourself原则。

l  调用getBytes时是返回byte[]的,而getChars是在传入的变量中指定位置的,要返回char[]得用toCharArray。

l  计算hashCode可能被认为比较费时,因此将计算过的值缓存在cachedHashCode中。cachedHashCode为0被认为没有计算过hashCode,即使hashCode真为0也重新计算(反正不费时间)

l  indexOf方法的String版本的实现居然是枚举fromIndex,一个个比较regionMatches。为什么不用KMP算法呢?(看新版本貌似改了)

l  substring方法中子串如果达到1/4原长就用don’t copy模式了。

l  以前以为trim的时候是去掉两头的空格,现在发现还有两头ascii码小于空格的那些控制符。

l  在String中valueOf是静态函数,可以用于各种类型的数据变成字符。

l  intern 方法针对的是一个字符串池。如果池里有一样的字符串,则返回池里的字符串。不然就把String加到池里,返回其引用。

l  可以使用append扩充StringBuffer,而不需要新建一个。

l  使用synchronized来确保线程安全。改动StringBuffer部分用synchronized(){}括起来,避免多个线程同时使用。有点类似于操作系统中的signal信号量。

String与StringBuffer的相互使用

String可以通过StringBuffer来构造。如果StringBuffer中的字符串长度在buffer总长度的1/4以内,使用VMSystem.arraycopy构造新的String。如果比1/4长的话,直接分享,让StringBuffer的share变量为true,把StringBuffer的char[]数组赋给String,类似于传了指针。

StringBuffer的substring方法可返回String,如果子串有1/4长的话就让share变true。构造String时根据share与否,决定是拷贝还是传指针。(String的substring方法也是类似)

具体在String中的构造函数是这样的:

</pre><pre name="code" class="java"> private voidensureCapacity_unsynchronized(int minimumCapacity)  {    if (shared || minimumCapacity >value.length)      {        int max = (minimumCapacity >value.length                   ? value.length * 2 + 2                   : value.length);        minimumCapacity = (minimumCapacity <max ? max : minimumCapacity);        char[] nb = new char[minimumCapacity];        VMSystem.arraycopy(value, 0, nb, 0,count);        value = nb;        shared = false;      }  }


通过这里value=data这样的做法,实际传的是指针。Java里没有指针的说法,可以认为传递的是char[]这个对象。在Java中对象的传递不是拷贝的,而仅仅是引用。

String总结

String类本身是final的。在我的理解中这里的final有点类似于C/C++里的const,也就是String一旦创建就改不了了。所以可以看到,对String进行处理的操作,其实都是返回一个新的String,而非对原来的String类进行操作。String的接口有Serializable, Comparable, CharSequence。

String中非static的变量有四个:

l  final char[] value;

l  final int count;

l  private int cachedHashCode;

l  final int offset;

也就是说除了比较的字符串中的值以外,每个string还要存储三个int。虽然不像C那么对空间锱铢必较,但浪费12个字节还算能接受。

StringBuffer总结

StringBuffer类本身是final的,接口有Serializable, CharSequence。非final的有

l  int count;

l  char[] value;

l  boolean shared;

StringBuffer类的append,insert,reverse方法是String类没有的。而一些跟正则表达式有关的匹配方法如split,replaceAll,replaceFirst等方法是StringBuffer类没有的。

String和StringBuffer的区别总结

l  StringBuffer是线程安全的,而String不是。使用多线程的时候还用StringBuffer比较好。

l  String中的char[]的余量主要在offset处,而一般offset为0,char的大小就是要保存的字符串的大小。当要做一些操作时,都要新构造一个String。StringBuffer的char[]大小有比较大的余量,做一些操作时如果新串的长度没有超过空间,就可以直接在原来的数组上做的。StringBuffer比较适合空间比较充裕,而希望效率高的情况。

0 0
原创粉丝点击