【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
已经基本废弃了。