Java 字符串处理

Table of Contents

一般程序需要处理大量文本数据,Java 语言的文本数据被保存为字符或字符串类型。关于字符及字符串的操作主要用到 String 类和 StringBuffer 类,如连接、修改、替换、比较和查找等。

字符串是 Java 程序中经常处理的对象之一,因此如果字符串运用得不好,将影响程序的运行效率。

定义字符串

字符串是 Java 中特殊的类,使用方法像一般的基本数据类型,被广泛应用在 Java 编程中。 Java 没有内置的字符类型,而是在标准 Java 类库中提供了一个 String 类来创建和操作字符串。

我们有两种方式定义字符串(如下),并且无论使用哪种形式创建字符串,字符串对象一旦被创建,其值是不能改变的,但可以使用其他变量重新赋值的方式进行更改。

1. 直接定义字符串

在 Java 中定义字符串最简单的方法是用双引号把它包围起来,这种用双引号括起来的一串字符实际上都是 String 对象,如字符串 "Hello" 在编译后即成为 String 对象。

1: String str1 = "Hello Java";
2: // OR
3: String str2;
4: str = "Hello Java";             // 必须初始化哦

*注意:字符串变量必须经过初始化才能使用。

2. 使用 String 类定义

前面提到在 Java 中每个双引号定义的字符串都是一个 String 类的对象,因此,可以通过使用 String 类的构造方法来创建字符串,该类位于 java.lang 包中。

String 类的构造方法有多种重载形式,每种形式都可以定义字符串。

具有和类名相同的名称,而且没有返回类型的方法称为构造方法。重载是指在一个类中定义多个同名的方法,但要求每个方法具有不同的参数的类型或参数的个数。

2.1 String()

初始化一个新创建的 String 对象,表示一个空字符序列。

2.2 String(String original)

初始化一个新创建的 String 对象,使其表示一个与参数相同的字符序列。换句话说,新创建的字符串是该参数字符串的副本。

1: String str1 = new String("Hello Java");
2: String str2 = new String(str1);

这里 str1str2 的值是相等的。

2.3 String(char[] value)

分配一个新的字符串,将参数中的字符数组元素全部变为字符串。该字符数组的内容已被复制,后续对字符数组的修改不会影响新创建的字符串。例如:

1: char[] a = {'H', 'e', 'l', 'l', 'o'};
2: String sChar = new String(a);
3: a[1] = 's';                     // 对数组 a 中元素的更改并不会影响 sChar 的值(毕竟是拷贝了)

2.4 String(char[] value, int offset, int count)

分配一个新的 String ,它包含来自该字符数组参数一个子数组的字符。 offset 参数是子数组第一个字符的索引, count 参数指定子数组的长度。该子数组的内容已被赋值,后续对字符数组的修改不会影响新创建的字符串。例如:

1: char[] a = {'H', 'e', 'l', 'l', 'o'};
2: String sChar = new String(a, 1, 4); // → "ello"
3: a[1] = 's';                         // 对 sChar 的值也没有任何影响

String 和 int 的相互转换

String 在编程中被广泛使用,所以掌握 String 和 int 的相互转换方法是极其重要的。

1. String 转换为 int

String 字符串转整型 int 有以下两种方式:

  • Integer.parseInt(str)
  • Integer.valueOf(str).intValue()

*注:Integer 是一个类,是 int 基本数据类型的封装类。

在 String 转换 int 时,String 的值一定是整数,否则会报数字转换异常( java.lang.NumberFormatException )。

2. int 转换为 String

整型 int 转 String 字符串类型有以下 3 种方法:

  • String s = String.valueOf(i); ,注意 valueOf 括号中的值不能为空,否则会报空指针异常;
  • String s = Integer.toString(i);
  • String s = "" + i; ,相对耗时较大。

3. valueOf()、parse() 和 toString()

3.1 valueOf()

valueOf() 方法将数据的内部格式转换为可读的形式。

它是一种静态方法,对于所有 Java 内置的类型,在字符串内被重载,以便每一种类型都能被转换成字符串。 valueOf() 方法还被 Object 重载,所以创建的任何形式类的对象也可被用作一个参数。

以下为 valueOf 的几种形式:

- static String valueOf(double num)
- static String valueOf(long num)
- static String valueOf(Object obj)
- static String valueOf(char[] chars)

