Java-9_2-正则表达式与序列化

本篇将讨论正则表达式和对象序列化的内容。

A. 正则表达式

正则表达式就是通过定义、使用一些特殊含义的字符,以一个字符串来表示一类具有同样规律的字符串。这里首先介绍 Java 中正则表达式中的特殊字符,其次,介绍 Java 中利用正则表达式对字符串进行匹配的方式。

a. 基本语法

在 Java 的正则表达式中,. * + ? { | ( ) [ \ ^ $ 具有特殊含义。

  • . 匹配任意字符;
  • * 前面的字符重复任意次;
  • + 前面的字符重复1次或更多次;
  • ? 前面的字符出现 0 或 1 次;(e.g. be+s? 与 “bee”, “be”, “bees” 匹配)
  • | 表示 或,例如 “.(oo|ee)f”, 可以匹配 “beef” 或 “woof”。
  • Character class, 是用 “[]” 括起来的内容,例如,"[0-9]"。 其中 “-“ 表示 从 左边字符 到右边字符的范围内的所有字符。
  • predefined character class, 例如 “\d” (digits), “\p{sc}” Unicode 中的货币符号。(java 中的其他特殊含义字符集,参考书中表格p311)
  • ^, $ 分别匹配字符串的开头和结尾;
  • \, 在特殊符号前加 \, 表明不使用他们的特殊含义。
  • \Q, \E 包裹字符串,表明内部内容都是字符串内容,没有特殊含义。例如 “\Q(\$0.99)\E”, 表示字符串 “(\$0.99)”。

b. 寻找匹配值

判定字符串是否与正则表达式匹配有多种方式,参考下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 通过 Pattern 的静态方法 matches()
String regex = "[+-]?\\d+";
CharSequence input = ...;
if(Pattern.matches(regex, input)){
...
}

// 如要多次使用正则表达式,
// 先将正则表达式进行编译,再进行匹配,效率更高
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(input);
if(matcher.matches()){
...
}

// 如果判断一个 String 的集合类
Stream<String> strings = ...;
Stream<String> res = strings.filter(pattern.asPredicate());

如果想寻找 一段字符串/一个文件中 中所有与正则表达式相匹配的内容,可以通过下面的方式进行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 法1:通过 matcher 对象及 while 循环来寻找
String input = ...;
Matcher matcher = pattern.matcher(input);
while(matcher.find()){
String match = matcher.group();
int matchStart = matcher.start(); // 匹配到的字符部分的开头位置
int matchEnd = matcher.end();
}

// 法2:通过 results() 方法返回 Stream<MatchResult> 来处理
List<String> matches = pattern.matcher(input)
.results()
.map(Matcher::group)
.collect(Collectors.toList());

// 法3:读取文件内容,使用 scanner 来处理
Scanner in = new Scanner(path, "UTF-8");
Stream<String> words = in.findAll("\\pL+")
.map(MatchResult::group);

需要说明的是,对于上面的第2个方法,调用 Matcher 对象的 results() 方法,返回 Stream<MatchResult>, 其中 MatchResult 为一个接口,它有方法:group(), start(), end()(note1: 实际上,Matcher 类实现了 MatchResult 接口)(Note2: results() 方法是 java 9 新加入的方法,之前的 Matcher 类没有该方法)。

对于上面的方法3,Scanner 对象的 findAll() 方法返回 Stream<MatchResult>

group(): Matcher 类对象的 group() 方法返回匹配到的 括号中的内容 (就是寻找正则表达式中有括号的内容,然后在目标中寻找该内容并返回)。 group(n) 表示返回第 n 个括号所匹配到的内容,(Note: group(0) 返回整个输入。) 此外,我们在 括号中还可以加上标号 例如 (?<currency>[A-Z]{3}), 然后在寻找时 可以使用 matcher.group("currency"); 方法来寻找对应 label中的表达式在目标字符串中的匹配。 (matcherMatcher 类的实例)

c. 其他操作

将 字符串 依据特定的分隔符进行切割,可以使用下述方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String input = "1, 2, 3"; // 待处理的字符串

// 最简单写法
String[] tokens = input.split("\\s*,\\s*"); // 表示 旁边可能有空格的逗号

// 将正则表达式编译后,提高匹配效率
Pattern commas = Pattern.compile("\\s*,\\s*");
String[] tokens = commas.split(input); // 输出 ["1", "2", "3"]

// 使用流处理,实现 lazily 执行
Stream<String> tokens = commas.splitAsStream(input);

// 处理文件,使用 scanner 处理
Scanner in = new Scanner(path, "UTF-8");
in.useDelimiter("\\s*,\\s*");
Stream<String> tokens = in.tokens();

根据正则表达式,寻找到特定的字段后进行替换,可以使用如下几种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 将旁边可能有 空格 的 逗号全部替换为 没有空格的 逗号
// 最简单的方式
String res = input.replaceAll("\\s*,\\s*", ",");

// 提前进行编译正则表达式
Pattern commas = Pattern.compile("\\s*,\\s*");
Matcher matcher = commas.matcher(input);
String res = matcher.repalceAll(",");

// 额外的两个复杂些的例子
// 更加复杂的正则表达式
String res = "3:45".replaceAll(
"(\\d{1,2}):(?<minutes>\\d{2})",
"$1 hours and ${minutes} minutes");
// res 为 "3 hours and 45 minutes"

// 使用函数进行匹配对象的处理
String res = Pattern.compile("\\pL{4,}")
.matcher("Mary has a little lamb")
.replaceAll(m -> m.group().toUpperCase());
// 得到结果为 "MARY has a LITTLE LAMB"

在进行模式匹配的时候,我们还可以通过添加 flag 的方式,设定匹配的模式,例如:

1
2
3
4
// 在匹配的时候忽略大小写,并且使用 Unicode 字符集
Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CHARACTER_CLASS);
// 或者在写正则表达式的时候,在头部使用 flags 进行说明
String regex = "(iU:expression)"; // i 表示不区分大小写;U 表示 unicode 字符集

B. 序列化

序列化在计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式,以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。

序列化示例

一个类的对象必须实现了 Serializable 接口才能进行序列化,此外,还要求该类中所有的变量都是基本数据类型或者 已经是可序列化的 类的引用。(Note: Serializable 是一个 marker 接口,只做标记使用,内部没有方法。)

数组和集合类都是可序列化的对象。

下面,通过一组代码展示序列化与反序列化的过程:

1
2
3
4
5
6
7
8
9
10
// 将对象输出到序列。通过 ObjectOutputStream
ObjectOutputStream out = new ObjectOutputStream(
FIles.newOutputStream(path));
Employee peter = new Employee("peter", 90000);
out.writeObject(peter); // 将对象写出为序列化内容

// 读取序列化内容, 通过 ObjectInputStream
ObjectInputStream in = new ObjectInputStream(
Files.newInputStream(path));
Employee e1 = (Employee)in.readObject();

在序列化的过程中,对象的类名以及 所有实例变量的名字和值都会保存,基本数据类型的值被直接存储为 二进制数据,对象会接着调用 writeObject() 进行存储。

此外,在序列化对象时,还会额外保存对象类的版本信息 在 serialVersionUID 中。

需要注意的是,在序列化的时候,每一个对象会获得一个 序列号 (serial number),当进行序列化时,会先判断这个对象的序列号是否之前出现过,如果为重复的对象,则只会在序列化文件中写入序列号而不再重复对该对象进行序列化(保证了同一个对象的唯一性)。

如果不希望对某个实例变量进行序列化(例如存储 cache 内容的类),则使用 transient 修饰符进行修饰。