大量程序表明,字符串操作是程序设计中的最基本操作。
一、不可变String
String对象是不可变的,每一个看似修改了String值的方法,实际都是产生了一个新的字符串。
1 | package com.chenxyt.java.practice; |
运行结果:
当把q传给upCase方法时,实际上传递的是引用的一个拷贝。其实每当String对象作为参数传递时,传递的都是一个拷贝。再看upCase方法,只有当该方法运行时,局部引用s才存在,一旦该方法结束,引用s就消失了。该方法的返回值,实际上是最终值的引用,也就是upCase返回的引用已经指向了一个新的对象,而原本的q并没有发生任何变化。比如如下的方法,我们并不希望在经过一个操作之后,改变原有的对象。
1 | String s = "abcde"; |
因为方法的参数是用来传递信息的,而不是用来改变原有对象本身的。
二、重载“+”与StringBuilder
String对象是不可变的,所以指向它的任何引用都不会改变该对象的值。不可变性会对效率带来一个问题,为String对象重载“+”操作符就是一个例子,重载的意思是一个操作符应用与特定的类时被赋有特殊的意义。(用于String的“+”和”+=”是Java中仅有的两个重载过的操作符,Java不允许程序员自己重载操作符)
操作符“+”可以用来连接两个String
1 | package com.chenxyt.java.practice; |
运行结果:
上述代码的运行过程可能是这样的:String可能有一个append方法,然后它会生成一个新的String对象用来连接abc和mango,然后该对象再与def相连生成新的对象,依次类推。这样做的话会产生很多中间垃圾需要清理因此它效率极低。
我们使用JDK自带的反编译工具查看上述代码如何工作:
我们代码中并没有使用StringBuilder类,然而编译器却自动的引入了StringBuilder类,从编译后的代码可以看出,字符串连接工作主要的操作是编译器创建一个StringBuilder对象,然后调用该对象的append()方法将所有的要连接的字符串连接到后边,最后调用toString()方法转换成String对象存给s。
因此在编写一个类似toString()的方法时,如果字符串较短时,我们可以使用普通的拼接方式,当字符串操作较为复杂的时候,我们在代码中直接创建一个StringBuilder对象进行操作效率会更加优异。
三、无意识的递归
我们希望使用toString()方法打印出对象的内存地址,那么我们可能会考虑使用this关键字:
1 | package com.chenxyt.java.practice; |
运行结果:
这里当运行
1 | return "InfiniteRecursion address" + this; |
时发生了类型转换,编译器发现“+”后边不是String类型,会试图转换成String类型,转换的方式就是调用toString方法,因此会递归调用,此处如果想正确打印地址,那么需要使用其基类Object的toString()方法。
四、String上的操作
所有的类都是Class类的对象,使用Class类的getMethods()方法可以返回该类的所有方法:
1 | package com.chenxyt.java.practice; |
运行结果:
1 | public int java.lang.String.hashCode() |
注释了一些常用的方法,其它的方法可自行查阅。
五、格式化输出
JavaSE5提供了格式化输出功能,这一功能使得控制输出的功能变得更加简单,同时也给开发者带来了更加强大的代码输出控制能力。JavaSE5引入的format方法可以用于PrintStream或PrintWriter对象,其中也包括System.out对象。format()方法模仿在C语言的printf()如下简单示例:
1 | package com.chenxyt.java.practice; |
运行结果:
可以看到,format()与printf()相同,只需要加上对应的格式化字符即可。
Java中所有新的格式化功能都由java.util.Formatter类处理,可以将其看做是一个翻译器,它将你的格式化字符串和数据翻译成想要的结果。当你创建了一个Formatter对象的时候,需要向编译器传递一些信息,告诉他最终的结果将向哪里输出。
1 | package com.chenxyt.java.practice; |
运行结果:
如上我们指定了格式化输出的结果分别打印到了System.out和System.err上
有的时候我们希望做一些更精致的格式化信息,比如控制空格与对齐,最常见的是控制域的最小尺寸,这可以通过指定width实现,Formatter对象通过在必要时添加空格,来确保一个域至少达到某个长度。默认情况下域是右对齐的,不过也可以通过“-”指定数据的对齐方式。
1 | package com.chenxyt.java.practice; |
运行结果:
可以看到Formatter类提供了很强大的格式化支持
Formatter类也有很多类型转换,常用的类型转换如下
d:整数型(十进制)e:浮点数(科学计数)c:Unicode字符 x:整数(十六进制)b:Boolean值 h:散列码(十六进制)s:String %:字符“%”f:浮点数(十进制),针对不同的数据类型,有些转换是无效的,如果强制使用则会发生异常。
六、正则表达式
正则表达式就是以某种方式来描述字符串,因此你可以说“如果一个字符串含有某些东西,那么它就是我需要的东西”。\d表示一位数字,Java中对于\反斜线有不同的处理,在其它语言中,\表示我想插入一个普通的反斜线,而在Java中\表示我将要插入一个正则表达式反斜线,所以它后边的值应该具有特殊意义。例如你想表示一个数字,那么就是\d,如果想插入一个普通的反斜线,则需要使用\\,不过制表之类的应该使用单反斜线,\d\n,使用?表示可能存在,使用+表示一个或多个+之前的表达式。比如我们要判断可能有一个负号后边跟着一个或多个数字则使用:
-?\d+
应用正则的最简单方式是使用String类提供的方法:
1 | package com.chenxyt.java.practice; |
运行结果:
前两个满足正则表达式的结果,第三个“+“作为一个独立的符号,所以与”可能有个负号开头的一个或多个整数“不匹配,应该使用”可能有一个正号或者符号开头的一个或多个整数“,括号有着分组的作用,|表示或。
String类还带有一个非常有用的正则表达式方法split(),功能是将字符串在正则表达式匹配的地方分割开:
1 | package com.chenxyt.java.practice; import java.util.Arrays; public class Splitting { public static String knights = "Then。when you have found the shrubbery.you must" + "cut down the mightiest tree in the forest。。。" + "with... a herring"; public static void split(String regex){ System.out.println(Arrays.toString(knights.split(regex))); } public static void main(String[] args) { split(" "); split("\\W+"); |
运行结果:
第一个是使用空格拆分,\W表示非单词字符,\w表示单词字符,第三个表示字母n以及后边的一个或多个非单词字符。可见与正则表达式相同的内容都消失了。
String类自带的最后一个正则表达式方法为替换,你可以替换正则表达式第一个匹配的子串,也可以替换所有匹配的地方。
1 | public class Replacing { |
运行结果:
第一个正则表示以字母f开头后面跟着一个或多个字母的,将第一个匹配的子串替换成“located”,第二个表达式是替换三个单词中的任意一个,只要存在则替换。
我们也可以自己创建正则表达式,java.util.regex.Pattern类提供了完整的正则表达式列表,先列举一些常用的:
以下是一些字符类创建的典型方式,以及一些预定义类。
作为演示,下面的每一个正则表达式都可以成功的匹配“Rudolph“
1 | package com.chenxyt.java.practice; |
运行结果:
我们的任务是在能够完成任务的情况下编写简单的易于理解的正则表达式,而不是编写难以理解的正则表达式。
我们在使用正则表达式的时候很容易混淆,因为他是一种在Java之上的新语言,CharSequence,该接口从CharBuffer、String、StringBuffer、StringBuilder类之中抽象出了字符序列的一般化定义。
1 | package com.chenxyt.java.practice; |
因此,这些类都实现了该接口,多数正则表达式操作都接受CharSequence类型的参数。
String类作为正则表达式的匹配使用功能有限,因此我们可以使用更为强大的正则表达式对象。只需要导入java.util.regex包,然后使用Pattern.compile()方法编译正则表达式。它会根据传入的String类型的正则表达式生成一个Pattern对象,接下来把我们想要检索的字符串传入Pattern.matcher()方法,该方法会生成一个Matcher对象,它有很多功能可以使用。
以下为演示示例,第一个控制台参数为要匹配的字符串,后边一个或多个参数都为正则表达式:
1 | package com.chenxyt.java.practice; |
运行结果:
Matcher.find()方法用来查找字符串中的多个匹配。start()方法返回匹配的起始位置的索引,end()方法匹配的终止位置的索引。
七、扫描输入
目前来说,读取文本或者从标准输入读取数据一般的解决方法是读入一行文本,然后对其进行分词,然后使用Integer、Double的各种解析方法来解析数据。
1 | package com.chenxyt.java.practice; |
运行结果:
以上代码中,使用了IO中的readLine()方法读取输入流中的一行,然后进行解析。终于在JavaSE5新增了Scanner类,它可以大大的减轻扫描输入的工作。
1 | package com.chenxyt.java.practice; |
运行结果:
Scanner的构造器可以接受任意类型的输入对象,其普通的next方法将返回一个String,而所有的基本类型都有一个next方法,该方法的作用是,读取到下一个完整的指定类型的分词之后才返回。同时Scanner还有对应的hasNext方法,用来判断是否有所输入分词的类型。
Scanner默认的定界符为空白符,我们也可以自己使用正则表达式指定分隔符:
1 | package com.chenxyt.java.practice; |
运行结果:
这个例子使用逗号以及任意的空白字符来实现字符串的分割。使用useDelimiter()方法设置定界符,也可以使用delimiter()方法返回现在所使用的定界符。
Scanner除了可以扫描基本类型外,还可以扫描正则表达式,这在扫描复杂数据类型的时候非常有用:
1 | package com.chenxyt.java.practice; |
运行结果:
当next()方法配合指定正则表达式使用时,将找到下一个匹配该模式的输入部分,调用match()方法获得匹配的结果。有一点需要注意,它只针对下一个输入分词进行匹配,如果正则表达式含有定界符,那将永远不能成功。
八、StringTokenizer
在正则表达式和Scanner引入之前,我们使用的是StringTokenizer来进行字符串匹配,下面演示这种方式并与其它方式进行比较:
1 | package com.chenxyt.java.practice; |
运行结果:
使用正则表达式或者Scanner我们可以使用更加复杂的模式匹配字符串,而使用StringTokenizer则很困难了,因此实际上StringTokenizer这个类基本上已经被废弃了。
九、总结
String类型作为程序设计中最为常见的一种操作类型,它是一种不可变的操作类型。它内部重载了“+”操作符,使得可以使用”+”操作符完成字符串的拼接工作,拼接的原理是编译器为我们自动生成了StringBuilder类来完成。因此对于复杂的字符串拼接操作,我们自己使用StringBuilder效率会更高一些。String还有很多常用的方法,正则表达式为我们提供了字符串匹配的多种形式。