Chinaunix首页 | 论坛 | 博客
  • 博客访问: 251960
  • 博文数量: 65
  • 博客积分: 2026
  • 博客等级: 大尉
  • 技术积分: 695
  • 用 户 组: 普通用户
  • 注册时间: 2006-02-12 14:34
文章分类

全部博文(65)

文章存档

2012年(1)

2011年(1)

2010年(1)

2009年(2)

2008年(7)

2007年(6)

2006年(47)

我的朋友

分类: Java

2006-03-08 14:16:40

       其实在现在的很多开发框架下,字符串操作已经成为了重中之重,毕竟个人电脑的主要工作是信息加工而不是自动控制,而信息,说白了就是字符串。HTML是字符串,源程序是字符串,Word文档什么什么的都是字符串。
        java提供了非比寻常的字符串处理包装。我想这也是让java能够大放异彩的因素之一。
 
        首先说一下字符串的原理。
        串这种数据结构表示的是一组具有连续内存空间的线性,诚然字符串也有链式存储的,但大多是作为索引等应用存在的。串的要求不是快速的插入删除个中元素,而是需要能够快速的模式匹配,模式匹配是信息加工中最为重要的一种操作。C/C++的标准库提供了基于逐字节比较的模式匹配,用汇编实现,在普通应用中已经足够了。但在java的源代码中我们可以看到,java采用了更好的KMP算法实现模式匹配,在查找大量重复数据的时候,有着更好的表现。
        字符串在java中是一种特殊的存在,所谓的特殊,是指它和其它编程语言向比较而言的。
        在C语言里,字符串就是字符的数组,这个数组的长度由用户定义,由库函数操作,当然这个数组的释放也由用户来管理,字符数组的长度永远比字符串长度至少多1个字节,最后的一个字节填充0表示字符串的结束。所以字符串的长度和字符数组的长度是没有关系的,在字符数组长度的范围内,用户可以随意改变字符串的长度,也可以替换字符串内部的数据。如果在pascal中或者在IDL中,字符串的定义则和C语言不同,字符串的开始4字节存储字符串的长度,紧跟其后的是字符串的内容,所以字符串不能超过4GB的长度限制。pascal的这种存储字符串的方式避免了每次取字符串长度都要进行的迭代操作,所以成为了存储字符串的标准格式。
        java采用了C的方法进行字符串存储,通过查看源代码,我们可以看到,最初的内存是存储字符串内容的,其后紧跟一个指针,用来指示当前访问的字符数组的索引,其后才是字符串的长度。这都无所谓,经过类封装后,各种字符串的存储都是无差别的,但如果java以后想和其它语言进行字符串交换,则可能需要另外的类来做字符串格式转换。
        java的字符串的本质是char的数组,在java中,char是一个16位的有符号整形,相当于C语言中的unsigned short或C++中的wchar_t,java的字符串在String类中封装。
        编码是字符串处理的另一个重要环节,国际通用标准是UNICODE,也就是java中char所要体现的编码形式,UNICODE采用双字节编码,可以编码世界上所有的文字,在编码上和标准ASCII兼容,采用固定的双字节编码可以有效的提高字符串操作的效率。采用压缩编码形式的有UTF编码系列,而且可以欣喜的看到,java完全支持中文的本地编码——GBK编码。所有不同的编码形式在储存之前统一的转化为UNICODE编码,存储在String类中。
        基于java托管堆的特性(类似于C语言的栈),一个存储空间一旦被托管,则无法改变其大小(这一点和C语言不同,在C语言里,堆存储空间的大小在某种程度上可以进行修改,搬移和连接,这里的某种情况指的是必须在操作系统的授权和支持下才能完成,比如windows的本地堆或者全局堆就可以进行如上操作。所以处于存储安全上的考虑,java的字符串一旦被建立,是不允许改变其内容的,任何一种需要改变字符串内容的操作,都将导致String类重新生成新的字符串对象。
        不允许改变字符串内容,还是处于对多线程编程的考虑。在C++标准库中,string类使用引用计数器来保证对堆最少次的分配和释放工作,但是这个引用计数器导致了线程非安全的恶果。java为每个线程生成一个字符串对象,如果一个线程需要访问另一个线程的字符串对象,并且需要进行内容上的修改的话,那么String类会为这个线程生成一个新的对象,从而不影响到其它线程的操作。
        java还有一个特点,就是自动装箱字符串常量(也就是说java其实不存在字符串常量这个概念,字符串只要存在,必然在托管堆之上),这就减少了字符串操作的复杂度,Stirng str = "不用new操作";是被允许的,因为字符串"不用new操作"本身就是一个托管堆上的对象,而str直接把引用指向它就可以了。可以直接用"字符串"加“.”来象操作一个String对象一般操作字符串,虽然这种方式没什么实际用途。
        下面简要介绍java的字符串操作方法,可以看到,Sun的考虑是非常周到的,几乎所有的字符串应用都被考虑周全了。
        String.String构造器可以用一个byte数组,char数组或已知字符串构造新的字符串,如果采用byte数组的话,String方法还提供编码器,让用户告诉构造器当前byte数组是采用何种编码来存储字符串的,构造器采用对应的方法将byte重新编码为UNICODE字符串存储起来,如果编码不能被识别,则抛出UnsupportedEncodingException异常。
        String.getBytes方法能够使用一种字符串编码方式将字符串转化为字节数组。在实验代码中,我尝试了ASCII,UNICODE,GBK和UTF8这四种常用编码,效果都非常好。
        String.toCharArray方法直接返回一个包含该字符串的char的数组,而String.toChars则可以有选择的将字符串中某些字符返回一个char的数组。
        String.length方法返回字符串的长度,由于是UNICODE编码的,所以返回的总是实际的字符串长度,而与字符串编码无关。
        String.startsWith和String.endWith这两个方法可以考察一个字符串是否以规定字符串为起始或者结尾,这是一个非常有用的操作,可以查看用户的输入字符串是否符合基本格式。
        String.indexOf和String.lastIndexOf方法可以从字符串的前部或后部为起始,进行模式匹配,查找字符串中是否有目标子字符串。同时提供两种方向的操作可以让用户按照需求选择最快的模式匹配方式。
        String.compare和String.compareToIgnoreCase方法可以进行区分英文大小写或不区分的字符串比较。
        String.concat可以进行字符串连接,这和+的作用类似,返回两个字符串连接后的新字符串对象。
        String.substring方法可以用一个起始位置和终止位置返回已知字符串的子字符串,有可能会抛出一个StringIndexOutOfBoundsException(数组下标越界)异常,表示起始位置或者终止位置超越了字符串本身的长度。
        String.trim返回一个不包含起始空格和终止空格的原字符串拷贝。
        String.replace方法可以替换原有字符串中的任意字符和子字符串,如果替换失败,则返回原字符串的引用,否则返回一个新的字符串拷贝。
        String.valueOf是一组静态方法,可以将其它数据类型转化为字符串,返回一个新的String对象。
        可以发现,String没有提供insert方法,但可以用一些技巧来完成insert操作。
        顺便说一下,String类被final关键字修饰过,表示这个类无法被继承,String类的成员属性也都被final修饰过,Sun不希望用户自己定义字符串操作,是出于安全性考虑的。

        java依旧提供了可变字符串StringBuffer类,java分开提供两种操作方式在我认为,就是要告诉用户,在java中,可变长字符串的操作是效率比较低下的,所以如果没有需要,那么使用String类就好,我觉得,重新创建一个新的String实例也许也比在已有的堆中作动态内存调整要高效得多。当然,这些仅仅都是“我认为”而已。
        在考察源代码的时候,我发现了java是通过下述几种途径来操作StringBuffer类的。
        1、首先在分配空间的时候采取多分配的策略,除非用户指定分配空间的大小,否则分配的空间总是比用户需求的数量多32个字节(16个char)。
        2、其次,在追加空间长度的时候采用多分配的策略,本着分配新空间的长度不小于 (原空间长度 + 1)* 2的原则分配新的字符数组空间,如果用户指定新的空间大小大于这个数字,则按照用户指定大小追加空间,否则按照这个数字追加空间,称之为“保守策略”。
        3、采用本地源生代码进行数据复制,StringBuffer类采用System.arraycopy方法进行数据复制,而该方法是一个native方法,也就是使用c语言编写的二进制方法,可以充分的利用诸如Intel处理器的movs等指令高速复制内存。
        从源代码中也可以看到,每次进行insert,replace等等方法的时候,StringBuffer类都要考察实际空间大小,如果需求小于等于实际空间大小,则直接进行操作,否则就要放弃当前内存空间的引用,重新分配空间,并且将原有数据进行拷贝,这个策略被C++标准库的vector容器一直使用着,但java毕竟不是C++,所以从效率上,java出于一个比较尴尬的处境,在条件允许的情况下,尽量使用String类而不是StringBuffer类才是上上策。
        StringBuffer提供了许多String类已经提供的功能,这是为了方便用户,而更多的新功能现简要说明如下:
        StringBuffer.StringBuffer构造器,允许从任意种数据类型构造StringBuffer类。
        StringBuffer.append方法可以将一个字符串追加到原有字符串之后,该方法重载了多次,参数可以是任何一种简单对象或者重载了toString方法的Object类的子类。
        StringBuffer.capacity可以得到缓冲区的长度;而StringBuffer.length可以得到字符串的实际长度。
        StringBuffer.charAt可以得到字符串中的任意一个字符,抛出tringIndexOutOfBoundsException
异常。
        StringBuffer.delete可以删除字符串中任意一个字符,删除完毕后对缓冲区长度没有影响,该方法抛出StringIndexOutOfBoundsException异常。
        StringBuffer.ensureCapacity可以增加缓冲区的长度,但不允许减少它,增加的数字按照上面所说的保守策略进行。
        StringBuffer.insert可以插入一个字符串到原有的字符串的指定位置中,这个方法同样也被重载了多次,以适应不同的数据类型,可能会抛出StringIndexOutOfBoundsException异常。
        StringBuffer.reverse将会倒置字符串,采用的是《编程珠玑》中提到的一种翻转算法,用一个char的缓冲区经过多次叠代就可以达到目的。
        StringBuffer.setCharAt可以在已有字符串的任意一个位置重新设置字符,抛出StringIndexOutOfBoundsException异常。
        StringBuffer.substring的作用和在String类中相同。
        StringBuffer.setLength可以改变缓冲区的大小,不采用保守策略,直接按照用户意愿改变缓冲区大小,如果缓冲区大小比原先的要小,则不会保留多余的内容。
       
        可以发现,许多方法需要抛出StringIndexOutOfBoundsException异常,这个异常其实是char数组抛出的,表示下标访问越界异常,如果用心操作StringBuffer类,这个异常是决不会引发的,所以这个异常是一个选择性捕获的异常。
        StringBuffer类的大部分方法被标记为synchronized方法,在多线程中可以确保同步。

阅读(2462) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~