十九、字符串的格式化输出与正则表达式

格式化输出

在 C 语言中,使用 printf 函数将字符串输出到控制台,而 printf 函数支持格式化输出,例如:

int main(){
    int arr[1,2,3,4,5];
    for(int i=0; i<5; i++){
        printf("%-4d",arr[i]);
    }
}
/*output:
1   2   3   4   5   
*/

上例中,使用%-4d来格式化输出,%d表示输出一个整数,中间的 4 表示这个整数占 4 个位置,负号表示左对齐,同理不带负号表示右对齐。

除了输出整数,还有对应的浮点数,字符串,长整数等,包括小数点后保留位数,都有对应的格式代码。

Java 中参考了 C 中的格式代码,Java SE5 引入了System.out.format,PrintWriter 等对象也可以使用这个方法。或者也可以使用System.out.printf()

public class test {
    public static void main(String[] args) {
        int x = 10;
        double y = 6.938;
        System.out.printf("%d %f",x,y);
        System.out.format("%d %f",x,y);
    }
}

Java 中可以使用java.util.Formatter类来处理格式化输出,构造 Formatter 对象并告诉它要输出到哪里(例如,控制台:System.out,或者输出到文件等),其次调用 Formatter 类对象的 format() 方法进行格式化输出。

public class test {
    public static void main(String[] args) {
        Formatter f = new Formatter(System.out);
        f.format("%s is %d and %d","truth", 10, 30);
    }
}

格式代码中的类型转换与格式修饰符参见 JDK 文档中 Formatter 类。

正则表达式

正则表达式是指用某种通用的方式来表达字符串,可以很具体也可以很宽泛,例如正则表达式hello,可以匹配字符串hello

更宽泛一点,例如我们要找一个带负号或者不带负号的数字,首先负号可有可无这个特点可以使用-?来表示,问号表示它前面的这个负号可以没有,一位数字可以用\d表示,多位数字则在后面跟上+即可。

需要注意的是,Java 中反斜杠需要转义,例如我们如果要使用\d来表示一个数字,实际写到正则表达式中应该是这样的:\\d,但是换行和制表符等可以用单个反斜杠(\n,\t等)。

接下来我们来使用上面提到的这个匹配一个有负号或者没有负号的整数,正则表达式为:-?\\d+

public class test {
    public static void main(String[] args) {
        System.out.println("-1234".matches("-?\\d+"));
        System.out.println("456".matches("-?\\d+"));
        //匹配的是带负号或者不带负号的,因此带加号的匹配不上
        System.out.println("+231231".matches("-?\\d+"));
        System.out.println("-33334".matches("-?\\d+"));
    }
}
/*Output:
true
true
false
true
*/

注意:在普通字符串中我们使用\\这样两个反斜杠来表示一个普通的反斜杠字符,现在正则表达式语法中已经使用了\\来表示正则表达式中的反斜杠,那么如果要在正则表达式中使用普通反斜杠应该怎么写呢?答案是四个反斜杠\\\\,我们可以这样理解,普通反斜杠需要两个,而正则表达式中的反斜杠本身就已经是两个了,即先转义成正则表达式中的反斜杠,再转义成普通反斜杠,转义两次!因此 2 乘 2 得 4 个

上面的例子中我们把带加号的排除了,那如何匹配一个既可以带加号也可以带负号也可以没有负号呢?

(-|\\+)?,中间的竖线表示或,加号在正则表达式中有特殊意义,因此需要用反斜杠转义,括号表示这一组正则表达式,问号表示它前面的这组表达式匹配的内容可以有,也可以没有,即带加号或带减号或没有负号。

对于正则表达式,还有一种用法是用于切割字符串,利用 Sting 中的 split() 函数。

public class test {
    public static void main(String[] args) {
        String a = "a,, ffb ssc asdd. fg";
        System.out.println(Arrays.toString(a.split(" ")));
        System.out.println(Arrays.toString(a.split("\\w+")));
        System.out.println(Arrays.toString(a.split("b\\w+")));
    }
}
/*Output:
[a,,, ffb, ssc, asdd., fg]
[a, ffb, ssc, asdd, fg]
[a,, ff, ssc asdd. fg]
*/

上述代码中,第一个输出的意思是以空格作为分割符,第二个中,\\w表示一个非单词字符,加号表示多个,意思是连续的非单词字符,第三个中,表示b开头后面连续多个非单词字符,具体查看输出便可理解。

split还有另外一个重载方法,split(String s, int limit)limit用于限制切分字符串后的数量。

在 String 类中还有一个 replaceFirst("",""),replaceAll("", "") 这样的替换字符串的方法,也可以使用正则表达式,将正则表达式匹配的部分替换成其他字符串。

创建正则表达式

正则表达式中不仅可以有常规的字符,还可以是一些字符类以及预定义的字符类,还有逻辑操作符,边界匹配符等。

下面是一些常用的,完整的请参见 JDK 文档 java.util.regex 包下的 Pattern 类。