对各种数据类型,可以直接调用 valueOf() 方法得到合理的字符串形式:

  • 所的有简单类型数据转换成相应于它们的普通字符串形式;
  • 任何传递给该方法的对象都将返回对象的 toString() 方法调用的结果。

事实上,也可以直接调用 toString() 方法而得到相同的结果。

对大多数数组, valueOf() 方法返回一个相当晦涩的字符串,说明它是一个某种类型的数组。然而,对于字符数组,它创建一个包含了字符数组中的字符的字符串对象。

字符串,无疑是很重要的,毕竟数字之上就是它了,然而这些稀奇古怪的方法却不用太在意的,只需要掌握一种通用的性能较好的就可以了,关键在于理解它们在编译器和内存中所作的动作。

3.2 parse()

parseXxx(String) 这种形式,是指把字符串转换为数值型,其中 Xxx 对应不同的数据类型,如 int 型和 float 型。

3.3 toString()

toString() 可以把一个引用类型转换为 String 字符串类型,是 sun 公司开发 Java 的时候为了方便所有类的字符串操作而特意加入的一个方法。

字符串拼接

通过字符串连接,可以将两个或多个字符串、字符、整数和浮点数等类型的数据连成一个更大的字符,String 字符串拼接可以使用 + 运算符或 String 的 concat(String str) 方法。

*注: + 运算符的优势是可以连接任何类型数据拼接为字符串,而 concat 方法只能拼接 String 类型字符串。

1. 使用连接运算符 +

+ 运算符是最简单、最快捷,也是使用最多的字符串连接方式。

*注:在使用 + 运算符连接字符串和 int 型(或 double 型)数据时,会自动将它们转换成 String 类型。

2. 使用 concat() 方法

在 java 中,String 类的 concat() 方法实现了将一个字符串连接到另一个字符串的后面。语法格式如下:

str1.concat(str2)

*注: concat() 方法一次只能连接两个字符串,如果需要连接多个字符串,需要调用多次 concat() 方法。

3. 连接其他类型数据

字符串也可同其他基本数据类型进行连接,如果将字符串同这些数据类型数据进行连接,此时会将这些数据直接转换成字符串。

*注:只要 + 运算符的一个操作数是字符串,编译器就会将另一个操作数转换成字符串形式,所以应该谨慎地将其他数据类型与字符串相连,以免出现意想不到的结果。

字符串的若干操作

获取字符串长度

在 Java 中,要获取字符串的长度,可以使用 String 类的 length() 方法,其语法形式如下:

字符串名.length();

字符串的大小写转换

String 类的 toLowerCase() 方法可以将字符串中的所有字符全部转换成小写,而非字母的字符不受影响。语法格式如下:

字符串名.toLowerCase();

toUpperCase() 则将字符串中的所有字符全部转换成大写,而非字母的字符不受影响。语法格式如下:

字符串名.toUpperCase();

去除字符串中的空格

字符串中存在的首尾空格一般情况下都没有任何意义,如字符串 “ Hello ” ,但是这些空格会影响到字符串的操作,如连接字符串或比较字符串等,所以应该去掉字符串中的首尾空格,这需要使用 String 类提供的 trim() 方法。语法格式如下:

字符串名.trim();

*注:如果不确定要操作的字符串首尾是否有空格,最好在操作之前调用该字符串的 trim() 方法去除首尾空格,然后再对其进行操作。

截取子字符串

形式如下:

字符串名.substring(int beginIndex[, int endIndex]);

其中:

  • beginIndex 表示截取的起始索引,截取的字符串中 包括 起始索引对应的字符;
  • endIndex 表示结束索引,截取的字符串中 不包括 结束索引对应的字符;
  • 如果不指定 endIndex ,则表示截取到目标字符串末尾。

*注: substring() 方法是按字符截取,而不是按字节截取。

分割字符串

String 类的 split() 方法可以按指定的分割符对目标字符串进行分割,分割后的内容存放在字符串数组中。该方法主要有以下两种重载形式:

str.split(String sign)
str.split(String sign, int limit)

其中:

  • str 为需要分割的目标字符串;
  • sign 为指定的分割符,可以是任意字符串;
  • limit 表示分割后生成的字符串的限制个数,如果不指定,则表示不限制,直到将整个目标字符串完全分割为止。

使用分隔符需要注意:

  • .| 都是转义字符,必须得加 \\
  • 如果在一个字符串中有多个分隔符,可以用 | 作为连字符,如要把 acount=? and uu=? or n=? 分隔出来,可以用 String.split("and|or")

