【Java编程思想】13.字符串
字符串操作是计算机程序设计中最常见的行为。
13.1 不可变 String
String 对象是不可变的。String 类中每一个看起来会修改 String 值的方法,实际上都是创建了一个全新的 String 对象去包含修改后的字符串内容;而最初的 String 对象则没有改变。
每当吧 Stirng 对象作为方法的参数时,都会复制一份引用,而该引用所指的对象一直待在单一的物理位置上,从未动过。
13.2 重载 “+” 与 StringBuilder
操作符的重载的意思是,一个操作符在用于特定的类时,被赋予了特殊的意义。
用于 String 的 “+” 与 “+=” 是 Java 中仅有的两个重载过的操作符,而 Java 不允许程序员重载任何其他操作符。
使用 “+” 可以连接 String,但是原理上,大致是使用类似 append() 方法,生成新的 String 对象,以包含连接后的字符串。这种工作方式,期间涉及大量的中间对象生成与回收,会带来一定的性能问题。
但其实通过反编译后的字节码,我们可以知道在使用 “+” 的时候,编译器会自动引入 java.lang.StringBuilder 类,并使用 append() 方法,最后 toString() 转换拼接好的字符串。
可以看出编译器是会自动优化性能的,但是使用多个 Stirng 对象和操作符拼接和使用 Stringbuilder 有什么不同呢:
假设在循环内拼接字符串,编译后的字节码会显示,使用操作符的方式每次在循环体内部都会新建一个 Stringbuilder 对象,因此显而易见,使用操作符 “+” 进行重载的方式,对性能的消耗是比较大的。
13.3 无意识的递归
对于 ArrayList.toString(),他会遍历 ArrayList 中包含的所有对象,调用每个元素上的 toString() 方法。这就是一种无意识的递归。
public class InfiniteRecursion {
@Override
public String toString() {
return " InfiniteRecursion address: " + this + "\n";
}
}
对于上述代码,在其他类型的对象与字符串用 “+” 相连接的时候,会发生自动类型转换,这个时候会调用该对象的 toString() 方法,产生了有害的递归调用。这种情况下,如果真想打印对象的内存地址,应该调用 Object.toString() 方法,因此不应该使用 this,而是应该调用 super.toString() 方法。
13.4 String 上的操作
String 对象的一些基本方法:
| 方法 | 参数、重载版本 | 应用 |
|---|---|---|
| 构造器 | 重载版本、默认版本、String、StringBuilder、StringBuffer、char 数组、byte 数组 | 创建 String 对象 |
| length() | String 中字符的个数 | |
| charAt() | Int 索引 | 取得 String 中该索引位置上的 char |
| getChars()/getBytes() | 要复制部分的七点和终点的索引,复制的目标数组,目标数组的其实索引 | 复制 char 或 byte 到一个目标数组中 |
| toCharArray() | 生成一个 char[],包含 String 的所有字符 | |
| equals()/equalsIgnoreCase() | 与之进行比较的 String | 比较两个 String 的内容是否相同 |
| compareTo() | 与之进行比较的 String | 按词典顺序比较 String 的内容,比较结果为负数、零或正数。注意,大小写并不等价 |
| contains() | 要搜索的 CharSequence | 如果该 String 对象包含参数的内容,返回 true |
| contentEquals() | 与之进行比较的 CharSequence 或 StringBuffer | 如果该 String 与参数的内容完全一致,则返回 true |
| equalsIgnoreCase() | 与之进行比较的 String | 忽略大小写,如果两个 String 的内容相同,则返回 true |
| regionMatcher() | 该 String 的索引偏移量,另一个 String 及其索引偏移量,要比较的长度。重载版本增加忽略大小写功能 | 返回 boolean 结果,以表明所比较区域是否相等 |
| startsWith() | 可能的起始 String,重载版本在参数中增加了偏移量 | 返回 boolean 结果,以表明该 String 是否以此参数起始 |
| endsWith() | 该 String 可能的后缀 String | 返回 boolean 结果,以表明该 String 是否是该字符串的后缀 |
| indexOf()/lastIndexOf() | 重载版本包括:char;char 与起始索引;String;Stirng 与起始索引 | 如果该 String 并不包含此参数,就返回-1,否则返回此参数在 String 中的起始索引。lastIndexOf()是从后向前搜索 |
| subString()(subSequence()) | 重载版本:起始索引;起始索引+终点坐标 | 返回一个新的 Stirng,以包含参数指定的子字符串 |
| concat() | 要连接的 Stirng | 返回一个新的 String 对象,内容为原始 String 连接上参数 String |
| replace() | 要替换掉的字符,用来进行替换的新字符。也可以用一个 CharSequence 来替换另一个 CharSequence | 返回替换字符后的新 Stirng 对象,如果没有替换发生,则返回原始的 String 对象 |
| toLowerCase()/toUpperCase() | 将字符的大小写改变后,返回一个新 String 对象。如果没有改变发生,则返回原始的 String 对象 | |
| trim() | 将 String 两端的空白字符删除后,返回一个新的 String 对象。如果没有改变发生,则返回原始的 String 对象 | |
| valueOf() | 重载版本:Object;char[];char[],偏移量,与字符个数;boolean;char;int;long;float;double | 返回一个表示参数内容的 Stirng |
| intern() | 为每个唯一的字符序列生成一个且仅生成一个 String 引用 |
总体上来说,在要改变字符串内容时,String 类的方法都会返回一个新的 Stirng 对象;如果内容没有改变,String 的方法只是返回指向原对象的引用。
13.5 格式化输出
Java 中的 printf() 可以使用格式修饰符来连接字符串。
printf("Row 1: [%d %f]\n", x, y);
Java 中还提供了与 printf() 等价的 format() 方法。该方法可用于 PrintStream 或 PrintWriter 对象。
Java 中所有新的格式化功能都由 java.util.Formatter 处理,当创建一个 Formatter 对象的时候,需要向其构造器传递一些信息,告诉它最终的结果将向哪里输出。如下
Formatter f = new Formatter(System.out);
再插入数据时,如果想要更精确的控制格式,那么需要更复杂的格式修饰符。以下是其抽象的语法:
%[argument_index$][flags][width][.precision]conversion
其中
width 控制一个域的最小尺寸,width 可以用于各种类型的数据转换,并且其行为方式都一样。默认情况下数据右对齐,可以通过使用“-”标志来改变对齐方向。
precision 用来指明最大尺寸,并不是所有类型的数据都能使用 precision,而且应用于不同类型的数据转换时,precision 的意义也不同:对于 String 表示打印时输出字符的最大数量;对于浮点数表示小数部分要显示出来的位数(默认6位小数)位数过多舍入,过少则补零;而 precision 没法应用于整数。
常用的类型转换字符:
| 转换字符 | 描述 |
|---|---|
| d | 整数型(十进制) |
| c | Unicode 字符 |
| b | Boolean 值 |
| s | String |
| f | 浮点数(十进制) |
| e | 浮点数(科学计数) |
| x | 整数(十六进制) |
| h | 散列码(十六进制) |
| % | 字符“%” |
String.format() 是一个 static 方法,他接受与 Formatter.format() 方法一样的参数,但是返回一个 String 对象。
String.format("%05X: ", str);
使用上面的方法,可以以可读的十六进制格式将字节数组打印出来。
13.6 正则表达式
使用正则表达式,就能够以编程的方式,构造复杂的文本模式,并对输入的字符串进行搜索。
正则表达式提供了一种完全通用的方式,能够解决各种字符串处理相关的问题:匹配、选择、编辑以及验证。
String 中提供了正则表达式工具
split() 将字符串从正则表达式匹配的地方切开
replace() 只替换正则表达式第一个匹配对象
replaceAll() 替换正则表达式全部的匹配对象
? 可以用来描述一个要查找的字符串
+ 一个或多个之前的表达式
\\ 在正则表达式中插入一个普通的反斜线,因此\\d可以表示一个数字,\\w表示一个非单词小写字符,\\W表示一个非单词大写字符
| 或操作
例:
-?\\d+,表示“可能有一个负号,后面跟着一位或者多位的数字”。
(-|\\+)? 表示“可能以一个正号或者负号开头的字符串”
创建正则表达式
| 字符 | |
|---|---|
| B | 指定字符 B |
| \xhh | 十六进制值为 oxhh 的字符 |
| \uhhhh | 十六进制表示为 oxhhhh 的 Unicode 字符 |
| \t | 制表符 Tab |
| \n | 换行符 |
| \r | 回车 |
| \f | 换页 |
| \e | 转义(Escape) |
| 字符类 | |
|---|---|
| . | 任意字符 |
| [abc] | 包含 a、b 和 c 的任何字符(和 a|b|c 作用相同 |
| [^abc] | 除了 a、b 和 c 之外的任何字符(否定) |
| [a-zA-Z] | 从 a 到 z 或从 A 到 Z 的任何字符(范围) |
| [abc[hij]] | 任意 a、b、c、h、i 和 j 字符(与 a|b|c|h|i|j 作用相同)(合并) |
| [a-z&&[hij]] | 任意 h、i 或 j(交集) |
| \s | 空白符(空格、tab、换行、换页和回车) |
| \S | 非空白符([^\s]) |
| \d | 数字[0-9] |
| \D | 非数字[^0-9] |
| \w | 词字符[a-zA-Z0-9] |
| \W | 非词字符[^\w] |
| 逻辑操作符 | |
|---|---|
| XY | Y 跟在 X 后面 |
| X|Y | X 或 Y |
| (X) | 捕获组(capturing group)。可以在表达式中用 \i 引用第 i 个捕获组 |
| 边界匹配符 | |
|---|---|
| ^ | 一行的起始 |
| $ | 一行的结束 |
| \b | 词的边界 |
| \B | 非词的边界 |
| \G | 前一个匹配的结束 |
量词
量词描述了一个模式吸收输入文本的方式
- 贪婪型:量词总是贪婪的,除非有其他的选项被设置。贪婪表达式会为所有可能的模式发现尽可能多的匹配。
- 勉强型:用问号来指定,这个量词匹配满足模式所需的最少字符数。因此也可以视作“懒惰的、最少匹配的、非贪婪的、不贪婪的”。
- 占有型:该量词只在 Java 中可用。正常当正则表达式被应用于字符串时,它会产生相当多的状态,以便在匹配失败时可以回溯。而“占有型”量词并不保存这些中间状态,因此他们可以用来防止回溯,这个特性常用于防止正则表达式失控,因此可以使正则表达式执行起来更有效。
| 贪婪型 | 勉强型 | 占有型 | 如何匹配 |
|---|---|---|---|
| X? | X?? | X?+ | 一个或零个 X |
| X* | X*? | X*+ | 零个或多个 X |
| X+ | X+? | X++ | 一个或多个 X |
| X | X{n}? | X{n}+ | 恰好 n 次 X |
| X | X{n,}? | X{n,}+ | 至少 n 次 x |
| X | X{n,m}? | X{n,m}+ | X 至少 n 次,且不超过 m 次 |
表达式 X 通常必须使用圆括号括起来以免造成不必要的歧义。
接口 CharSequence 从 CharBuffer、String、StringBuffer、StringBuilder 类之中抽象出了字符序列的一般化定义。多数正则表达式操作都接受 CharSequence 类型的参数。
Pattern 和 Matcher
使用 static Pattern.compile() 方法来编译正则表达式,它会根据 String 类型的正则表达式生成一个 Pattern 对象。
接下来可以把想要检索的字符串传入 Pattern 对象的 matcher() 方法。该方法会生成一个 Matcher 对象,有很多种用法。
示例如下:
public class TestRegularExpression {
public static void main(String[] args) {
if (args.length < 2) {
print("Usage:\njava TestRegularExpression " +
"characterSequence regularExpression+");
System.exit(0);
}
print("Input: \"" + args[0] + "\"");
for (String arg : args) {
print("Regular expression: \"" + arg + "\"");
Pattern p = Pattern.compile(arg);
Matcher m = p.matcher(args[0]);
while (m.find()) {
print("Match \"" + m.group() + "\" at positions " +
m.start() + "-" + (m.end() - 1));
}
}
}
}
可以看到,Pattern 对象表示编译后的正则表达式,利用该对象上的 matcher() 方法加上一个输入字符串,即可构造出 Matcher 对象,用来进行相应的匹配或其他操作。
Pattern 类还提供:
matches()该方法完整为static boolean matches(String regex, CharSequence input),用以检查 regex 是否匹配整个 CharSequence 类型的 input 参数。split()该方法从匹配 regex 的地方分隔输入字符串,返回分割后的子字符串 String 数组。
Matcher 类提供:
boolean matches()判断整个输入字符串是否匹配正则表达式模式。boolean lookingAt()用来判断该字符串(不必是整个字符串)的始部分是否能匹配模式。boolean find()用来在CharSequence中查找多个匹配。boolean find(int start)
组(Group)是用括号划分的正则表达式。可以根据组的编号来引用整个组。组号为0表达整个表达式;组号为1表示被第一对括号括起的组,以此类推。
Matcher 类提供一系列方法用于获取与组相关的信息:
public int groupCount()返回该匹配器的模式中的分组数目,第0组不包括在内。public String group()返回前一次匹配操作(例如 find())的第0组(整个匹配)。public String group(int i)返回前一次匹配操作期间指定的组号,如果匹配成功,但指定的组没有匹配输入字符串的任何部分,则会返回 null。public int start(int group)返回在前一次匹配操作中寻找到的组的起始索引。public int end(int group)返回在前一次匹配操作中寻找到的组的最后一个字符索引加一的值。
Pattern 标记:Pattern 类的 compile() 方法还有另外一个版本,它接受一个标记参数,以调整匹配的行为。完整方法表达为:Pattern Pattern.compile(String regex, int flag)。
其中 flag 来自以下 Pattern 类中的常量:
| 编译标记 | 效果 |
|---|---|
| Pattern.CANON_EQ | 两个字符当且仅当他们完全规范分解相匹配时,就认为他们是匹配的。在默认情况下匹配不考虑规范的等价性 |
| Pattern.CASE_INSENSITIVE(?i) | 默认情况下,大小写不敏感的匹配假定只有 US-ASCII 字符集中的字符才能进行。这个标记允许模式匹配不必考虑大小写。通过指定 UNICODE_CASE 标记以及结合此标记,就可以开启基于 Unicode 的大小写不敏感匹配 |
| Pattern.COMMENTS(?x) | 在这种模式下空格符会被忽略,并且以#开始直到行末的注释也会被忽略。通过嵌入的标记表达式也可以开启 Unix 的行模式 |
| Pattern.DOTALL(?s) | 在 dotall 模式中,表达式 "." 匹配所有字符,包括行终结符。默认情况下 "."不匹配行终结符 |
| Pattern.MULTILINE(?m) | 在多行模式下,表达式^和\(分别匹配一行的开始和结束。^还匹配输入字符串的开始,\)还匹配输入字符串的结尾。默认情况下,这些表达式只匹配输入的完整字符串的开始和结束 |
| Pattern.UNICODE_CASE(?a) | 当指定这个标记,并且 开启 CASE_INSENSITIVE 时,大小写不敏感的匹配将按照与 Unicode 标准相一致的方式进行。默认情况下,大小写不敏感的匹配假定只有 US-ASCII 字符集中的字符才能进行。 |
| Pattern.UNIX_LINES(?d) | 这种模式下,在 ./^/$ 的行为中,只识别行终结符 \n |
示例:
Pattern p = Pattern.compile("^java",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
split() 方法将输入字符串断开成字符串对象数组,断开边界有下列正则表达式确定:
String[] split(CharSequence input)String[] split(CharSequence input, int limit)限制了将输入分割成字符串的数量
替换操作
replaceFirst(String replacement)以参数字符串 replacement 替换掉第一个匹配成功的部分。replaceAll(String replacement)以参数字符串 replacement 替换所有匹配成功的部分。appendReplacement(StringBuffer sbuf, String replacement)执行渐进式替换,它允许你调用其他方法来生成或处理 replacement,使你能够以编程的方式将目标分割成组。appendTail(StringBuffer sbuf)在执行一次或多次appendReplacement()之后,调用此方法可以将输入字符串余下的部分复制到 sbuf 中。
通过 reset() 方法可以将现有的 Matcher 对象应用于一个新的字符序列。使用不带参数的 reset() 方法可以将 Matcher 对象重新设置到当前字符序列的起始位置。
13.7 扫描输入
Java SE5中新增了 Scanner 类,可以用于扫描输入工作。
Scanner 的构造器可以接受任何类型的输入对象,包括 File、InputStream、String 或 Readable 等。Readable 接口表示”具有 read() 方法的某种东西“。
对于 Scanner,所有的输入、分词以及翻译操作都隐藏在不同类型的 next() 方法中,所有基本类型(除 char 之外)都有对应的 next() 方法。对于所有的 next() 方法,只有找到一个完整的分词之后才会返回。Scanner 也有相应的 hasNext() 方法,用来判断下一个输入分词是否为所需类型。
默认情况下,Scanner 根据空白字符对输入进行分词,但是也可以用正则表达式指定自己所需的定界符。
13.8 StringTokenier
在 Java 引入正则表达式(J2SE1.4)和 Scanner 类(Java SE5)之前,使用 StringTokenier 来进行分词。
下面是两者的比较:
public class ReplacingStringTokenizer {
public static void main(String[] args) {
String input = "But I'm not dead yet! I feel happy!";
StringTokenizer stoke = new StringTokenizer(input);
while (stoke.hasMoreElements())
System.out.print(stoke.nextToken() + " ");
System.out.println();
System.out.println(Arrays.toString(input.split(" ")));
Scanner scanner = new Scanner(input);
while (scanner.hasNext())
System.out.print(scanner.next() + " ");
}
}
输出:
But I'm not dead yet! I feel happy!
[But, I'm, not, dead, yet!, I, feel, happy!]
But I'm not dead yet! I feel happy!
StringTokenier 已经基本废弃了。

浙公网安备 33010602011771号