字符类意义
.表示任意一个字符
[abc]包含 a 或 b 或 c 任意一个字符
[^abc]除了 a、b、c 以外的任意一个字符
[a-zA-Z]表示 a 到 z 和 A 到 Z 的任意一个字符
[abc[hij]]表示 abchij 中任意一个字符
[a-z&&[hij]]取交集,得到是 hij 中任意一个字符
\s空白符,空格,tab,换行,换页,回车等
\S非空白符,和 [^\s] 相同
\d表示数字,和 [0-9] 相同
\D表示非数字, 和 [^0-9] 相同
\w表示单词字符,和 [a-zA-Z0-9] 相同
\W表示非字符,和 [^\w] 相同
逻辑操作符意义
XY表示X在Y的前面
X|Y表示 X 或 Y
(X)表示捕获组
边界匹配符意义
^表示一行中的起始
$表示一行的结束
\b表示词的边界
\B表示非词的边界
\G表示前一个匹配的结束

在正则表达式中,还有一种量词,量词是作用于某个正则表达式的,比如[abc]?表示有一个 a 或者 b 或者 c 或者没有。

量词中同样的意思又分为三种类型,分别是贪婪型、勉强型和占有型。

Greedy 数量词意义
X?X,一次或一次也没有
X*X,零次或多次
X+X,一次或多次
X{n}X,恰好 n 次
X{n,}X,至少 n 次
X{n,m}X,至少 n 次,但是不超过 m 次
Reluctant 数量词意义
X??X,一次或一次也没有
X*?X,零次或多次
X+?X,一次或多次
X{n}?X,恰好 n 次
X{n,}?X,至少 n 次
X{n,m}?X,至少 n 次,但是不超过 m 次
Possessive 数量词意义
X?+X,一次或一次也没有
X*+X,零次或多次
X++X,一次或多次
X{n}+X,恰好 n 次
X{n,}+X,至少 n 次
X{n,m}+X,至少 n 次,但是不超过 m 次
  1. Greedy(贪婪型):最大匹配
    X?、X*、X+、X{n,}都是最大匹配。
    例如你要用"<.+>"去匹配"<div>abcdefg</div>abf""<.+>"的意思是匹配一对尖括号,并且内部有任意多个任意字符。
    也许你所期待的结果是想匹配<div>,但是实际结果却会匹配到"<div>abcdefg</div>"”。这是为什么呢?
    下面我们跟踪下最大匹配的匹配过程。
    <匹配字符串的第一个<
    .+匹配字符串的div>abcdefg</div>abf,在进行最大匹配时,它把中间遇到的几个个“>”都匹配了(所以它比较贪婪),它匹配了所有字符,直到文本的最后字符f
    ③这时,发现f不能成功匹配“>”,开始按原路回退,用b>匹配,直到abf前面的>匹配成功。
  2. Reluctant(Laziness)(勉强型):最小匹配
    第一点说了,X?、X*、X+、X{n,}都是最大匹配。再后面加个?就成了Laziness匹配。
    例如X??、X*?、X+?、X{n,}?都是最小匹配,其实X{n,m}?和X{n }?有些多余。
    最小匹配意味者前面例子中.+? 匹配一个字符后,马上试一试>的匹配可能,失败了,则.+?再匹配一个字符,再马上试一试>的匹配可能。
  3. Possessive(占有型):完全匹配
    与最大匹配不同,还有一种匹配形式:X?+、X*+、X++、X{n,}+等,也就是在后面加个+。它和最大匹配一样,一直匹配所有的字符,直到文本的最后,但它不由原路返回。也就是说,一口匹配,搞不定就算了,干脆利落。

最后要注意一点,上面量词中的X,是指一个正则表达式,通常需要用圆括号括起来,例如abc+(abc)+,意义是完全不一样的。

Patern 和 Matcher

相较于直接使用字符串相关的类和方法来操作正则表达式,直接使用正则表达式类显得更合理,Java 中 java.util.regex 包中有两个正则表达式相关的常用类。

如标题所示Patern类中的static Matcher compile(String regex)方法可以将一个字符串类型的正则表达式编译成一个正则表达式类,即Patern对象。

在调用Patern类中的matcher(String str)方法可以得到一个Matcher对象,这个对象我们可以理解为一个正则表达式和一个字符串比较这件事的对象,因为比较可以有很多种比较方法,因此Matcher类中有不少好用的方法。具体参考 JDK 文档。

Matcher类的常用方法:

判断功能:
public boolean matches():尝试将整个区域与模式匹配
public lookingAt():判断字符串起始部分是否能够匹配

匹配功能:
public boolean find():尝试查找与该模式匹配的输入序列的下一个子序列
public boolean find(int i):从字符串的第 i 个字符开始匹配下一个子序列

获取功能:
public String group():返回由以前匹配操作所匹配的输入子序列
public String group(int i):得到正则表达式中第组 i 组匹配的字符串
public int groupCount():得到正则表达式的组数