字符串的替换

1. replace()

replace() 方法用于将目标字符串中的指定字符(串)替换成新的字符(串),语法格式如下:

字符串.replace(String oldChar, String newChar);

其中:

  • oldChar 表示被替换的字符串;
  • newChar 表示用于替换的字符串。

*注: replace() 方法会将字符串中所有 oldChar 替换成 newChar

2. replaceFirst()

replaceFirst() 方法将目标字符串中匹配某正则表达式的第一个字条串替换成新的字符串,语法格式如下:

字符串.replaceFirst(String regex, String replacement);

3. replaceAll()

replaceAll() 方法用于将目标字符串匹配某正则表达式的所有子字符串替换成新的字符串,语法格式如下:

字符串.replaceAll(String regex, String replacement);
正则表达式,以后专题再说……

字符串比较

字符串比较是常见的操作,包括比较相等、大小、前缀和后缀等。

在 Java 中,比较字符串的常用方法有 3 个: equals() 方法、 equalsIgnoreCase() 方法、 compareTo() 方法。

1. equals()

equals() 方法将逐个地比较两个字符串的每个字符是否相同(大小写也在检查范围之内),语法格式如下:

str1.equals(str2);

当相等时,返回 true ,否则返回 false

2. equalsIgnoreCase()

equalsIgnoreCase() 方法的作用和语法与 equals() 方法完全相同,唯一不同的是 equalsIgnoreCase() 比较时 不区分大小写

3. equals() 与 == 的比较

理解 equals() 方法和 == 运算符执行的是两个不同的操作是重要的:

  • equals() 方法比较字符串对象中的字符;
  • == 运算符比较两个对象引用看它们是否引用相同的实例。
1: String s1 = "Hello";
2: String s2 = new String(s1);
3: System.out.println(s1.equals(s2)); // → true
4: System.out.println(s1 == s2);      // → false

*注:千万不要使用 == 运算符测试字符串的相等性,以免在程序中出现糟糕的 bug。

C++ 的 String 类重载了 == 运算符以便检测字符串内容的相等性,可惜 Java 没有采用这种方式,它的字符串“看起来、感觉起来”与数值一样,但进行相等性测试时,其操作方式又类似于指针。

当然,每一种语言都会存在一些不太一致的地方。比如,C 程序员从不使用 == 对字符串进行比较,而使用 strcmp 函数。

4. compareTo()

Java 的 compareTo 方法与 strcmp 完全类似。

通常,仅仅知道两个字符串是否相同是不够的。对于排序应用来说,必须知道一个字符串是大于、等于还是小于另一个。一个字符串小于另一个指的是它在字典中先出现,而一个字符串大于另一个指的是它在字典中后出现。

compareTo() 方法用于按字典顺序比较两个字符串的大小,该比较是基于字符串各个字符的 Unicode 值,语法格式如下:

str.compartTo(String otherstr);

如果按字典顺序 str 位于 otherster 参数之前,比较结果为一个负整数;如果 str 位于 otherstr 之后,比较结果为一个正整数;如果两个字符串相等,则结果为 0

*注:如果两个字符串调用 equals() 方法返回 true ,那么调用 compareTo() 方法会返回 0

字符串查找

在给定的字符串中查找字符或字符串是比较常见的操作。字符串查找分为两种形式:

  • 一种是在字符串中获取匹配字符(串)的索引值 – 根据字符查找索引;
  • 另一种是在字符串中获取指定索引位置的字符 – 根据索引查找字符。

1. 根据字符查找

1.1 indexOf()

indexOf() 方法用于返回字符(串)在指定字符串中首次出现的索引位置,如果能找到,则返回索引值,否则返回 -1 。该方法主要有以下两种重载形式:

str.indexOf(value)
str.indexOf(value, int fromIndex)

其中:

  • str 表示指定字符串;
  • value 表示待查找的字符(串);
  • fromIndex 表示查找时的起始索引,缺省为 0 。

1.2 lastIndexOf()

lastIndexOf() 方法用于返回字符(串)在指定字符串中最后一次出现的索引位置,如果能找到则返回索引值,否则返回 -1 。该方法也有两种重载形式:

str.lastIndexOf(value)
str.lastIndexOf(value, int fromIndex)

*注: lastIndexOf() 方法的查找策略是从右往左查找,如果不指定起始索引,则默认从字符串的末尾开始查找。

