第十三章 字符串
13.1 不可变String
String对象是不可变的(不能够原地修改),具备只读特性。String类中每一个修改String值的方法,实际上是创建了一个新的String对象。
当把String对象作为方法的参数时,都会复制一份引用,而该引用所指向的对象其实一直呆在单一的物理位置上,从未移动过。
13.2 重载”+”与StringBuilder
String对象的这种只读特性,在进行字符串连接时会产生性能问题。例如:
package com.mzm.chapter13;
/**
*
*/
public class Concatenation {
public static void main(String[] args){
String mango = "mango";
String s = "abc" + mango + "def" + 47;
System.out.println(s);
}
}
在上述代码中,在进行字符串连接时,首先是”abc”和”mango”这两个字符串常量连接,创建一个”abcmango”字符串常量,之后是”abcmango”和”def”这两个字符串常量连接,创建一个”abcmangodef”字符串常量,最后是”abcmangodef”与数字常量47的连接,最终创建出”abcmangodef47”的字符创常量,可见在使用”+”重载符进行字符串连接时,会产生大量需要垃圾回收的中间对象。
但是在JVM会在底层调用StringBuilder类进行优化。
当为类编写toString()方法时,建议使用StringBuilder。
13.3 无意识的递归
package com.mzm.chapter13;
import java.util.ArrayList;
import java.util.List;
/**
* 打印对象的内存地址
*/
public class InfiniteRecursion {
public String toString() {
//此处不能使用this,因为存在自动类型转换,会调用this的toString()方法,这样就形成了递归调用
//return " InfiniteRecursion address: " + this + "\n";
//应该使用Object类的toString(),即super.toString()
return " InfiniteRecursion address: " + super.toString() + "\n";
}
public static void main(String[] args) {
List<InfiniteRecursion> v = new ArrayList<>();
for (int i = 0; i < 10; i++) {
v.add(new InfiniteRecursion());
}
System.out.println(v);
}
}
13.4 String上的操作
方法 | 参数,重载版本 | 应用 |
---|---|---|
构造器 | 重载版本:默认版本,String,StringBuilder/StringBuffer,char数组,byte数组 | 创建String对象 |
length() | String中字符的个数 | |
charAt() | Int索引 | 取得String中该索引位置上的char |
getChars()/getBytes() | 要复制部分的起点和终点的索引,复制的目标数组,目标数组的起始索引 | 复制char或byte到一个目标数组中 |
toCharArray() | 生成一个char[],包含String中的所有字符 | |
equals()/equalsIngoreCase() | 与之进行比较的String | 比较两个String的内容是否相同 |
compareTo() | 与之进行比较的String | 按词典顺序比较String的内容,比较结果为负数、零或正数。注意,大小写不等价 |
contains() | 要搜索的CharSequence | 如果该String对象包含参数的内容,则返回true |
containEquals() | 与之进行比较的CharSequence或StringBuffer | 如果该String与参数的内容完全一致,则返回true |
equalsIgnoreCase() | 与之进行比较的String | 忽略大小写,如果两个String的内容相同,则返回true |
regionMatcher() | 该String的索引偏移量,另一个String及其索引偏移量,要比较的长度。重载版本增加了”忽略大小写”功能 | 返回boolean结果,以表明所比较区域是否相等 |
startWith() | 可能的起始String。重载版本在参数中增加了偏移量 | 返回boolean结果,以表明该String是否以此参数起始 |
endWith() | 该String可能的后缀String | 返回boolean结果,以表明次蚕食是否是该字符串的后缀 |
indexOf()/lastIndexOf() | 重载版本包括:char,char与起始索引,String,String与起始索引 | 如果该String并不包含此参数,则返回-1;否则返回此参数在String中的起始索引。lastIndexOf()是从后向前搜索 |
substring()/subSequence() | 重载版本:起始索引;起始索引+终点坐标 | 返回一个新的String,以包含参数指定的子字符串 |
concat() | 要连接的String | 返回一个新的String对象,内容为原始String连接上参数String |
replace() | 要替换掉的字符,用来进行替换的新字符。也可以用一个CharSequence来替换另一个CharSequence | 返回替换后的新String对象。如果没有替换发生,则返回原始的String对象 |
toLowerCase()/toUpperCase() | 将字符的大小写改变后,返回一个新的String对象。如果没有改变发生,则返回原始的String对象 | |
trim() | 将String两端的空白字符删除后,返回原始的String对象 | |
valueOf() | 重载版本:Object;char[];char[],偏移量,与字符个数;boolean;char;int;long;float;double | 返回一个表示参数内容的String |
intern() | 为每个唯一的字符序列生成一个且仅生成一个String引用 |
13.5 格式化输出
13.5.1 printf()
与C语言的printf()类似。
格式修饰符:表明插入数据的位置和插入数据的类型。
13.5.2 System.out.format()
等价于printf()
13.5.3 Formatter类
在Java中,所有新的格式化功能都由java.util.Formatter类处理。创建Formatter对象可以指定输出流,表示格式化后的结果最终输出地。Formatter对象的format()方法即与前面提到的System.out.printf()和System.out.format()用法相同。
13.5.4 格式化说明符
精细复杂的格式化修饰符:
%[argument_index$][flags][width][.precision]conversion
width用来控制最小尺寸,不足时添加空格。
默认是右对齐,可以使用-改变对其方式。
precision用来控制最大尺寸。当precision用于String时,表示打印String时输出字符的最大数量。当precision用于浮点数时,表示小数部分显示的位数,多则舍入,少则补零。但是precision不能用于整数,否则会抛异常。
13.5.5 Formatter转换
类型转换字符 | |
---|---|
d | 整数型(十进制) |
c | Unicode字符 |
b | Boolean值 |
s | String |
f | 浮点数(十进制) |
e | 浮点数(科学计数) |
x | 整数(十六进制) |
h | 散列码(十六进制) |
% | 字符”%” |
13.5.6 String.format()
String.format()参照了C语言的sprintf()方法,生成格式化的String对象,其参数与Formatter.format()一样,只是返回一个String对象。
其实在String.format()方法内部,也是创建一个Formatter对象,然后参数传给Formatter对象进行处理。
13.6 正则表达式
13.6.1 基础
String对象自带的match()方法,可以判断该字符串是否匹配指定的正则表达式。
String对象的split()方法,可以传入指定的正则表达式,表示将该字符串按照给定的正则表达式分割,其还有一个重载版本,允许传入第二个参数,来限定字符串分割的次数。
String对象的replace()/replaceFirst()/replaceAll()等方法,第一个参数可以是正则表达式,第二个参数为作为替换的字符串。
13.6.2 创建正则表达式
字符 | |
---|---|
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或jh(交) |
\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 | 前一个匹配的结束 |
13.6.3 量词
量词描述了一个模式吸收文本的方式:
- 贪婪型:量词总是贪婪的,除非有其他的选项被设置。贪婪表达式会为所有可能的模式发现尽可能多的匹配。
- 勉强型:用问号指定,这个量词匹配满足模式所需的最少字符数。
- 占有型:目前只存在于Java。当正则表达式被应用于字符串时,它会产生相当多的状态,以便在匹配失败时可以回溯。不常用。
贪婪型 | 勉强型 | 占有型 | 如何匹配 |
---|---|---|---|
X? | X?? | X?+ | 一个或零个X |
X* | X*? | X*+ | 零个或多个X |
X+ | X+? | X++ | 一个或多个X |
X{n} | X{n}? | X{n}+ | 恰好n次X |
X{n,} | X{n,}? | X{n,}+ | 至少n次X |
X{n,m} | X{n,m}? | X{n,m}+ | X至少n次,且不超过m次 |
注意X通常必须用括号括起。
13.6.4 Pattern和Matcher
package com.mzm.chapter13;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 测试正则表达式是否匹配
* 第一个参数是要匹配的字符串
* 之后的参数都是正则表达式
*
*/
public class TestRegularExpression {
public static void main(String[] args){
if(args.length < 2){
System.out.println("Usage:\njava TestRegularExpression characterSequence regularExpression+");
System.exit(0);
}
System.out.println("Input: \"" + args[0] + "\"");
for(String arg : args){
System.out.println("Regular expression: \"" + arg + "\"");
Pattern p = Pattern.compile(arg);
Matcher m = p.matcher(args[0]);
while(m.find()){
System.out.println("Match \"" + m.group() + "\" at positions " + m.start() + "-" + (m.end() - 1));
}
}
}
}
先使用Pattern.compile()传入要正则表达式来创建Pattern对象,之后使用该Pattern对象的matcher()方法,传入要匹配的字符串,获得Matcher对象。Pattern类还有提供了静态的matches()方法:
static boolean matches(String regex, CharSequence input)
(CharSequence是CharBuffer、String、StringBuffer和StringBuilder这四个类抽象出来的字符序列接口)
该方法检查regex是否匹配整个CharSequence类型的input参数。Pattern对象还有split()方法,与String对象的split()方法类似。
获取Matcher对象后,可以使用Matcher上的方法:
boolean mathes()
boolean lookingAt()
boolean find()
boolean find(int start)
matches()方法判断整个字符串是否匹配正则表达式模式;
lookingAt()方法则表示该字符串的起始部分是否匹配正则表达式,无需整个字符串匹配。
find()方法可用来在CharSequence中查找多个匹配。
package com.mzm.chapter13;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
*
*/
public class Finding {
public static void main(String[] args){
Matcher m = Pattern.compile("\\w+").matcher("Evening is full of the linnet's wings");
while(m.find()){
System.out.print(m.group()+ " ");
}
System.out.println();
int i = 0;
while(m.find(i)){
System.out.print(m.group() + " ");
i++;
}
}
}
模式\w+将字符串划分为单词,find()匹配每一个单词。find(int start)表示从该字符串的start位置开始匹配单词。
组(Group)
组是用括号划分的正则表达式,可以根据组的编号来引用某个组。组号为0表示整个正则表达式,组号为1表示被第一对括号括起的组,依此类推。
Matcher对象关于组的方法:
public int groupCount():返回该匹配器的模式中的分组数目,不包括第0组;
public String group():返回前一次匹配操作的第0组(整个匹配);
public String group(int i):返回在前一次匹配操作期间指定的组号,如果匹配成功,但指定的组没有匹配输入字符串的任何部分,则返回null;
public int start(int group):返回在前一次匹配操作中寻找到的组的起始索引;
public int end(int group):返回在前一次匹配操作中寻找到的组的最后一个字符索引加一的值。
package com.mzm.chapter13;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
*
*/
public class Groups {
static public final String POEM =
"Twas brillig, and the slithy toves\n" +
"Did gyre and gimble in the wabe.\n" +
"All mimsy were the borogoves,\n" +
"And the mome raths outgrabe.\n\n" +
"Beware the Jabberwock, my son,\n" +
"The jaws that bite, the claws that catch.\n" +
"Beware the Jubjub bird, the claws that catch.\n" +
"The frumious Bandersnatch.";
public static void main(String[] args){
//正则表达式匹配的是每一行的最后三个单词
//?m表示标记模式,指定$表示的是每一行的末尾
Matcher m = Pattern.compile("(?m)(\\S+)\\s+((\\S+)\\s+(\\S+))$").matcher(POEM);
while(m.find()){
for(int j = 0; j <= m.groupCount(); j++){
System.out.print("[" + m.group(j) + "]");
}
System.out.println();
}
}
}
start()和end()
在匹配成功后,start()返回先前匹配的起始位置的索引,而end()返回所匹配的最后字符的索引加一的值。而匹配失败,调用start()或end()都会抛出IllegalStateException异常。
package com.mzm.chapter13;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Created by 蒙卓明 on 2017/11/4.
*/
public class StartEnd {
public static String input =
"As long as there is injustice, whenever a\n" +
"Targathian baby cries out, wherever a distress\n" +
"signal sounds among the stars ... We'll be there.\n" +
"This fine ship, and this fine crew ...\n" +
"Never give up! Never surrender!";
private static class Display{
private boolean regexPrinted = false;
private String regex;
Display(String regex){
this.regex = regex;
}
void display(String message){
if(!regexPrinted){
System.out.println(regex);
regexPrinted = true;
}
System.out.println(message);
}
}
static void examine(String s, String regex){
Display d = new Display(regex);
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(s);
while(m.find()){
d.display("find() '" + m.group() + "' start = " + m.start() + " end = " + m.end());
}
if(m.lookingAt()){
d.display("lookingAt() start = " + m.start() + " end = " + m.end());
}
if(m.matches()){
d.display("matches() start = " + m.start() + " end = " + m.end());
}
}
public static void main(String[] args){
for(String in : input.split("\n")){
System.out.println("input : " + in);
for(String regex : new String[]{"\\w*ere\\w*", "\\w*ever", "T\\w+", "Never.*?!"}){
examine(in, regex);
}
}
}
}
find()可以在输入的任意位置开始匹配正则表达式;
lookingAt()只能在字符串的开头开始匹配,但不需要整个字符串匹配成功;
matches()也是只能在字符串的开头开始匹配,且需要整个字符串匹配成功。
Pattern标记
Pattern类还有一个complie()方法,接受一个标记参数。
Pattern Pattern.compile(String regex, int flag)
其中flag来自于以下Pattern类常量:
编译标记 | 效果 |
---|---|
Pattern.CANON_EQ | 两个字符当且仅当它们的完全规范分解相匹配时,就认为它们是匹配的。例如,如果我们指定这个标记,表达式a\u030A就会匹配字符串?。默认的情况下,匹配不考虑规范的等价性。 |
Pattern.CASE_INSENSITIVE(?i) | 默认情况下,大小写不敏感的匹配假定只有US-ASCII字符集中的字符才能进行。这个标记模式不必考虑大小写。与UNICODE_CASE结合,开启基于Unicode的大小写不敏感的匹配 |
Pattern.COMMENTS(?x) | 在这种模式下,空格符将被忽略掉,并且以#开始直到行末的注释也会被忽略掉。通过嵌入标记表达式也可以开启Unix的行模式 |
Pattern.DOTALL(?s) | 在dotall模式中,表达式”.”匹配所有字符,包括行终止符。默认情况下,”.”表达式不匹配行终止符 |
Pattern.MULTILINE(?m) | 在多行模式下,表达式^和 分别匹配一行的开始和结束,同时还匹配输入字符串的开始, 分 别 匹 配 一 行 的 开 始 和 结 束 , 同 时 还 匹 配 输 入 字 符 串 的 开 始 , 还匹配输入字符串的结束。在默认模式下,^和$分别只匹配字符串的开始和结束 |
Pattern.UNICODE_CASE(?u) | 当指定这个标记,并且开启CASE_INSENSITIVE时,大小写不敏感的匹配将按照与Unicode标准相一致的方式进行。默认情况下,大小写不敏感的匹配假定只能在US-ASCII字符集中的字符才能进行 |
Pattern.UNIX_LINES(?d) | 在这种情况下,在.、^和$行为中,只识别行终结符\n |
Pattern.CASE_INSENSITIVE、Pattern.MULTILINE和Pattern.COMMENTS比较有用。
package com.mzm.chapter13;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
*
*/
public class ReFlags {
public static void main(String[] args){
Pattern p = Pattern.compile("^java", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
Matcher m = p.matcher(
"java has regex\nJava has regex\n" +
"JAVA has pretty good regular expressions\n" +
"Regular expressions are in Java"
);
while(m.find()){
System.out.println(m.group());
}
}
}
13.6.5 split()
Pattern类的split()分割字符串,断开边界由正则表达式确定:
String[] split(CharSequence input)
String[] split(CharSequence input, int limit)
13.6.6 替换操作
replaceFirst(String replacement)以参数字符串replacement替换掉第一个匹配成功的部分;
replaceAll(String replacement)以参数字符串replacement替换所有匹配成功的部分;
appendReplacement(StringBuffer sbuf, String replacement)执行渐进式替换,该方法允许调用其他方法来生成或处理replacement。
appendTail(StringBuffer sbuf)在执行一次或多次appendReplacement()后,调用此方法可以将输入的字符串余下的部分复制到sbuf中。
package com.mzm.chapter13;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* appendReplacement()的用法
*/
public class TheReplacements {
public static void main(String[] args){
String s =
"/*! Here's a block of text to use as input to\n" +
" the regular expression matcher. Note that we'll\n" +
" first extract the block of text by looking for\n" +
" the special delimiters, then process the \n" +
" extracted block. !*/";
Matcher mInput = Pattern.compile("/\\*!(.*)!\\/", Pattern.DOTALL).matcher(s);
if(mInput.find()){
s = mInput.group(1);
}
s = s.replaceAll(" {2,}", " ");
s = s.replaceAll("(?m)^ +", "");
System.out.println(s);
s = s.replaceFirst("[aeiou]", "(VOWEL1)");
StringBuffer sbuf = new StringBuffer();
Pattern p = Pattern.compile("[aeiou]");
Matcher m = p.matcher(s);
while(m.find()){
//每捕获一个组,用另一个字符串替换(这里是捕获组的大写)
//每进行一次替换,将结果存入一个StringBuffer对象中
m.appendReplacement(sbuf, m.group().toUpperCase());
}
//将输入字符串的剩余部分复制到StringBuffer对象中
m.appendTail(sbuf);
System.out.println(sbuf);
}
}
13.6.7 reset()方法
reset()方法可以将现有的Matcher对象应用于新的字符序列
13.6.8 正则表达式与Java IO
13.7 扫描输入
Scanner的构造器可接受任何类型的输入对象,包括File、InputStream、String或者Readable对象。所有的输入、分词以及翻译的操作都隐藏在不同类型的next方法中。next()返回下一个String,除char之外,所有的基本类型都有对应的next方法,包括BigInteger和BigDecimal。hasNext()方法判断下一个输入分词是否所需的类型。
Scanner类接受输入时,无需显式的抛出IOException,因为Scanner假设输入结束就会抛出IOExceotion,并将该异常吞掉。但可以通过ioException()方法找到最近发生的异常。
13.7.1 Scanner定界符
在默认的情况下,Scanner根据空白字符对输入进行分词,但可以使用正则表达式指定所需的定界符:
package com.mzm.chapter13;
import java.util.Scanner;
/**
* 使用逗号作为Scanner类的定界符来进行分词
* Created by 蒙卓明 on 2017/11/5.
*/
public class ScannerDelimiter {
public static void main(String[] args) {
Scanner scanner = new Scanner("12, 42, 78, 99, 42");
//指定逗号作为定界符
scanner.useDelimiter("\\s*,\\s*");
while (scanner.hasNextInt()) {
System.out.println(scanner.nextInt());
}
}
}
Scanner对象还有delimiter()方法,返回当前作为定界符使用的Pattern对象。
13.7.2 用正则表达式进行扫描
除扫描基本类型外,还可以使用正则表达式进行扫描:
package com.mzm.chapter13;
import java.util.Scanner;
import java.util.regex.MatchResult;
/**
* Scanner使用正则表达式扫描
*
*/
public class ThreatAnalyzer {
static String threatData =
"58.27.82.161@02/10/2005\n" +
"204.45.234.40@02/11/2005\n" +
"58.27.82.161@02/11/2005\n" +
"58.27.82.161@02/12/2005\n" +
"58.27.82.161@02/12/2005\n" +
"[Next log section with different data format]";
public static void main(String[] args){
Scanner scanner = new Scanner(threatData);
String pattern = "(\\d+[.]\\d+[.]\\d+[.]\\d+)@(\\d{2}/\\d{2}/\\d{4})";
//使用正则表达式扫描
while(scanner.hasNext(pattern)){
scanner.next(pattern);
//调用match()方法匹配
MatchResult match = scanner.match();
String ip = match.group(1);
String date = match.group(2);
System.out.format("Threat on %s from %s\n", date, ip);
}
}
}
13.8 StringTokenizer
在JDK1.4之前,使用StringTokenizer进行分词,但引入正则表达式和Scanner类后,该类已较少使用。