这里提到了正则表达式的分组,其实就是一组圆括号为一组,例如:"(A)((B)(C))",这里就有四组"(A)""((B)(C))""(B)""(C)"。第 0 组是它本身。

此外Patern类还提供一个静态方法static boolean matches(String regex, CharSequence input)

下面来尝试一下使用这些方法做字符串匹配

public class test {
    public static void main(String[] args){
        String str = "sad  asdwie  eriuieu oifsd  wisji asdh";
        String regex = "(\\S+)\\s+((\\S+)\\s+(\\S+))$";//匹配最后三个单词,\S 表示字母,\s 表示非单词字符,+表示多个,$ 表示从末尾开始匹配
        Matcher m = Patern.compile(regex).matcher(str);
        
        while(m.find()) {
            for(int i=0; i<=m.groupCount(); i++) {
                System.out.println("["+m.group(i)+"]");
            }
        }
    }
}
/*Output:
[oifsd  wisji asdh][oifsd][wisji asdh][wisji][asdh]
*/
Pattern标记

Pattern类中,编译正则表达式的方法还有另外一种:

Pattern Pattern.compile(String regex, int flag);

这其中的flag,是来自于Pattern类定义的常量,用于调整匹配行为的。下面的表格中展示了常用的几个标记。

编译标记效果
Pattern.CASE_INSENSITIVE(?i)匹配时忽略大小写
Pattern.MULTILINE(?m)默认情况下^$是从完整字符串的开头或结尾开始匹配,而标记之后,以行的开头结尾为准。
Pattern.COMMENTS(?x)忽略空格和以#开头的注释

在使用时,可以使用|来结合多个标记同时产生作用。

例如,我们将之前的匹配代码稍作修改:

public class test {
    public static void main(String[] args){
        String str = "sad  asdwie  eriuieu oifsd  wisji asdh\n"+
        "adb  cbhd   jasd   ksirj\n";
        String regex = "(?m)(\\S+)\\s+((\\S+)\\s+(\\S+))$";//增加标记(?m),表示匹配时按行来
        Matcher m = Patern.compile(regex).matcher(str);
        
        while(m.find()) {
            for(int i=0; i<=m.groupCount(); i++) {
                System.out.println("["+m.group(i)+"]");
            }
        }
    }
}
/*Output:
[oifsd  wisji asdh][oifsd][wisji asdh][wisji][asdh]
[cbhd   jasd   ksirj][cbhd][jasd   ksirj][jasd][ksirj]
*/

替换字符串功能

在字符串操作中,通常需要将字符串中的某些字符替换掉,例如要把一段文本中每个单词之间多余的空格去掉,将代码中某个变量名字更改,就需要全部替换,亦或者将部分字符都改为大写等。下面列出了 String 类和 Matcher 类中的常用替换字符串的方法:

String 类中:

 String replaceAll(String regex, String replacement) 
          使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。 
 String replaceFirst(String regex, String replacement) 
          使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。 
------------------------------------
Matcher 类中:
 String replaceAll(String replacement) 
          替换模式与给定替换字符串相匹配的输入序列的每个子序列。 
 String replaceFirst(String replacement) 
          替换模式与给定替换字符串匹配的输入序列的第一个子序列。 
 Matcher appendReplacement(StringBuffer sb, String replacement) 
          实现非终端添加和替换步骤。 
 StringBuffer appendTail(StringBuffer sb) 
          实现终端添加和替换步骤。 

下面是一个使用这些方法的例子:

public class test {
    public static void main(String[] args) {
        String s = "asdasldj kjsaldk jwiqeo   alsdjk a  qewi  aa";
        s.replaceAll(" {2,}", " ");//将超过两个连续空格替换成一个空格
        s.replaceFirst("[aeiou]","first");//将第一个元音字母替换成first
        
        StringBuffer sb = new StringBuffer();
        Matcher m = Pattern.compile("[aeiou]").matcher(s);
        
        while(m.find()) {
            m.appendReplacement(sb, m.group().toUpperCase());//将匹配到的转换成大写
        }
        m.appendTail(sb);//将剩余未处理的字符串加载到缓冲区
        System.out.println(sb);
    }
}


上例中 String 类的替换方法很好理解,replaceAll 方法将所有匹配正则表达式的字符串都替换成指定字符串。replaceFirst 方法则只替换第一个匹配的。Matcher 类中的这两个方法同理。

appendReplacement(StringBuffer sb, String replacement)方法有点不太好理解,简单的说这个方法提供了一个途径:可以让你将匹配到的字符串单独拿出来进行处理。具体在执行的时候,是将原字符串读入缓冲区,在读入之前进行匹配操作,匹配上则将替换的字符串读入缓冲区。

appendTail(StringBuffer sb)方法可以在一次或多次调用 appendReplacement 方法后调用它来复制剩余的字符串进入缓冲区。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值