Lambda、方法引用、排序与正则表达式
把一个行为作为参数传递,并正确处理比较规则、查找前提和文本匹配。
继续上次小节 →Java 进阶线 · 线路目录
Java 进阶线
对象与类型设计3 站
文件、配置与并发4 站
网络、框架与复习3 站
本页导读
day05|java.lang 包下的常用 API、算法、正则表达式
本日从「数组怎么操作」出发,先用 Arrays 工具类收口数组的打印、拷贝、改写与排序,再顺着「对象数组怎么排序」引出 JDK8 的两大语法糖:Lambda 表达式与方法引用;后半程换到算法视角,用冒泡排序、选择排序、顺序查找、二分查找四个例子讲「先理清流程,再推敲代码」;最后是正则表达式,解决数据格式校验、文本内容爬取、搜索替换与分割三类问题。学完后应能回答:Lambda 到底简化了什么、什么条件下才能写成 类名::方法、二分查找为什么必须有序、matches / find / replaceAll / split 分别在什么场景用。
本日 PPT 目录(第 2、7、13、15、23、33、46 页反复出现的导航条):Arrays → JDK8 新特性:Lambda 表达式(认识 / 简化例子 / 省略规则) → JDK8 新特性:方法引用(静态方法引用、实例方法引用、特定类型方法引用、构造器引用) → 常见算法(简单认识算法、排序算法、查找算法) → 正则表达式(概述与初体验、书写规则、应用案例、用于查找信息、用于搜索替换与分割内容)。
本日不涉及异常:课件文件名里带了「异常」二字,但整份 PPT(61 页)没有任何异常相关知识页,第 60、61 页是结束动画,异常内容在 day06 展开。
1. Arrays 工具类
1.1 概述与常见方法
Arrays 是「用来操作数组的一个工具类」(第 3 页)。课件给出的常见方法如下(课件表格中 copyOf、setAll 的返回类型省略,这里按配套代码 d1_array/ArraysDemo1.java 补全):
| 方法名 | 说明 |
|---|---|
public static String toString(类型[] arr) | 返回数组的内容 |
public static int[] copyOfRange(类型[] arr, 起始索引, 结束索引) | 拷贝数组(指定范围) |
public static 类型[] copyOf(类型[] arr, int newLength) | 拷贝数组 |
public static void setAll(double[] array, IntToDoubleFunction generator) | 把数组中的原数据改为新数据 |
public static void sort(类型[] arr) | 对数组进行排序(默认是升序排序) |
public static <T> void sort(T[] arr, Comparator<? super T> c) | 对数组进行排序(支持自定义排序规则)(第 5 页) |
配套代码把这几个方法串了一遍:
public class ArraysDemo1 {
public static void main(String[] args) {
// 1、返回数组内容
int[] arr = {11, 55, 33, 22, 98};
System.out.println(Arrays.toString(arr));
// 2、拷贝数组的指定范围(包前不包后)
int[] arr2 = Arrays.copyOfRange(arr, 1, 4);
System.out.println(Arrays.toString(arr2));
// 3、给数组扩容:新长度大于原长度时后面补默认值
int[] arr3 = Arrays.copyOf(arr, 10);
System.out.println(Arrays.toString(arr3));
// 4、修改数组中每个数据,再存入
double[] scores = {99.5, 90, 59.5, 78, 98, 55};
Arrays.setAll(scores, new IntToDoubleFunction() {
@Override
public double applyAsDouble(int index) {
return scores[index] + 10;
}
});
System.out.println(Arrays.toString(scores));
// 5、排序(默认升序)
Arrays.sort(scores);
System.out.println(Arrays.toString(scores));
}
}
1.2 演示:数值数组的排序
第 4 页用「对数组中的数据进行排序」引出 Arrays.sort():
double[] prices = {99.8, 128, 100};
Arrays.sort(prices);
System.out.println(Arrays.toString(prices));
1.3 对象数组怎么排序:两种方式
紧接着课件提问:「如果数组中存储的是对象,如何排序?」(第 5 页)以 Student 对象数组为例,给出了两条路:
Student[] students = new Student[4];
students[0] = new Student("蜘蛛精", 169.5, 23);
students[1] = new Student("紫霞", 163.8, 26);
students[2] = new Student("紫霞", 163.8, 26);
students[3] = new Student("至尊宝", 167.5, 24);
| 方式 | 做法 | 调用 |
|---|---|---|
| 方式一 | 让该对象的类实现 Comparable(比较规则)接口,然后重写 compareTo() 方法,自己来制定比较规则 | Arrays.sort(students); |
| 方式二 | 使用带比较器的 sort 重载方法,创建 Comparator 比较器接口的匿名内部类对象,自己制定比较规则 | Arrays.sort(students, new Comparator<Student>() { ... }); |
// 方式一:类实现 Comparable,重写 compareTo()
public class Student implements Comparable<Student> {
// ... 属性与构造器省略 ...
// 比较者:this;被比较者:o
@Override
public int compareTo(Student o) {
// return this.age - o.age; // 升序
return o.age - this.age; // 降序
}
}
// 方式二:sort 重载方法 + Comparator 匿名内部类(课件标注:优先使用这种)
Arrays.sort(students, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return Double.compare(o1.getHeight(), o2.getHeight());
}
});
1.4 比较规则的官方约定
第 6 页给出「自定义排序规则时需要遵循的官方约定」:
| 判断 | 返回值 |
|---|---|
| 认为左边对象 大于 右边对象 | 返回正整数 |
| 认为左边对象 小于 右边对象 | 返回负整数 |
| 认为左边对象 等于 右边对象 | 返回 0 |
按这个约定返回,Arrays.sort() 默认就是升序;想要降序,把 this 与 o(或 o1 与 o2)的运算顺序对调即可,例如 o2.age - o1.age。
配套代码 d1_array/Student.java 的注释把这一点说得很直白:「只要这么干,默认就是升序排序」。浮点字段不能直接相减,要用 Double.compare(a, b) 来产生正负整数。
(课件表格笔误:第 6 页第三行写成「应该返回 0 整数」,应为「返回 0」。)
2. JDK8 新特性:Lambda 表达式
2.1 认识 Lambda 表达式
第 8 页的结论:Lambda 表达式是 JDK 8 开始新增的一种语法形式,作用是简化匿名内部类的代码写法。
它简化的是函数式接口的匿名内部类:
- 什么是函数式接口?有且仅有一个抽象方法的接口。
- 格式:
(被重写方法的形参列表) -> {
被重写方法的方法体代码。
}
- 注意:将来见到的大部分函数式接口,上面都可能会有一个
@FunctionalInterface注解,有该注解的接口就必定是函数式接口。 - 注意:Lambda 表达式只能简化函数式接口的匿名内部类(第 9 页小结:作用 = 简化函数式接口的匿名内部类;函数式接口 = 只有一个抽象方法的接口)。
配合代码的写法是:用 @FunctionalInterface 标注接口,再用 Lambda 提供实现。
@FunctionalInterface // 函数式接口中有且仅有一个抽象方法
interface Swimming {
void swim();
}
public class LambdaTest1 {
public static void main(String[] args) {
// 匿名内部类写法
Swimming s1 = new Swimming() {
@Override
public void swim() {
System.out.println("学生游泳贼溜");
}
};
s1.swim();
// Lambda 写法:可以上下文推断出真实的代码形式
Swimming s2 = () -> {
System.out.println("老师游泳贼溜");
};
s2.swim();
}
}
(配套代码 d2_lambda/LambdaTest1.java 还演示了一个错误示范:把一个抽象类 Animal 的匿名内部类改写成 Lambda 会编译失败,因为抽象类不是函数式接口。)
2.2 例子一:简化 Arrays.setAll() 的匿名内部类
第 11 页的需求是给分数打八折:
double[] scores = {99.8, 128.0, 100.0};
// 匿名内部类写法
Arrays.setAll(scores, new IntToDoubleFunction() {
@Override
public double applyAsDouble(int value) {
return scores[value] * 0.8;
}
});
System.out.println(Arrays.toString(scores));
// Lambda 写法
Arrays.setAll(scores, (int value) -> {
return scores[value] * 0.8;
});
IntToDoubleFunction 只有一个抽象方法 applyAsDouble(int value),所以可以直接用 Lambda 顶替匿名内部类。
2.3 例子二:简化 Comparator 接口的匿名形式
第 12 页沿用 Student 数组:
public static void main(String[] args) {
// ...
// 匿名内部类写法
Arrays.sort(students, new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
return s1.getAge() - s2.getAge();
}
});
// Lambda 写法
Arrays.sort(students, (Student s1, Student s2) -> {
return s1.getAge() - s2.getAge();
});
}
(课件文字里年龄相减的减号被写成了长破折号「–」,实际代码应为半角减号 -。)
2.4 省略规则
第 14 页给出「省略写法(进一步简化 Lambda 表达式的写法)」,共三条:
- 参数类型可以省略不写。
- 如果只有一个参数,参数类型可以省略,同时
()也可以省略。 - 如果 Lambda 表达式中的方法体代码只有一行,可以省略大括号不写,同时要省略分号!此时,如果这行代码是
return语句,也必须去掉return不写。
配套代码 d2_lambda/LambdaTest2.java 用 setAll 和 sort 各演示了一遍逐级简化的过程:
// 从完整写法开始,一路简化到最短形式
Arrays.setAll(scores, (int index) -> {
return scores[index] + 10;
});
Arrays.setAll(scores, (index) -> {
return scores[index] + 10;
});
Arrays.setAll(scores, index -> {
return scores[index] + 10;
});
Arrays.setAll(scores, index -> scores[index] + 10); // 省略 {}、; 与 return
Arrays.sort(students, (Student o1, Student o2) -> {
return Double.compare(o1.getHeight(), o2.getHeight());
});
Arrays.sort(students, (o1, o2) -> {
return Double.compare(o1.getHeight(), o2.getHeight());
});
Arrays.sort(students, (o1, o2) -> Double.compare(o1.getHeight(), o2.getHeight()));
3. JDK8 新特性:方法引用
方法引用的标志性符号是 ::(第 15 页配图文字:「方法引用的标志性符号 ::」)。只有当 Lambda 表达式「只是调用一个已有方法」时,才可以进一步改写成方法引用——四种形式的判断标准,课件总结为「参数形式一致」。
3.1 静态方法引用
- 格式:
类名::静态方法。 - 使用场景:如果某个 Lambda 表达式里只是调用一个静态方法,并且前后参数的形式一致,就可以使用静态方法引用。
// 目标:理解静态方法引用
Student[] students = new Student[4];
students[0] = new Student("周芷若", 21, '女', 169.3);
students[1] = new Student("殷素素", 38, '女', 172.2);
students[2] = new Student("小昭", 19, '女', 168.5);
students[3] = new Student("张无忌", 23, '男', 183.5);
// 1、完全自定义比较规则
Arrays.sort(students, (o1, o2) -> Double.compare(o1.getHeight(), o2.getHeight()));
// 2、Lambda 里只调用了一个静态方法
Arrays.sort(students, (o1, o2) -> Student.compareByHeight(o1, o2));
// 3、静态方法引用(终极简化代码)
Arrays.sort(students, Student::compareByHeight);
System.out.println(Arrays.toString(students));
其中 Student.compareByHeight 就是类里定义的一个静态方法:
public static int compareByHeight(Student o1, Student o2) {
return Double.compare(o1.getHeight(), o2.getHeight());
}
3.2 实例方法引用
- 格式:
对象名::实例方法。 - 使用场景:如果某个 Lambda 表达式里只是调用一个实例方法,并且前后参数的形式一致,就可以使用实例方法引用。
public class Test2 {
public static void main(String[] args) {
// ... 准备 students 数组,省略 ...
Test2 t = new Test2();
// 简化前:Arrays.sort(students, (o1, o2) -> t.compare(o1, o2));
Arrays.sort(students, t::compare);
}
public int compare(Student o1, Student o2) {
return Double.compare(o1.getHeight(), o2.getHeight());
}
}
注意与静态方法引用的区别:这里被调用的方法属于某个对象,所以引用的是「对象名」而不是「类名」。
3.3 特定类型的方法引用
- 格式:
类型::方法。 - 使用场景:如果某个 Lambda 表达式里只是调用一个实例方法,并且前面参数列表中的第一个参数是作为方法的主调,后面的所有参数都是作为该实例方法的入参,则此时就可以使用特定类型的方法引用。
String[] names = {"dlei", "Angela", "baby", "caocao", "Coach", "曹操", "deby", "eason", "andy"};
// 1、匿名内部类
Arrays.sort(names, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareToIgnoreCase(o2); // o1 是主调,o2 是入参
}
});
// 2、Lambda
Arrays.sort(names, (o1, o2) -> o1.compareToIgnoreCase(o2));
// 3、特定类型的方法引用(忽略大小写排序)
Arrays.sort(names, String::compareToIgnoreCase);
System.out.println(Arrays.toString(names));
这里 o1.compareToIgnoreCase(o2) 中 o1 恰好是第一个参数,类型是 String,所以可以写成 String::compareToIgnoreCase。
3.4 构造器引用
- 格式:
类名::new。 - 使用场景:如果某个 Lambda 表达式里只是在创建对象,并且前后参数情况一致,就可以使用构造器引用。
public class Test4 {
public static void main(String[] args) {
// 匿名内部类写法:new Create() { public Car createCar(String name) { return new Car(name); } }
// Lambda 写法:Create c1 = name -> new Car(name);
// 构造器引用
Create c1 = Car::new;
Car car = c1.createCar("布加迪威龙");
System.out.println(car);
}
}
@FunctionalInterface // 函数式接口
interface Create {
Car createCar(String name);
}
3.5 四种方法引用对照
| 形式 | 写法 | 使用场景(课件表述) |
|---|---|---|
| 静态方法引用 | 类名::静态方法 | Lambda 里只是调用一个静态方法,并且前后参数的形式一致 |
| 实例方法引用 | 对象名::实例方法 | Lambda 里只是调用一个实例方法,并且前后参数的形式一致 |
| 特定类型的方法引用 | 类型::方法 | Lambda 里只是调用一个实例方法,且第一个参数作为方法的主调,后面的参数都是该实例方法的入参 |
| 构造器引用 | 类名::new | Lambda 里只是在创建对象,并且前后参数情况一致 |
4. 常见算法
4.1 简单认识算法
第 24 页的配图文字给出了三个递进的提问与回答:
- 为什么要学习算法? 因为算法能力是编程思维的一部分,生活中到处是算法:地图导航的路线规划、网约车的派单与预计时间、直播间观众列表的排序,背后都是算法在算。
- 什么是算法? 算法就是解决某个实际问题的过程和方法。
第 29 页给出学习算法的技巧:
- 先搞清楚算法的流程。
- 直接去推敲如何写代码。
4.2 排序算法:升序与降序
第 26~28 页用同一个数组演示了「排序 → 升序排序 → 降序排序」:
7 23 79 81 103 127 131 147
0 1 2 3 4 5 6 7
- 排序:把一组数据按某个规则排列。
- 升序排序:由小到大。
- 降序排序:由大到小。
4.3 冒泡排序
第 30 页的定位是:「每次从数组中找出最大值放在数组的后面去。」(更准确的表述:冒泡排序通过相邻两个元素两两比较,把本轮的最大值一步步交换到末尾。)
实现冒泡排序的关键步骤分析:
- 确定总共需要做几轮:数组的长度 - 1。
- 每轮比较几次:当前位置大于后一个位置则交换数据,次数规律为 数组的长度 - i - 1。
| i(第几轮) | 比较位 | 次数 |
|---|---|---|
| 0 | 0、1、2 | 3 次 |
| 1 | 0、1 | 2 次 |
| 2 | 0 | 1 次 |
以 int[] arr = {5, 2, 3, 1}; 为例,逐轮过程(课件第 30 页用「第一轮 / 第二轮 / 第三轮」三组表格配合动画演示,下表按算法流程与配套代码整理):
| 轮数 i | j 的取值 | 本轮比较与交换 | 本轮结束后数组 |
|---|---|---|---|
| 第 1 轮 | 0、1、2 | 5>2 交换 → [2,5,3,1];5>3 交换 → [2,3,5,1];5>1 交换 → [2,3,1,5] | [2, 3, 1, 5] |
| 第 2 轮 | 0、1 | 2<3 不动;3>1 交换 → [2,1,3,5] | [2, 1, 3, 5] |
| 第 3 轮 | 0 | 2>1 交换 → [1,2,3,5] | [1, 2, 3, 5] |
代码实现(配套代码 d4_sf/Demo1.java):
public class Demo1 {
public static void main(String[] args) {
int[] arr = {5, 2, 3, 1};
// 0 1 2 3
// 1、外层循环控制冒几轮
for (int i = 0; i < arr.length - 1; i++) {
// 轮数(i) 每轮的次数 j 的取值
// 第一轮 0 3 0 1 2
// 第二轮 1 2 0 1
// 第三轮 2 1 0
// 2、内层循环控制每轮比较几次
for (int j = 0; j < arr.length - i - 1; j++) {
// 3、当前位置大于后一个位置则交换
if (arr[j] > arr[j + 1]) {
int temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
}
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 5]
}
}
4.4 选择排序
第 32 页:每轮选择当前位置,开始找出后面的较小值与该位置交换。
选择排序的关键:
- 确定总共需要选择几轮:数组的长度 - 1。
- 控制每轮以当前位置为基准,与后面元素选择几次。
| i(第几轮) | 比较位 | 次数 |
|---|---|---|
| 0 | 1、2、3 | 3 次 |
| 1 | 2、3 | 2 次 |
| 2 | 3 | 1 次 |
以 int[] arr = {5, 1, 3, 2}; 为例(课件同样用三组表格 + 动画演示每轮结束后的数组状态,下表按算法流程与配套代码整理):
| 轮数 i | j 的取值 | 本轮比较与交换 | 本轮结束后数组 |
|---|---|---|---|
| 第 1 轮 | 1、2、3 | 1<5 交换 → [1,5,3,2];3<1 不换;2<1 不换 | [1, 5, 3, 2] |
| 第 2 轮 | 2、3 | 3<5 交换 → [1,3,5,2];2<3 交换 → [1,2,5,3] | [1, 2, 5, 3] |
| 第 3 轮 | 3 | 3<5 交换 → [1,2,3,5] | [1, 2, 3, 5] |
写法一:一发现更小值就立刻交换(配套代码 d4_sf/Demo2.java)。
int[] arr = {5, 1, 3, 2};
// 0 1 2 3
for (int i = 0; i < arr.length - 1; i++) {
// 轮数(i) 次数 j 的取值
// 0 3 1 2 3
// 1 2 2 3
// 2 1 3
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[i]) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
System.out.println(Arrays.toString(arr));
写法二:先用变量记住本轮最小值索引,本轮结束再交换一次(配套代码 d4_sf/Demo2_2.java,交换次数更少)。
for (int i = 0; i < arr.length - 1; i++) {
int min = i; // 记住本轮最小值对应的索引
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min]) {
min = j;
}
}
if (min != i) { // 本轮结束再交换
int temp = arr[i];
arr[i] = arr[min];
arr[min] = temp;
}
}
4.5 查找算法之一:基本查找(顺序查找)
第 34 页:
81 103 131 127 147 23 7 79
基本查找就是「从前往后挨个找」(又叫顺序查找),找 81 时第一个元素就命中。课件提示:在数据量特别大的时候,基本查找这种从前往后挨个找的形式,性能是很差的!
4.6 查找算法之二:二分查找(折半查找)
第 35~45 页。两个前提与核心思想:
- 前提条件:数组中的数据必须是有序的。
- 核心思想:每次排除一半的数据,查询数据的性能明显提高极多!
- 中间位置:
mid = (left + right) / 2。
以数组 {7, 23, 79, 81, 103, 127, 131, 147}(索引 0~7)为例。
查找 79(能查到):
| 轮次 | left | right | mid | arr[mid] | 判断与处理 |
|---|---|---|---|---|---|
| 1 | 0 | 7 | 3 | 81 | 79 < 81 → 往左找,right = mid - 1 = 2 |
| 2 | 0 | 2 | 1 | 23 | 79 > 23 → 往右找,left = mid + 1 = 2 |
| 3 | 2 | 2 | 2 | 79 | 相等 → 返回索引 2 |
查找 150(查不到):
| 轮次 | left | right | mid | arr[mid] | 判断与处理 |
|---|---|---|---|---|---|
| 1 | 0 | 7 | 3 | 81 | 150 > 81 → left = 4 |
| 2 | 4 | 7 | 5 | 127 | 150 > 127 → left = 6 |
| 3 | 6 | 7 | 6 | 131 | 150 > 131 → left = 7 |
| 4 | 7 | 7 | 7 | 147 | 150 > 147 → left = 8 |
| 结束 | 8 | 7 | — | — | left > right,循环结束,未找到 |
第 44 页的结论:二分查找正常的折半条件应该是开始位置 left <= 结束位置 right。
第 45 页归纳了实现步骤:
- 定义变量记录左边和右边位置。
- 使用
while循环控制二分查询(条件是左边位置<=右边位置)。 - 循环内部获取中间元素索引。
- 判断当前要找的元素如果大于中间元素,左边位置 = 中间索引 + 1。
- 判断当前要找的元素如果小于中间元素,右边位置 = 中间索引 - 1。
- 判断当前要找的元素如果等于中间元素,返回当前中间元素索引。
public static int searchDataIndex(int[] array, int number) {
// array = {7, 23, 79, 81, 103, 127, 131, 147}
// 1、定义头尾指针
int left = 0;
int right = array.length - 1;
// 2、开始折半查询
while (left <= right) {
// 3、取中间索引
int middle = (left + right) / 2;
// 4、判断当前要找的数据与中间位置处的数据大小情况
if (number > array[middle]) {
left = middle + 1; // 5、往右边找
} else if (number < array[middle]) {
right = middle - 1; // 6、往左边找
} else {
return middle; // 找到,返回索引
}
}
return -1; // 循环结束仍未找到
}
调用效果:searchDataIndex(array, 79) 返回 2;searchDataIndex(array, 179) 返回 -1。
5. 正则表达式
5.1 概述与初体验
第 47 页给出定义与两大作用:
- 定义:正则表达式就是由一些特定的字符组成,代表的是一个规则。
- 作用一:用来校验数据格式是否合法(例:电话
13811112222、10233233A、020-3212323;邮箱dlei@163.com、dlei#gmail.com;QQ 号12abc、25666666、09999)。 - 作用二:在一段文本中查找满足要求的内容。
String 提供了一个匹配正则表达式的方法(第 49 页):
| 方法名 | 说明 |
|---|---|
public boolean matches(String regex) | 判断字符串是否匹配正则表达式,匹配返回 true,不匹配返回 false |
用「校验 QQ 号」做初体验:需求是「号码必须 5 位以上、全部是数字、不能以 0 开头」。不用正则要写一堆判断,用了正则只要一行:
public static boolean checkQQ(String qq) {
// 不用正则:逐个条件判断 + 逐位检查字符
if (qq == null || qq.startsWith("0") || qq.length() <= 5) {
return false;
}
for (int i = 0; i < qq.length(); i++) {
char c = qq.charAt(i);
if (c < '0' || c > '9') {
return false;
}
}
return true;
}
public static boolean checkQQ2(String qq) {
// 用正则:一位不能是 0,后面至少 5 位数字
return qq != null && qq.matches("[1-9]\\d{5,}");
}
5.2 书写规则:字符类、预定义字符、数量词
字符类(只匹配单个字符):
| 规则 | 含义 |
|---|---|
[abc] | 只能是 a、b 或 c |
[^abc] | 除了 a、b、c 之外的任何字符 |
[a-zA-Z] | a 到 z、A 到 Z(范围) |
[a-d[m-p]] | a 到 d,或 m 到 p |
[a-z&&[def]] | d、e 或 f(交集) |
[a-z&&[^bc]] | a 到 z,除了 b 和 c(等同于 [ad-z]) |
[a-z&&[^m-p]] | a 到 z,除了 m 到 p(等同于 [a-lq-z]) |
预定义字符(只匹配单个字符):
| 规则 | 含义 |
|---|---|
. | 任何字符 |
\d | 一个数字:[0-9] |
\D | 非数字:[^0-9] |
\s | 一个空白字符 |
\S | 非空白字符:[^\s] |
\w | [a-zA-Z_0-9] |
\W | [^\w],一个非单词字符 |
数量词:
| 规则 | 含义 |
|---|---|
X? | X,一次或 0 次 |
X* | X,零次或多次 |
X+ | X,一次或多次 |
X{n} | X,正好 n 次 |
X{n,} | X,至少 n 次 |
X{n,m} | X,至少 n 但不超过 m 次 |
第 50 页的小结表把常用符号集中列了一遍:
| 符号 | 含义 | 举例 |
|---|---|---|
[] | 里面的内容出现一次 | [abc] |
^ | 取反 | [^abc] |
&& | 交集,不能写单个的 & | [a-z&&m-p] |
. | 任意字符 | \n 回车符号不匹配 |
\ | 转义字符 | \\d |
\d | 0-9 | \d+ |
\D | 非 0-9 | \D+ |
\s | 空白字符 | — |
\S | 非空白字符 | [^\s] |
\w | 单词字符 | [a-zA-Z_0-9] |
\W | 非单词字符 | [^\w] |
() | 分组 | a(bc)+ |
| | 写在方括号外面表示并集 | ab|AB |
? | 0 次或 1 次 | \d? |
* | 0 次或多次 | \d*、(abc)* |
+ | 1 次或多次 | \d+、(abc)+ |
{} | 具体次数 | a{7}、\d{7,19} |
(?i) | 忽略后面字符的大小写 | (?i)abc;a((?i)b)c 只忽略 b 的大小写 |
在 Java 中要特别注意转义:\ 有特殊用途,不能独立存在,写正则时必须用 \\ 表示一个 \(配套代码注释:「\ 需要再使用 \ 转义」)。上面带 [内容] 的字符类和预定义字符都只能匹配单个字符,要匹配多个字符必须配合数量词。
5.3 应用案例一:校验数据格式
第 52 页的需求:校验用户输入的电话、邮箱、时间是否合法。课件示例中的做法(配套代码 d5_regex/RegexTest3.java)是「循环 + matches,不合法就重新输入」:
public static void checkPhone() {
while (true) {
Scanner sc = new Scanner(System.in);
System.out.println("请您输入手机号码:");
String phone = sc.next();
if (phone.matches("1[3-9]\\d{9}")) {
System.out.println("手机号码合法,录入成功!");
break;
} else {
System.out.println("您输入的手机号码有毛病!请重新输入!");
}
}
}
public static void checkEmail() {
while (true) {
Scanner sc = new Scanner(System.in);
System.out.println("请您输入邮箱:");
String email = sc.next();
// 示例:dlei0009@163.com、5423253@qq.com、xulei2@itcast.com.cn
if (email.matches("\\w{2,30}@\\w{2,20}(\\.\\w{2,20}){1,2}")) {
System.out.println("邮箱合法,录入成功!");
break;
} else {
System.out.println("您输入的邮箱有毛病!请重新输入!");
}
}
}
5.4 应用案例二:用于查找信息(爬取)
第 54~57 页给出三段文本爬取的需求,套路固定为四步:定义规则 → 封装成 Pattern → 得到 Matcher → 循环 find() 取内容。
需求 1:把下面文本中的电话、邮箱、座机号码、热线都爬取出来。
String data = "来黑马程序员学习Java,\n" +
"电话:18512516758,18512508907\n" +
"或者联系邮箱: boniu@itcast.cn\n" +
"座机电话:01036517895,010-98951256\n" +
"邮箱:bozai@itcast.cn,\n" +
"邮箱2:dlei0009@163.com,\n" +
"热线电话:400-618-9090 ,400-618-4000,4006184000,4006189090";
// 1、定义爬取规则(正则表达式)
String regex = "(\\w{1,}@\\w{2,10}(\\.\\w{2,10}){1,2})|" +
"(1[3-9]\\d{9})|(0\\d{2,5}-?\\d{5,15})|400-?\\d{3,8}-?\\d{3,8}";
// 2、把正则表达式封装成一个 Pattern 对象
Pattern pattern = Pattern.compile(regex);
// 3、通过 pattern 对象得到查找内容的匹配器
Matcher matcher = pattern.matcher(data);
// 4、通过匹配器开始去内容中查找信息
while (matcher.find()) {
System.out.println(matcher.group()); // 取出信息
}
(配套代码 RegexTest4.java 的规则更严格一些:(\w{2,30}@\w{2,20}(\.\w{2,20}){1,2})|(1[3-9]\d{9})|(0\d{2,6}-?[1-9]\d{3,10})|(400-?[1-9]\d{2,6}-?[1-9]\d{2,6})。)
需求 2:只需要把每个邮箱中的用户名爬取出来——关键在于用分组 () 圈住要单独取出的部分,再用 matcher.group(1) 取第一组:
String regex = "(\\w{2,})@\\w{2,20}(\\.\\w{2,10}){1,2}";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(data);
while (matcher.find()) {
System.out.println(matcher.group(1)); // 取出第一组信息(用户名)
}
需求 3:把日志里进入系统的用户名爬出来,这里要区分贪婪匹配与非贪婪匹配:
String data = "欢迎张全蛋光临本系统!他删库并跑路,欢迎李二狗子光临本系统!" +
"欢迎马六子光临本系统!它浏览了很多好看的照片!欢迎夏洛光临本系统!他在六点钟购买了一台拖拉机!";
String regex = "欢迎(.+)光临"; // .+ 是贪婪匹配:最大范围匹配
String regex2 = "欢迎(.+?)光临"; // .+? 是非贪婪匹配
Pattern pattern = Pattern.compile(regex2);
Matcher matcher = pattern.matcher(data);
while (matcher.find()) {
String rs = matcher.group(); // 找到的整段内容
System.out.println(rs);
String rs2 = matcher.group(1); // 第一组内容,就是人的名字
System.out.println(rs2);
}
用贪婪的 .+ 时,一整段「欢迎……光临」会被当成一个匹配;用非贪婪的 .+? 才会一次只匹配到最近的一个「光临」,从而把每个名字分别取出来。
5.5 应用案例三:用于搜索替换、分割内容
第 59 页给出需要配合的 String 方法:
| 方法名 | 说明 |
|---|---|
public String replaceAll(String regex, String newStr) | 按照正则表达式匹配的内容进行替换 |
public String[] split(String regex) | 按照正则表达式匹配的内容进行分割字符串,返回一个字符串数组 |
String s1 = "古力娜扎ai8888迪丽热巴99fafas9aa5566马尔扎哈fbbADFFfsfs42425卡尔扎巴";
// 1、把所有非中文字符替换成 "-"
String result = s1.replaceAll("\\w+", "-");
System.out.println(result);
// 2、把人名分割出来
String[] names = s1.split("\\w+");
for (int i = 0; i < names.length; i++) {
System.out.println(names[i]);
}
6. 作业与练习
- 作业文档:
day05-常用API(二):Lambda、方法引用详解/day05作业/作业05/day05作业.md - 配套代码:
day05-常用API(二):Lambda、方法引用详解/代码/day05-api(d1_array、d2_lambda、d3_method_reference、d4_sf、d5_regex)
作业要点:
- 选择题 9 题:Lambda 的格式与省略模式(错项是「多个参数之间用分号隔开」;
->不可以省略)、函数式接口与::、Arrays.sort(T[], Comparator)只能对引用类型数组排序、方法引用与构造器引用int[]::new的正确写法、冒泡与选择排序的对比(「冒泡效率最高」是错的,选择排序效率更高),以及两组正则完全匹配题(back/back-end;(010)-62661617/01062661617)。 - 今日单词:
Arrays、Functional interface(函数式接口)、Method Reference(方法引用)、bubble sort(冒泡排序)、Binary search(二分查找)。 - 简答题 6 题:如何用
Arrays实现降序排序;什么是面向函数式编程、与面向对象编程有何区别;()->{}每个符号的含义;什么是方法引用、什么情况下可以使用;冒泡与选择排序的排序原理;正则表达式区分语言吗、有何作用。 - 排错题 4 题:①
Arrays.sort(int[], Comparator<int>)——Comparator不能用于基本类型数组,泛型也不能是int;② 对无序数组{5,2,3,1,4}直接用Arrays.binarySearch(arr, 5)——二分查找要求数组先有序,必须先排序;③@FunctionalInterface接口里同时写了show()和toString()——toString是Object的方法,接口中不允许重写Object的公共方法(应删掉);④ 给出的一段 Lambda 写法纠错。 - 代码题:第一题在
sort项目Entry类里补全「学生按身高从小到大排队」,先用冒泡排序、再用选择排序比较身高;第二题在supplier项目里用Supplier<String>生成四位随机验证码(字符从OPTIONS_CAHS中获取);第三题在function项目里用Function<String[], String>实现点名器(从userNames数组随机取一个元素);第四题(选做)在lottery项目里按权重抽奖:一等奖 1、二等奖 2、三等奖 3、谢谢惠顾 4,权重区间为[0,1)、[1,3)、[3,6)、[6,10),生成随机权重数后用二分查找定位奖项。
7. 复习要点
Arrays是操作数组的工具类:toString、copyOfRange(包前不包后)、copyOf(扩容)、setAll(按索引改写每个元素)、sort(默认升序)。- 对象数组排序有两条路:类实现
Comparable并重写compareTo,或用带Comparator的sort重载方法传比较器;后者是课件标注的优先方案。 - 自定义比较规则的官方约定:左边大于右边返回正整数、小于返回负整数、等于返回 0;照此返回即为升序,对调运算顺序即得降序。浮点字段用
Double.compare(a, b)。 - Lambda 表达式是 JDK8 新增语法,只能简化函数式接口的匿名内部类;函数式接口就是「有且仅有一个抽象方法」的接口,常用
@FunctionalInterface标注。 - Lambda 的三条省略规则:参数类型可省;只有一个参数时类型和
()都可省;方法体只有一行时可省{}、;以及return。 - 方法引用用
::,是 Lambda 的进一步简化,条件是「Lambda 里只是调用一个已有方法且参数形式一致」:静态方法引用类名::静态方法、实例方法引用对象名::实例方法、特定类型方法引用类型::方法、构造器引用类名::new。 - 冒泡排序:共
数组长度 - 1轮,第 i 轮比较长度 - i - 1次,相邻两两比较,大的往后换;选择排序:每轮以当前位置为基准,用它后面的较小值与之交换,两种写法(立即交换 / 记住最小索引最后交换)。 - 查找分两类:基本查找(顺序查找)从前往后挨个找,数据量大时性能差;二分查找(折半查找)要求数组有序,折半条件是
left <= right,mid = (left + right) / 2,比中间值大就往右找(left = mid + 1),比中间值小就往左找(right = mid - 1),找不到返回-1。 - 正则表达式是用特定字符描述的规则,两大作用:校验数据格式是否合法、在一段文本中查找满足要求的内容;
String.matches(regex)用于整体匹配校验。 - 正则的书写规则由字符类(
[abc]、[^abc]、[a-z&&[^bc]])、预定义字符(.、\d、\s、\w等)、数量词(?、*、+、{n,m})与其他符号(^、&&、()、|、(?i))组成;Java 中\要写成\\。 - 文本爬取四步:定义规则 →
Pattern.compile→pattern.matcher→while (matcher.find())配合group()取整体、group(n)取第 n 组;(.+)是贪婪匹配,(.+?)是非贪婪匹配。 - 搜索替换与分割用
replaceAll(regex, newStr)和split(regex),配合\w+可以一键清理文本里的非中文字符或按非中文字符切分。