1. 文章
  2. Java 进阶线
  3. 数据处理与集合

Lambda、方法引用、排序与正则表达式

更新于 23 分钟阅读 第 5 / 15 站 #java#java-advanced#day05

把一个行为作为参数传递,并正确处理比较规则、查找前提和文本匹配。

我的学习 →

Java 进阶线 · 线路目录
本页导读

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));

Arrays.sort(double[]) 后打印的结果

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 表达式的写法)」,共三条:

  1. 参数类型可以省略不写。
  2. 如果只有一个参数,参数类型可以省略,同时 () 也可以省略。
  3. 如果 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 里只是调用一个实例方法,且第一个参数作为方法的主调,后面的参数都是该实例方法的入参
构造器引用类名::newLambda 里只是在创建对象,并且前后参数情况一致

4. 常见算法

4.1 简单认识算法

第 24 页的配图文字给出了三个递进的提问与回答:

  • 为什么要学习算法? 因为算法能力是编程思维的一部分,生活中到处是算法:地图导航的路线规划、网约车的派单与预计时间、直播间观众列表的排序,背后都是算法在算。
  • 什么是算法? 算法就是解决某个实际问题的过程和方法。

为什么学算法 · 导航路线规划

为什么学算法 · 打车路线与时间预估

为什么学算法 · 观众列表的排序规则

为什么学算法 · 编程思维对应的工程场景

第 29 页给出学习算法的技巧:

  1. 先搞清楚算法的流程。
  2. 直接去推敲如何写代码。

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(第几轮)比较位次数
00、1、23 次
10、12 次
201 次

以 int[] arr = {5, 2, 3, 1}; 为例,逐轮过程(课件第 30 页用「第一轮 / 第二轮 / 第三轮」三组表格配合动画演示,下表按算法流程与配套代码整理):

轮数 ij 的取值本轮比较与交换本轮结束后数组
第 1 轮0、1、25>2 交换 → [2,5,3,1];5>3 交换 → [2,3,5,1];5>1 交换 → [2,3,1,5][2, 3, 1, 5]
第 2 轮0、12<3 不动;3>1 交换 → [2,1,3,5][2, 1, 3, 5]
第 3 轮02>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(第几轮)比较位次数
01、2、33 次
12、32 次
231 次

以 int[] arr = {5, 1, 3, 2}; 为例(课件同样用三组表格 + 动画演示每轮结束后的数组状态,下表按算法流程与配套代码整理):

轮数 ij 的取值本轮比较与交换本轮结束后数组
第 1 轮1、2、31<5 交换 → [1,5,3,2];3<1 不换;2<1 不换[1, 5, 3, 2]
第 2 轮2、33<5 交换 → [1,3,5,2];2<3 交换 → [1,2,5,3][1, 2, 5, 3]
第 3 轮33<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(能查到):

轮次leftrightmidarr[mid]判断与处理
10738179 < 81 → 往左找,right = mid - 1 = 2
20212379 > 23 → 往右找,left = mid + 1 = 2
322279相等 → 返回索引 2

查找 150(查不到):

轮次leftrightmidarr[mid]判断与处理
107381150 > 81 → left = 4
2475127150 > 127 → left = 6
3676131150 > 131 → left = 7
4777147150 > 147 → left = 8
结束87——left > right,循环结束,未找到

第 44 页的结论:二分查找正常的折半条件应该是开始位置 left <= 结束位置 right。

第 45 页归纳了实现步骤:

  1. 定义变量记录左边和右边位置。
  2. 使用 while 循环控制二分查询(条件是左边位置 <= 右边位置)。
  3. 循环内部获取中间元素索引。
  4. 判断当前要找的元素如果大于中间元素,左边位置 = 中间索引 + 1。
  5. 判断当前要找的元素如果小于中间元素,右边位置 = 中间索引 - 1。
  6. 判断当前要找的元素如果等于中间元素,返回当前中间元素索引。
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
\d0-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)