来看个例子吧。

 1: // 原始字符串是 "today,monday,sunday"
 2: indexOf("day");                 // → 2
 3: indexOf("day", 5);              // → 9
 4: indexOf("o");                   // → 1
 5: indexOf("o", 6);                // → 7
 6: 
 7: lastIndexOf("day");             // → 16
 8: lastIndexOf("day", 5);          // → 2
 9: lastIndexOf("o");               // → 7
10: lastIndexOf("o", 6);            // → 1

2. 根据索引查找

String 类的 charAt() 方法可以在字符串内根据指定的索引查找字符,该方法的语法形式如下:

字符串名.charAt(索引值)

*注:字符串本质上是字符数组,因此它也有索引,索引从零开始。

StringBuffer 类

在 Java 中,除了通过 String 类创建和处理字符串之外,还可以使用 StringBuffer 类来处理字符串,它可以比 String 类更高效地处理字符串。

因为 StringBuffer 类是 可变字符串类 ,创建 StringBuffer 类的对象后可以随意修改字符串的内容。每个 StringBuffer 类的对象都能够存储指定容量的字符串,如果字符串的长度超过了 StringBuffer 类对象的容量,则该对象的容量会自动扩大。

1. 创建 StringBuffer 类

StringBuffer 类提供了 3 个构造方法来创建一个字符串:

  • StringBuffer() 构造一个空的字符串缓冲区,并且初始化为 16 个字符的容量;
  • StringBuffer(int length) 创建一个空的字符串缓冲区,并且初始化为指定长度 length 的容量;
  • StringBuffer(String str) 创建一个字符串缓冲区,并将其内容初始化为指定的字符串内容 str ,字符串缓冲区的初始化容量为 16 加上字符串 str 的长度。

2. 追加字符串

StringBuffer 类的 append() 方法用于向原有 StringBuffer 对象中追加字符串,语法格式如下:

StringBuffer 对象.append(String str)

该方法的作用是追加内容到当前 StringBuffer 对象的末尾,类似于字符串的连接。调用该方法以后,StringBuffer 对象的内容也发生了改变。

3. 替换字符

StringBuffer 类的 setCharAt() 方法用于在字符串的指定索引位置替换一个字符。语法格式如下:

StringBuffer 对象.setCharAt(int index, char ch);
1: StringBuffer sb = new StringBuffer("hello");
2: sb.setCharAt(1, 'E');           // → hEllo
3: sb.setCharAt(0, 'H');           // → HEllo
4: sb.Setcharat(2, 'p');           // → HEplo

4. 反转字符串

StringBuffer 类中的 reverse() 方法用于将字符串序列用其反转的形式取代。语法格式如下:

StringBuffer 对象.reverse()

5. 删除字符串

StringBuffer 类提供了 deleteCharAt()delete() 两个删除字符串的方法,语法格式如下:

StringBuffer 对象.deleteCharAt(int index);
StringBuffer 对象.delete(int start, int end);

6. String、StringBugger 和 StringBuilder 类的区别

在 Java 中字符串属于对象。

Java 提供了 String 类来创建和操作字符串,String 类是不可变类,即一旦一个 String 对象补创建以后,包含在这个对象中的字符序列是不可变的,直至这个对象被销毁。

Java 提供了两个可变字符串类 StringBuffer 和 StringBuilder ,即字符串缓冲区。

StringBuilder 类是 JDK 1.5 新增的类,它也代表可变字符串对象,它与 StringBuffer 功能基本相似,方法也差不多。不同的是,StringBuffer 是线程安全的,而 StringBuilder 则没有实现线程安全功能(所以性能略高)。通常情况下,如果需要创建一个内容可变的字符串对象,则应该优先考虑 StringBuilder 类。

StringBuffer、StringBuilder、String 中都实现了 CharSequnce 接口。 CharSequnce 是一个定义字符串操作的接口,它只包括 length()、CharAt(int index)、subSequnce(int start, int end) 这几个 API 。

StringBuffer、StringBuilder、String 对 CharSequnce 接口的实现过程不一样,如下图所示:

可见,String 直接实现了 CharSequence 接口,StringBuilder 和 StringBuffer 都是可变的字符序列,它们都继承于 AbstractStringBuilder ,实现了 CharSequence 接口。

7. 小结

String 是 Java 中基础且重要的类,被声明为 final class ,是不可变字符串。因为它的不变性,所以拼接字符串时会产生很多无用的中间对象,如果频繁的进行这样的操作对性能有所影响。

