公司动态
Java——字符串
字符串52、推荐使用String直接量赋值53、注意方法中传递的参数要求54、正确使用String、StringBuffer、StringBuilder55、注意字符串的位置56、自由选择字符串拼接方法57、推荐在复杂字符串操作中使用正则表达式58、强烈建议使用UTF编码59、对字符串排序持一种宽容的心态52、推荐使用String直接量赋值一般对象都是通过new关键字生成的但是String还有第二种生成方式也就是我们经常使用的直接声明方式比如Str str “a”即是通过直接量“a”进行赋值的。对于String对象来说这种方式是极力推荐的但不建议使用new String(“a”)的方式赋值。为什么呢我们来看一段程序publicclassClient{publicstaticvoidmain(String[]args){Stringstr1中国;Stringstr2中国;Stringstr3newString(中国);Stringstr4str3.intern();//两个直接量是否相等booleanb1(str1str2);//trueSystem.out.println(b1);//直接量和对象是否相等booleanb2(str1str3);//falseSystem.out.println(b2);//经过intern处理后的对象与直接量是否相等booleanb3(str1str4);//trueSystem.out.println(b3);}}注意看上面的程序我们使用“”判断的是两个对象的引用地址是否相同也就是判断是否为同一个对象打印的结果是true,false,true。即有两个直接量是同一个对象经过intern处理后的String与直接量是同一个对象但直接通过new生成的对象却与之不相等原因何在原因是Java为了避免在一个系统中大量产生String对象为什么会大量产生因为String字符串是程序中最经常使用的类型于是就设计了一个字符串池也有叫做字符串常量池String Pool或String Constant Pool 或String Literal Pool在字符串池中所容纳的都是String字符串对象它的创建机制是这样的创建一个字符串时首先检查池中是否有字面值相等的字符串如果有则不再创建直接返回池中该对象的引用若没有则创建之然后放到池中并返回新建对象的引用这个池和我们平常所说的池概念非常相似。对于此例子来说就是在创建第一个“中国”字符串时先检查字符串池中有没有该对象发现没有于是就创建了“中国”这个字符串并放到池中待再创建str2字符串时由于池中已经有了该字符串于是就直接返回了该对象的引用此时str1和str2指向的是同一个地址所以使用“”来判断那当然是相等的了。那为什么使用new String(“中国”)就不相等了呢因为直接声明一个String对象是不检查字符串池的也不会把对象放到池中那当然“”为false了。那为什么使用intern方法处理后就又相等了呢因为intern会检查当前的对象在对象池中是否有字面值相同的引用对象如果有则返回池中对象如果没有则放置到对象池中并返回当前对象。可能有读者要问了对象放到池中会不会产生线程安全问题呀好问题不过Java已经考虑到了String类是一个不可变Immutable对象其实有两层意思一是String类是final类不可继承不可能产生一个String的子类二是在String类提供的所有方法中如果有String返回值就会新建一个String对象不对原对象进行修改这也就保证了原对象是不可改变的。还有读者问了放到池中是不是要考虑垃圾回收问题呀不用考虑了虽然Java的每个对象都保存在堆内存中但是字符串池非常特殊它在编译期已经决定了其存在JVM的常量池Constant Pool垃圾回收器是不会对它进行回收的。通过上面的介绍我们发现Java在字符串的创建方面确实提供了非常好的机制利用对象池不仅可以提高效率同时也减少了内存空间的占用建议大家在开发中使用直接量赋值方式除非确有必要才新建立一个String对象。53、注意方法中传递的参数要求有这样一个简单需求写一个方法实现从原始字符串中删除与之匹配的所有子字符串比如在“蓝蓝的天白云飘”中删除“白云飘”输出“蓝蓝的天”代码如下publicclassStringUtils{//删除字符串publicstaticStringremove(Stringsource,Stringsub){returnsource.replaceAll(sub,);}}StringUtils工具类很简单它采用了String的replace方法该方法是做字符串替换的我们来编写一个测试用例检查remove方法是否正确如下所示assertTrue(StringUtils.remove(好是好,好).equals(是));测试的结果是绿条Green Bar正确无误但是再看看如下的测试用例assertTrue(StringUtils.remove($是$,$).equals(是));上面只是把“好是好”中的两个“好”字替换成了一个“$符号猜猜结果会是什么应该也是绿条吧但是非常遗憾结果是红条测试未通过。就这么简单一个的替换为什么测试通不过呢问题就出在了replaceAll方法上该方法确实需要传递两个String类型的参数也确实进行了字符串替换但是它要求第一个参数是一个正则表达式符合正则表达式的字符串才会被替换。对上面的例子来说第一个测试案例传递进来的是一个字符串“好”这是一个全匹配查找替换处理得非常正确第二个测试案例传递进来的是“”符号“ ”符号“”符号“”符号在正则表达式中表示的是字符串的结束位置也就是说执行完repalceAll后在字符串结尾的地方加上了空字符串其结果还是“是 是是”所以测试失败也就在所难免了。问题清楚了解决方案也就出来了使用replace方法替代即可它是repalceAll方法的简化版可传递两个String参数继续替换与我们的编码意图是相吻合的。读者如果注意看JDK文档会发现replace(CharSequence target,CharSequencereplacement)方法是在1.5版本以后才开始提供的在此之前如果要对一个字符串进行全替换只能使用replaceAll方法不过由于replaceAll方法的第二个参数使用了正则表达式而且参数类型只要是CharSequence就可以String的父类所以很容易让使用者误解稍有不慎就会导致严重的替换错误。注意replaceAll传递的第一个参数是正则表达式。54、正确使用String、StringBuffer、StringBuilderCharSequence接口有三个实现类与字符串有关String、StringBuffer、StringBuilder虽然它们都与字符串有关但是其处理机制是不同的。String类是不可改变的量也就是创建后就不能再修改了比如创建了一个“abc”这样的字符串对象那么它在内存中永远都会是“abc”这样具有固定表面值的一个对象不能被修改即使想通过String提供的方法来尝试修改也是要么创建一个新的字符串对象要么返回自己比如Stringstrabc;Stringstr1str.substring(1);其中str是一个字符串对象其值是“abc”通过substring方法又重新生成了一个字符串str1它的值是“bc”也就是说str引用的对象一旦产生就永远不会改变。为什么上面还说有可能不创建对象而返回自己呢那是因为采用str.substring(0)就不会创建新对象JVM会从字符串池中返回str的引用也就是自身的引用。StringBuffer是一个可变字符序列它与String一样在内存中保存的都是一个有序的字符序列char类型的数组不同点是StringBuffer对象的值是可改变的例如StringBuffersbnewStringBuffer(a);sb.append(b);从上面的代码可以看出sb的值在改变初始化的时候是“a”经过append方法后其值变成了“ab”。可能有读者会问了这与String类通过“”连接有什么区别例如Stringsa;ssb;有区别字符串变量s初始化时是“a”对象的引用经过加号计算后s变量就修改为了“ab”的引用但是初始化的“a”对象还是没有改变只是变量s指向了新的引用地址。再看看StringBuffer的对象它的引用地址虽不变但值在改变。StringBuilder与StringBuffer基本相同都是可变字符序列不同点是StringBuffer是线程安全的StringBuilder是线程不安全的翻翻两者的源代码就会发现在StringBuffer的方法前都有synchronized关键字这也是StringBuffer在性能上远低于StringBuilder的原因。在性能方面由于String类的操作都是产生新的String对象而StringBuilder和StringBuffer只是一个字符数组的再扩容而已所以String类的操作要远慢于StringBuffer和StringBuilder。弄清楚了三者的原理我们就可以在不同的场景下使用不同的字符序列了1使用String类的场景在字符串不经常变化的场景中可以使用String类例如常量的声明、少量的变量运算等。2使用StringBuffer类的场景在频繁进行字符串的运算如拼接、替换、删除等并且运行在多线程的环境中则可以考虑使用StringBuffer例如XML解析、HTTP参数解析和封装等。3使用StringBuilder类的场景在频繁进行字符串的运算如拼接、替换、删除等并且运行在单线程的环境中则可以考虑使用StringBuilder如SQL语句的拼装、JSON封装等。注意在适当的场景选用字符串类型。55、注意字符串的位置看这样一段程序publicstaticvoidmain(String[]args){Stringstr112 apples;Stringstr2apples:12;}想想看这两个字符串输出的苹果数量是否一致如果一致那是几个呢答案是不一致str1的值是“3 apples”str2的值是“apples:12”这中间悬殊很大只是把“apples”调换了一下位置为何会发生如此大的变化呢这都源于Java对加号的处理机制在使用加号进行计算的表达式中只要遇到String字符串则所有的数据都会转换为String类型进行拼接如果是原始数据则直接拼接如果是对象则调用toString方法的返回值然后拼接如strstrnewArrayList();上面就是调用ArrayList对象的toString方法返回值进行拼接的。再回到前面的问题上对于str1字符串Java的执行顺序是从左到右先执行12也就是算数加法运算结果等于3然后再与字符串进行拼接结果就是“3apples”其形式类似于如下计算Stringstr1(12) apples;而对于str2字符串由于第一个参与运算的是String类型加上1后的结果是“apples:1”这仍然是一个字符串然后再与2相加其结果还是一个字符串也就是“apples:12”。这说明如果第一个参数是String则后续的所有计算都会转变成String类型谁让字符串是老大呢注意在“”表达式中String字符串具有最高优先级。56、自由选择字符串拼接方法对一个字符串进行拼接有三种方法加号、concat方法及StringBuilder或StringBuffer由于StringBuffer的方法与StringBuilder相同文中不再赘述的append方法其中加号是最常用的其他两种方式偶尔会出现在一些开源项目中那这三者之间有什么区别吗我们来看下面的例子//加号拼接strc;//concat方法连接strstr.concat(c);上面是两种不同的字符串拼接方式循环5万次后再检查其执行的时间加号方式的执行时间是1438毫秒而concat方法的执行时间是703毫秒时间相差1倍如果使用StringBuilder方式执行时间会更少其代码如下publicstaticvoiddoWithStringBuffer(){StringBuildersbnewStringBuilder(a);for(inti0;i50000;i){sb.append(c);}Stringstrsb.toString();}StringBuffer的append方法的执行时间是0毫秒说明时间非常非常短暂毫秒不足以计时读者可以使用纳秒进行计算。这个实验也说明在字符串拼接方式中append方法最快concat方法次之加号最慢这是为何呢1“”方法拼接字符串虽然编译器对字符串的加号做了优化它会使用StringBuilder的append方法进行追加按道理来说其执行时间也应该是0毫秒不过它最终是通过toString方法转换成String字符串的例子中“”拼接的代码与如下代码相同strnewStringBuilder(str).append(c).toString();注意看它与纯粹使用StringBuilder的append方法是不同的一是每次循环都会创建一个StringBuilder对象二是每次执行完毕都要调用toString方法将其转换为字符串—它的执行时间就是耗费在这里了2concat方法拼接字符串我们从源码上看一下concat方法的实现代码如下publicStringconcat(Stringstr){intotherLenstr.length();//如果追加的字符串长度为0则返回字符串本身if(otherLen0){returnthis;}//字符数组容纳的是新字符串的字符charbuf[]newchar[countotherLen];//取出原始字符串放到buf数组中getChars(0,count,buf,0);//追加的字符串转化成字符数组添加到buf中str.getChars(0,otherLen,buf,count);//复制字符数组产生一个新的字符串returnnewString(0,countotherLen,buf);}其整体看上去就是一个数组拷贝虽然在内存中的处理都是原子性操作速度非常快不过注意看最后的return语句每次的concat操作都会新创建一个String对象这就是concat速度慢下来的真正原因它创建了5万个String对象呀3append方法拼接字符串StringBuilder的append方法直接由父类AbstractStringBuilder实现其代码如下publicAbstractStringBuilderappend(Stringstr){//如果是null值则把null作为字符串处理if(strnull)strnull;intlenstr.length();//字符串长度为0则返回自身if(len0)returnthis;intnewCountcountlen;//追加后的字符数组长度是否超过当前值if(newCountvalue.length)expandCapacity(newCount);//加长并做数组拷贝//字符串复制到目标数组str.getChars(0,len,value,count);countnewCount;returnthis;}看到没整个append方法都在做字符数组处理加长然后数组拷贝这些都是基本的数据处理没有新建任何对象所以速度也就最快了注意例子中是在最后通过StringBuffer的toString返回了一个字符串也就是说在5万次循环结束后才生成了一个String对象。三者的实现方法不同性能也就不同但并不表示我们一定要使用StringBuilder这是因为“”非常符合我们的编码习惯适合人类阅读两个字符串拼接就用加号连一下这很正常也很友好在大多数情况下我们都可以使用加号操作只有在系统性能临界如在性能“增之一分则太长”的情况下的时候才可以考虑使用concat或append方法。而且很多时候系统80%的性能是消耗在20%的代码上的我们的精力应该更多的投入到算法和结构上。注意适当的场景使用适当的字符串拼接方式。57、推荐在复杂字符串操作中使用正则表达式字符串的操作诸如追加、合并、替换、倒序、分割等都是在编码过程中经常用到的而且Java也提供了append、replace、reverse、split等方法来完成这些操作它们使用起来也确实方便但是更多的时候需要使用正则表达式来完成复杂的处理我们来看一个例子统计一篇文章中英文单词的数量很简单吧代码如下publicstaticvoidmain(String[]args){//接收键盘输入ScannerinputnewScanner(System.in);while(input.hasNext()){Stringstrinput.nextLine();//使用split方法分隔后统计intwordsCountstr.split( ).length;System.out.println(str 单词数wordsCount);}}使用split方法根据空格来分割单词然后计算分隔后的数组长度这种方法可靠吗可行吗我们来看输出TodayisMondayTodayisMonday单词数3TodayisMondayTodayisMonday单词数4TodayisMonday?No!TodayisMonday?No!单词数3ImOk.ImOk.单词数2注意看输出除了第一个输入“Todady is Monay”正确外其他都是错误的第二条输入中单词“Monday”前有2个连续的空格第三条输入中“NO”单词的前后都没有空格最后一个输入则没有把连写符号“”考虑进去这样统计出来的单词数量肯定错误一堆那怎么做才合理呢如果考虑使用一个循环来处理这样的“异常”情况会使程序的稳定性变差而且要考虑太多太多的因素这让程序的复杂性也大大提高了。那如何处理呢可以考虑使用正则表达式代码如下publicstaticvoidmain(String[]args){//接收键盘输入ScannerinputnewScanner(System.in);while(input.hasNext()){Stringstrinput.nextLine();//正则表达式对象PatternpatternPattern.compile(\\b\\w\\b);//生成匹配器Matchermatcherpattern.matcher(str);//记录单词数量intwordsCount0;//遍历查找匹配统计单词数量while(matcher.find()){wordsCount;}System.out.println(str 单词数wordsCount);}}准不准确我们来看相同的输入所产生的结果TodayisMondayTodayisMonday单词数3TodayisMondayTodayisMonday单词数3TodayisMonday?No!TodayisMonday?No!单词数4ImOk.ImOk.单词数3每项的输出都是准确的而且程序也不复杂先生成一个正则表达式对象然后使用匹配器进行匹配之后通过一个while循环统计匹配的数量。需要说明的是在Java的正则表达式中“\b”表示的是一个单词的边界它是一个位置界定符一边为字符或数字另外一边则非字符或数字例如“A”这样一个输入就有两个边界即单词“A”的左右位置这也就说明了为什么要加上“\w”它表示的是字符或数字。正则表达式在字符串的查找、替换、剪切、复制、删除等方面有着非凡的作用特别是面对大量的文本字符需要处理如需要读取大量的LOG日志时使用正则表达式可以大幅地提高开发效率和系统性能但是正则表达式是一个恶魔Regular Expressions is evil它会使程序难以读懂想想看写一个包含^、$、\A、\s、\Q、、、()、[]、{}等符号的正则表达式然后告诉你这是一个“这样这样……”的字符串查找你是不是要崩溃了这代码只有上帝才能看懂了注意正则表达式是恶魔威力巨大但难以控制。58、强烈建议使用UTF编码Java的乱码问题由来已久有点经验的开发人员肯定都遇到过乱码问题有时是从Web上接收的乱码有时是从数据库中读取的乱码有时是在外部接口中接收到的乱码文件这些都让我们困惑不已甚至是痛苦不堪看如下代码publicstaticvoidmain(String[]args)throwsException{Stringstr汉字;//读取字节byte[]bstr.getBytes(UTF-8);//重新生成一个新的字符串System.out.println(newString(b));}Java文件是通过IDE工具默认创建的编码格式是GBK大家想想看上面的输出结果会是什么可能是乱码吧两个编码格式不相同。我们暂不公布结果先解释一下Java中的编码规则。Java程序涉及的编码包括两部分1Java文件编码如果我们使用记事本创建一个.java后缀的文件则文件的编码格式就是操作系统默认的格式。如果是使用IDE工具创建的如Eclipse则依赖于IDE的设置Eclipse默认是操作系统编码Windows一般为GBK。2Class文件编码通过javac命令生成的后缀名为.class的文件是UTF-8编码的UNICODE文件这在任何操作系统上都是一样的只要是class文件就会是UNICODE格式。需要说明的是UTF是UNICODE的存储和传输格式它是为了解决UNICODE的高位占用冗余空间而产生的使用UTF编码就标志着字符集使用的是UNICODE。再回到我们的例子上getBytes方法会根据指定的字符集提取出字节数组这里按照UNICODE格式来提取然后程序又通过newString(byte[] bytes)重新生成一个字符串。来看看String这个构造函数通过操作系统默认的字符集解码指定的byte数组构造一个新的String。结果已经很清楚了如果操作系统是UTF-8编码的话输出就是正确的如果不是则会是乱码。由于这里使用的是默认编码GBK那么输出的结果也就是乱码了。我们再详细分解一下运行步骤步骤1 创建Client.java文件。该文件的默认编码GBK如果使用Eclipse则可以在属性查看到。步骤2 编写代码如上。步骤3 保存并使用javac编译。注意我们没有使用“javac-encoding GBK Client.java”显式声明Java的编码格式javac会自动按照操作系统的编码GBK读取Client.java文件然后将其编译成.class文件。步骤4 生成.class文件。编译结束生成.class文件并保存到硬盘上。此时.class文件使用的是UTF-8格式编码的UNICODE字符集可以通过javap命令阅读class文件。其中“汉字”变量也已经由GBK编码转变成UNICODE格式了。步骤5 运行main方法提取“汉字”的字节数组。“汉字”原本是按照UTF-8格式保存的要再提取出来当然没有任何问题了。步骤6 重组字符串。读取操作系统的编码格式GBK然后重新编码变量b的所有字节。问题就在这里产生了因为UNICODE的存储格式是两个字节表示一个字符注意这里是指UCS-2标准虽然GBK也是2个字节表示一个字符但两者之间没有影射关系要想做转换只能读取映射表不能实现自动转换—于是JVM就按照默认的编码格式GBK读取了UNICODE的两个字节。步骤7 输出乱码程序运行结束。问题清楚了解决方案也随之产生方案有两个。步骤8 修改代码。明确指定编码即可代码如下System.out.println(newString(b,UTF-8));步骤9 修改操作系统的编码方式。各个操作系统的修改方式不同不再赘述。我们可以把从字符串读取字节的过程看作是数据传输的需要比如网络、存储而重组字符串则是业务逻辑的需求这样就可使乱码现场重现通过JDBC读取的字节数组是GBK的而业务逻辑编码时采用的是UTF-8于是乱码产生了。对于此类问题最好的解决办法就是使用统一的编码格式要么都用GBK要么都用UTF-8各个组件、接口、逻辑层都用UTF-8拒绝独树一帜的情况。问题解释清楚了我们再来看以下代码publicclassClient{publicstaticvoidmain(String[]args)throwsException{Stringstr汉字;//读取字节byte[]bstr.getBytes(GB2312);//重新生成一个新的字符串System.out.println(newString(b));}}仅仅修改了读取字节的编码格式修改成了GB2312格式的结果会是怎样的呢又或者将其修改成GB18030结果又是怎样的呢结果都是“汉字”不是乱码。哈哈这是因为GB2312是中文字符集的V1.0版GBK是V2.0版本GB18030是V3.0版版本是向下兼容的只是它们包含的汉字数量不同而已注意UNICODE可不在这个序列之内的。注意一个系统使用统一的编码。59、对字符串排序持一种宽容的心态在Java中一涉及中文处理就会冒出很多问题来其中排序也是一个让人头疼的课题我们来看下面的代码publicstaticvoidmain(String[]args){String[]strs{张三(Z),李四(L),王五(W)};//排序默认是升序Arrays.sort(strs);inti0;for(Stringstr:strs){System.out.println((i)、str);}}上面的代码定义一个数组然后进行升序排序我们期望的结果是按照拼音升序排列即为李四、王五、张三但是结果却不是这样的1、张三(Z)2、李四(L)3、王五(W)这是按照什么排序的呀非常混乱我们知道Arrays工具类的默认排序是通过数组元素的compareTo方法来进行比较的那我们来看String类的compareTo的主要实现while(klim){//原字符串的字符数组charc1v1[k];//比较字符串的字符数组charc2v2[k];if(c1!c2){//比较两者的char值大小returnc1-c2;}k;}上面的代码先取得字符串的字符数组然后一个一个地比较大小注意这里是字符比较减号操作符也就是UNICODE码值的比较查一下UNICODE代码表“张”的码值是5F20而“李”是674E这样一看“张”排在“李”的前面也就很正确了—但这明显与我们的意图冲突了。这一点在JDK文档中也有说明对于非英文的String排序可能会出现不准确的情况。那该如何解决这个问题呢Java推荐使用Collator类进行排序那好我们把代码修改一下publicstaticvoidmain(String[]args)throwsException{String[]strs{张三(Z),李四(L),王五(W)};//定义一个中文排序器ComparatorcCollator.getInstance(Locale.CHINA);//升序排列Arrays.sort(strs,c);inti0;for(Stringstr:strs){System.out.println((i)、str);}}输出结果如下1、李四(L)2、王五(W)3、张三(Z)这确实是我们期望的结果应该举杯庆贺了吧但是且慢中国的汉字博大精深Java是否都能精确的排序呢最主要的一点是汉字中有象形文字音形分离是不是每个汉字都能按照拼音的顺序排列好呢我们写一个复杂的汉字来看看publicstaticvoidmain(String[]args)throwsException{String[]strs{犇(B),鑫(X)};Arrays.sort(strs,Collator.getInstance(Locale.CHINA));inti0;for(Stringstr:strs){System.out.println((i)、str);}}三个牛“犇”读bēn三个金“鑫”读xīn这两个字经常出现在饭店和商店的名称上我们来看排序的输出结果1、鑫(X)2、犇(B)输出结果又乱了不要责怪Java它已经尽量为我们考虑了只是因为我们的汉字文化太博大精深了要做好这个排序确实有点难为它。更深层次的原因是Java使用的是UNICODE编码而中文UNICODE字符集是来源于GB18030的GB18030又是从GB2312发展起来GB2312是一个包含了7000多个字符的字符集它是按照拼音排序并且是连续的之后的GBK、GB18030都是在其基础上扩充出来的所以要让它们完整排序也就难上加难了。如果是排序对象是经常使用的汉字使用Collator类排序完全可以满足我们的要求毕竟GB2312已经包含了大部分的汉字如果需要严格排序则要使用一些开源项目来自己实现了比如pinyin4j可以把汉字转换为拼音然后我们自己来实现排序算法不过此时你也会发现要考虑诸如算法、同音字、多音字等众多问题。注意如果排序不是一个关键算法使用Collator类即可。