作业要点:

  1. 选择题 9 题:Lambda 的格式与省略模式(错项是「多个参数之间用分号隔开」;-> 不可以省略)、函数式接口与 ::、Arrays.sort(T[], Comparator) 只能对引用类型数组排序、方法引用与构造器引用 int[]::new 的正确写法、冒泡与选择排序的对比(「冒泡效率最高」是错的,选择排序效率更高),以及两组正则完全匹配题(back / back-end;(010)-62661617 / 01062661617)。
  2. 今日单词:Arrays、Functional interface(函数式接口)、Method Reference(方法引用)、bubble sort(冒泡排序)、Binary search(二分查找)。
  3. 简答题 6 题:如何用 Arrays 实现降序排序;什么是面向函数式编程、与面向对象编程有何区别;()->{} 每个符号的含义;什么是方法引用、什么情况下可以使用;冒泡与选择排序的排序原理;正则表达式区分语言吗、有何作用。
  4. 排错题 4 题:① Arrays.sort(int[], Comparator<int>)——Comparator 不能用于基本类型数组,泛型也不能是 int;② 对无序数组 {5,2,3,1,4} 直接用 Arrays.binarySearch(arr, 5)——二分查找要求数组先有序,必须先排序;③ @FunctionalInterface 接口里同时写了 show() 和 toString()——toString 是 Object 的方法,接口中不允许重写 Object 的公共方法(应删掉);④ 给出的一段 Lambda 写法纠错。
  5. 代码题:第一题在 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. 复习要点

  1. Arrays 是操作数组的工具类:toString、copyOfRange(包前不包后)、copyOf(扩容)、setAll(按索引改写每个元素)、sort(默认升序)。
  2. 对象数组排序有两条路:类实现 Comparable 并重写 compareTo,或用带 Comparator 的 sort 重载方法传比较器;后者是课件标注的优先方案。
  3. 自定义比较规则的官方约定:左边大于右边返回正整数、小于返回负整数、等于返回 0;照此返回即为升序,对调运算顺序即得降序。浮点字段用 Double.compare(a, b)。
  4. Lambda 表达式是 JDK8 新增语法,只能简化函数式接口的匿名内部类;函数式接口就是「有且仅有一个抽象方法」的接口,常用 @FunctionalInterface 标注。
  5. Lambda 的三条省略规则:参数类型可省;只有一个参数时类型和 () 都可省;方法体只有一行时可省 {}、; 以及 return。
  6. 方法引用用 ::,是 Lambda 的进一步简化,条件是「Lambda 里只是调用一个已有方法且参数形式一致」:静态方法引用 类名::静态方法、实例方法引用 对象名::实例方法、特定类型方法引用 类型::方法、构造器引用 类名::new。
  7. 冒泡排序:共 数组长度 - 1 轮,第 i 轮比较 长度 - i - 1 次,相邻两两比较,大的往后换;选择排序:每轮以当前位置为基准,用它后面的较小值与之交换,两种写法(立即交换 / 记住最小索引最后交换)。
  8. 查找分两类:基本查找(顺序查找)从前往后挨个找,数据量大时性能差;二分查找(折半查找)要求数组有序,折半条件是 left <= right,mid = (left + right) / 2,比中间值大就往右找(left = mid + 1),比中间值小就往左找(right = mid - 1),找不到返回 -1。
  9. 正则表达式是用特定字符描述的规则,两大作用:校验数据格式是否合法、在一段文本中查找满足要求的内容;String.matches(regex) 用于整体匹配校验。
  10. 正则的书写规则由字符类([abc]、[^abc]、[a-z&&[^bc]])、预定义字符(.、\d、\s、\w 等)、数量词(?、*、+、{n,m})与其他符号(^、&&、()、|、(?i))组成;Java 中 \ 要写成 \\。
  11. 文本爬取四步:定义规则 → Pattern.compile → pattern.matcher → while (matcher.find()) 配合 group() 取整体、group(n) 取第 n 组;(.+) 是贪婪匹配,(.+?) 是非贪婪匹配。
  12. 搜索替换与分割用 replaceAll(regex, newStr) 和 split(regex),配合 \w+ 可以一键清理文本里的非中文字符或按非中文字符切分。