StringBuffer 就是为了解决大量拼接字符串时产生很多中间对象问题而提供的一个类,它提供了 appendadd 方法,可以将字符串添加到已有序列的末尾或指定位置,它的本质是一个线程安全的那部分,减少了开销。

什么情况下用什么呢?

  • 操作少量的数据使用 String ;
  • 单线程操作大量数据使用 StringBuilder ;
  • 多线程操作大量数据使用 StringBuffer 。

速度方面,一般情况下,速度从快到慢为 StringBuilder > StringBuffer > String,当然这是相对的,不是绝对的。

正则表达式

理解正则表达式

正则表达式(Regular Expression)是一个强大的字符串处理工具,可以对字符进行查找、提取、分割、替换等操作,是一种用于模式匹配和替换的规范。

一个正则表达式就是由普通的字符( a~z )以及特殊字符(元字符)组成的文字模式,它用以描述在查找文字主体时待匹配的一个或多个字符串。

很多读者都会觉得正则表达式是一个非常神奇、高级的知识,其实正则表达式是一种非常简单而且非常实用的工具。

正则表达式是一个用于匹配字符串的模板。 实际上,任意字符串都可以当成正则表达式使用,例如 "abc" 也是一个正则表达式,只是它只能匹配 "abc" 字符串而已。

普通字符串是正则表达式的一种特殊形式,字符串统一于正则表达式。哎,哲学这个诱人的小妞,哪儿都有你。

String 类里也提供了如下几个特殊的方法:

boolean matches(String regex) 判断该字符串是否匹配指定的正则表达式
String replaceAll(String regex, String replacement) 将该字符串中匹配 regex 的子串替换成 replacement
String replaceFirsr(String regex, String replacement) 将该字符串中第一个匹配 regex 的子串替换成 replacement
String[] split(String regex) regex 作为分隔符,把该字符串分割成多个子串

创建正则表达式就是创建一个特殊的字符串。

Table 1: 正则表达式所支持的合法字符
字符 解释
x 字符 xx 可代表任何合法的字符 )
\0mnn 八进制数 0mnn 所表示的字符
\xhh 十六进制值 0xhh 所表示的字符
\uhhhh 十六进制值 0xhhhh 所表示的 Unicode 字符
\t 制表符( '\u0009'
\n 新行(换行)符( '\u000A'
\r 回车符( '\u000D'
\f 换页符( '\u000C'
\a 报警(bell)符( '\u0007'
\e Escape 符( '\u001B'
\cx x 对应的控制符。例如, \cM 匹配 Ctrl-Mx 值必须为 A~Za~z 之一

除此这外,正则表达式中有一下些特殊字符,它们在正则表达式中有其特殊用途,比如前面介绍的反斜线 \ 。如果需要匹配这些特殊字符, 就必须首先将这些字符转义 ,也就是在前面添加一个反斜线 \

Table 2: 正则表达式中的特殊字符
特殊字符 说明
$ 匹配一行的结尾
^ 匹配一行的开头
() 标记子表达式的开始和结束位置
[] 用于确定中括号表达式的开始和结束位置
{} 用于标记前面子表达式的出现频度
* 指定前面子表达式可以出现零次或多次
+ 指定前面子表达式可以出现一次或多次
? 指定前面子表达式可以出现零次或一次
. 匹配除换行符 \n 之外的任何单字符
\ 用于转义下一个字符,或指定八进制、十六进制字符
I 指定两项之间任选一项

*注:最后的 I| ,只不过在 Emacs 的表格中敲不出来。

将上面多个字符拼起来,就可以创建一个正则表达式,还是来看几个例子吧:

1: "\u0041\\\\"                    // → 匹配 A\
2: "\u0061\t"                      // → 匹配 a<制表符>
3: "\\?\\["                        // → 匹配 ?[

为什么第一行的正则表达式会有那么多反斜杠?这是由于 Java 字符串中反斜杠本身需要转义,因此两个反斜杠( \\ )实际上相当于一个(前一个用于转义)。

Java 中的正则表达式和 Emacs Lisp 中的一样让人感到…… xx?+-^|...

上面的正则表达式依然只能匹配单个字符,这是因为还未在表达式中使用“通配符”。

通配符是什么呢?它是可以匹配多个字符的特殊字符。正则表达式中的“通配符”远远超出了普通通配符的功能,它被称为预定义符。

Table 3: 预定义符
预定义符 说明
. 可以匹配任何字符
\d 匹配 0~9 的所有数字
\D 匹配非数字
\s 匹配所有的空白字符,包括空格、制表符、回车符、换页符、换行符等
\S 匹配所有的非空白字符
\w 匹配所有的单词字符,包括 0~9 所有数字、26 个英文字母和下划线 _
\W 匹配所有的非单词字符

上面的 7 个预定义字符其实很容易记忆,其中:

  • d 是 digit 的意思,代表数字;
  • s 是 space 的意思,代表空白;
  • w 是 word 的意思,代表单词;
  • d、s、w 的大写形式恰好匹配与之相反的字符。

有了预定义符后,接下来就可以创建更强大的正则表达式了,例如:

1: "c\\wt"                            // → 可以匹配 cat cbt cct cOt c9t 等一批字符串
2: "\\d\\d\\d-\\d\\d\\d-\\d\\d\\d\\d" // → 匹配 000-000-0000 形式的电话号码

在一些特殊情况下,例如,若只想匹配 a-f 的字母,或者匹配除 ab 之外的所有小写字母,或者匹配中文字符,上面这些预定义字符就无能为力了,此时就需要使用方括号表达式。

Table 4: 方括号表达式
方括号表达式 说明
表示枚举 [abc] 表示 a、b、c 其中的任意一个字符
表示范围 - [a-f] 表示 a~f 范围内的任意字符;
  [\\u0041-\\u0056] 表示十六进制字符 \u0041\u0056 范围的字符
  范围可以和枚举结合使用,如 [a-cx-x] 表示 a~c、x~z 范围内的任意字符
表示求否 ^ [^abc] 表示非 a、b、c 的任意字符
  [^a-f] 表示不是 a-f 范围内的任意字符
表示“与”运算 && [a-x&&^bc]a~z 范围内的所有字符,除 b、c 之外
  [a-z&&[def]]a~z[def] 的交集
  [a-z][a-z&&[m-p]]a~z 范围内的所有字符,除 m~p 范围之外的字符
表示“并”运算 并运算与前面的枚举类似,如 [a-d[m-p]] 表示 [a-dm-p]

方括号表达式比前面的预定义字符灵活多了,几乎可以匹配任何字符。

例如,若需要匹配所有的中文字符,就可以利用 [\\u0041-\\u0056] 形式 – 因为所有中文字符的 Unicode 值是连续的,只要找出所有中文字符中最小、最大的 Unicode 值,就可以利用上面的形式来匹配所有的中文字符。

正则表达式还支持圆括号,用于将多个表达式组成一个子表达式,圆括号中可以使用或运算符 | 。如正则表达式 "((public)|(protected)|(private))" 用于匹配 Java 的三个访问控制符其中之一。

除此之外,Java 正则表达式还支持如下表所示的几个边界匹配符:

Table 5: 边界匹配符
边界匹配符 说明
^ 行的开头
$ 行的结尾
\b 单词的边界
\B 非单词的边界
\A 输入的开头
\G 前一个匹配的结尾
\Z 输入的结尾,仅用于最后的结束符
\z 输入的结尾

前面例子中需要建立一个匹配 000-000-0000 形式的电话号码时,使用了 \\d\\d\\d-\\d\\d\\d-\\d\\d\\d\\d 正则表达式,这看起来比较烦琐。实际上,正则表达式还提供了数量标识符,正则表达式支持的数量标识符有如下几种模式:

  • Greedy(贪婪模式):数量表示符默认采用贪婪模式,除非另有表示.

贪婪模式的表达式会一直匹配下去,直到无法匹配为止。如果你发现表达式匹配的结果与预期不符,很可能是因为你以为表达式只会匹配前面几个字符,而实际上它是贪婪模式,会一直匹配下去。

  • Reluctant(勉强模式):用问号后缀( ? )表示,它只匹配最少的字符,也称为最小匹配;
  • Possessive(占有模式):用加号后缀( + )表示,目前只有 Java 支持占有模式,通常很少使用。
Table 6: 三种模式的数量表示符
贪婪模式 勉强模式 占用模式 说明
X? X?? X?+ X 表达式出现零次或一次
X* X*? X*+ X 表达式出现零次或多次
X+ X+? X++ X 表达式出现一次或多次
X{n} X{n}? X{n}+ X 表达式出现 n 次
X{n,} X{n,}? X{n,}+ X 表达式最少出现 n 次
X{n, m} X{n, m}? X{n, m}+ X 表达式最少出现 n 次,最多出现 m 次

关于贪婪模式和勉强模式的对比,还是来看段代码吧,如下:

1: String str = "hello, java";
2: // 贪婪模式的正则表达式
3: System.out.println(str.replaceFirst("\\w*", "■"));  // → ■, java!
4: // 勉强模式的正则表达式
5: System.out.println(str.replaceFirst("\\w*?", "■")); // → ■hello, java!

Pattern 类和 Matcher 类

java.util.regex 是一个用正则表达式所订制的模式来对字符串进行匹配工作的类库包,它包括两个类: PatternMatcher

Pattern 对象是正则表达式编译后在内存中的表示形式,因此,正则表达式字符串必须先被编译为 Pattern 对象,然后再利用该 Pattern 对象创建对应的 Matcher 对象。执行匹配所涉及的状态保留在 Matcher 对象中,多个 Matcher 对象可共享同一个 Pattern 对象。

因此,典型的调用顺序如下:

1: // 将一个字符串编译成 Pattern 对象
2: Pattern p = Pattern.compile("a*b");
3: // 使用 Pattern 对象创建 Matcher 对象
4: Matcher m = p.matcher("aaaaab");
5: boolean b = m.mathces();       // → true

上面定义的 Pattern 对象可以多次重复使用。如果某个正则表达式仅需一次使用,则可直接使用 Patter 类的静态 matches 方法,此方法自动把指定字符串编译成匿名的 Patther 对象,并执行匹配,如下所示:

1: boolean b = Patter.matches("a*b", "aaaaab"); // → true

上面语句等效于前面的三条语句,但采用这种语句每次都需要重新编译新的 Pattern 对象,不能重复利用已编译的 Patter 对象,所以效率不高。Pattern 是不可变类,可供多个并发线程安全使用。

Table 7: Matcher 类的几个常用方法
名称 说明
find() 返回目标字符串是否包含与 Pattern 匹配的子串
group() 返回上一次与 Pattern 匹配的子串
start() 返回上一次与 Pattern 匹配的子串在目标字符串中的开始位置
end() 返回上一次与 Pattern 匹配的子串在目标字符串中的结束位置加 1
lookingAt() 返回目标字符串前面部分与 Pattern 是否匹配
matches() 返回整个目标字符串与 Pattern 是否匹配
reset() 将现有的 Matcher 对象应用于一个新的字符序列

Pattern、Matcher 类的介绍中经常会看到一个 CharSequence 接口,该接口代表一个字符序列,其中 CharBuffer、String、StringBuffer、StringBuilder 都是它的实现类。简单地说, CharSequence 代表一个各种形式的字符串。

通过 Matcher 类的 find()group() 方法可以从目标字符串中依次取出特定子串(匹配正则表达式的子串),例如互联网的网络爬虫,它们可以自动从网页中识别出所有的电话号码。

下面的程序示范了如何从大段的字符串中找出电话号码。

 1: public class FindGroup {
 2:     public static void main(String[] args) {
 3:         // 使用字符串横批从网络上得到的网页源码
 4:         String str = "我想找一套适合自己的JAVA教程,尽快联系我13500006666" + "交朋友,电话号码是13611125565" + "出售二手电脑,联系方式15899903312";
 5:         // 创建一个 Pattern 对象,并用它建立一个 Matcher 对象
 6:         // 该正则表达式只抓取 13X 和 15X 段的手机号
 7:         // 实际要抓取哪些电话号码,只要修改正则表达式好可
 8:         Matcher m = Pattern.compile("((13\\d)|(15\\d))\\d{8}").matcher(str);
 9:         // 将所有符合正则表达式的子串(电话号码)全部输出
10:         while (m.find()) {
11:             System.out.println(m.group());
12:         }
13:     }
14: }
15: // → 13500006666
16: // → 13611125565
17: // → 15899903312

从上面运行结果可以看出, find() 方法依次查找字符串与 Pattern 匹配的子串,一旦找到对应的子串,下次调用 find() 方法时将接着向下查找。

*注:通过程序运行结果不难看出,使用正则表达式可以提取网页上的电话号码,也可以提取邮件地址等信息。如果程序再进一步,可以从网页上提取超链接信息,再根据超链接打开其他网页,然后在其他网页一重复这个过程就可以实现简单的网络爬虫了。

find() 方法还可以传入一个 int 类型的参数,带 int 参数的 find() 方法将从该 int 索引处向下搜索。 start()end() 方法主要用于确定在目标字符串的位置,如下程序所示:

 1: public class StartEnd {
 2:     public static void main(String[] args) {
 3:         // 创建一个 Pattern 对象,并用它建立一个 Matcher 对象
 4:         String regStr = "Java is very easy!";
 5:         System.out.println("目标字符串是:" + regStr);
 6:         Matcher m = Pattern.compile("\\w+").matcher(regStr);
 7:         while (m.find()) {
 8:             System.out.println(m.group() + "子串的起始位置:" + m.start() + ",其结束位置:" + m.end());
 9:         }
10:     }
11: }

运行结果如下:

目标字符串是:Java is very easy!
Java子串的起始位置:0,其结束位置:4
is子串的起始位置:5,其结束位置:7
very子串的起始位置:8,其结束位置:12
easy子串的起始位置:13,其结束位置:17

matches()lookingAt() 方法有点相似,只是 matches() 方法要求整个字符串和 Pattern 完全匹配时才返回 true ,而 lookingAt() 只要字符串以 Pattern 开头就会返回 true

reset() 方法可将现有的 Matcher 对象应用于新的字符序列。

 1: public class MatchesTest {
 2:     public static void main(String[] args) {
 3:         String[] mails = {"kongyeeku@163.com", "kongyeeku@gmail.com", "ligang@crazyit.org", "wawa@abc.xx"};
 4:         String mailRegEx = "\\w{3, 20}@\\w+\\.(com|org|cn|net|gov)";
 5:         Pattern mailPattern = Pattern.compile(mailRegEx);
 6:         Matcher matcher = null;
 7: 
 8:         for (String mail : mails) {
 9:             if (matcher == null) {
10:                 matcher = mailPattern.matcher(mail);
11:             } else {
12:                 matcher.reset(mail);
13:             }
14: 
15:             String result = mail + (matcher.matches() ? "是" : "不是" + "一个有效的邮件地址!");
16:             System.out.println(result);
17:         }
18:     }
19: }

上面程序创建了一个邮件地址的 Pattern ,接着用这个 Pattern 与多个邮件地址进行匹配。当程序中的 Matchernull 时,程序调用 matcher() 方法来创建一个 Matcher 对象,一旦 Matcher 对象被创建,程序就调用 Matcher 的 reset() 方法将该 Matcher 应用于新的字符序列。

从某个角度来看,Matcher 的 matches()、lookingAt() 和 String 类的 equals 有点相似。区别是后者都是与字符串进行比较,而前者则是与正则表达式进行匹配。

事实上,String 类里面也提供了 matches() 方法,该方法返回该字符串是否匹配指定的正则表达式。如:

1: "kongyeeku@163.com".matches("\\w{3, 20}@\\w+\\.(com|org|cn|net|gov)"); // → true

除此之外,还可以利用正则表达式对目标字符串进行分割、查找、替换等操作,如下:

 1: public class ReplaceTest {
 2:     public static void main(String[] args) {
 3:         String[] msgs = { "Java has regular expressions in 1.4", "regular expressions now expressing in Java"};
 4:         Pattern p = Pattern.compile("re\\w*");
 5:         Matcher matcher = null;
 6: 
 7:         for (int i = 0; i < msgs.length; i++) {
 8:             if (matcher == null) {
 9:                 matcher = p.matcher(msgs[i]);
10:             } else {
11:                 matcher.reset(msgs[i]);
12:             }
13: 
14:             System.out.println(matcher.replaceAll("哈哈 :)"));
15:         }
16:     }
17: }

实际上,String 类中也提供了 replaceAll()、replaceFirst()、split() 等方法。

 1: public class StringReg {
 2:     public static void main(String[] args) {
 3:         String[] msgs = { "Java has regular expressions in 1.4", "regular expressions now expressing in Java", "Java represses oracular expressions"};
 4: 
 5:         for (String msg : msgs) {
 6:             System.out.println(msg.replaceFirst("re\\w*", "哈哈:)"));
 7:             System.out.println(Arrays.toString(msg.split(" ")));
 8:         }
 9:     }
10: }

Date: 2020-09-17 Thu 07:54

Author: Jack Liu

Created: 2020-09-18 Fri 15:48

Validate