Java 正则表达式校验字符串源码:从晦涩到明晰

335 阅读6分钟

在Java中,正则表达式(Regular Expressions, regex)是一种强大的工具,用于字符串模式匹配和文本处理。它允许你定义复杂的搜索模式,从而可以高效地验证字符串是否符合某个特定的模式。这篇文章将详细介绍Java中使用正则表达式来校验字符串的实现原理及示例。

正则表达式的基本概念

  1. 元字符:特殊字符,用于构建复杂的搜索模式。常见的元字符包括:

    • .: 匹配任意字符
    • *: 匹配前面的子表达式零次或多次
    • +: 匹配前面的子表达式一次或多次
    • ?: 匹配前面的子表达式零次或一次
    • []: 字符类,用于匹配方括号内的任意字符
    • ^: 匹配字符串的开始
    • $: 匹配字符串的结束
    • |: 表示逻辑或
    • (): 用于分组
  2. 预定义字符类

    • \d: 匹配任何数字,相当于[0-9]
    • \w: 匹配任何字母数字字符,相当于[a-zA-Z_0-9]
    • \s: 匹配任何空白字符,包括空格、制表符等
  3. 数量词

    • {n}: 匹配前面的子表达式恰好n次
    • {n,}: 匹配前面的子表达式至少n次
    • {n,m}: 匹配前面的子表达式至少n次,但不超过m次

Java中的正则表达式API

Java提供了java.util.regex包中的两个核心类来处理正则表达式:

  1. Pattern类:表示编译后的正则表达式。
  2. Matcher类:用于对输入字符串进行模式匹配操作。

使用步骤

  1. 编译正则表达式:通过Pattern.compile(String regex)方法,将正则表达式字符串编译成一个Pattern对象。
  2. 创建Matcher对象:通过Pattern.matcher(CharSequence input)方法,创建一个匹配器对象。
  3. 执行匹配操作:使用Matcher对象的各种方法来执行匹配操作,如matches()find()等。

示例代码

以下是一个示例代码,演示如何使用正则表达式在Java中验证电子邮件地址:

import java.util.regex.Pattern;
import java.util.regex.Matcher;

public class RegexExample {
    public static void main(String[] args) {
        // 定义电子邮件的正则表达式
        String emailRegex = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$";

        // 待验证的字符串
        String email1 = "example@example.com";
        String email2 = "invalid-email@com";

        // 编译正则表达式
        Pattern pattern = Pattern.compile(emailRegex);

        // 创建匹配器对象并验证字符串
        Matcher matcher1 = pattern.matcher(email1);
        Matcher matcher2 = pattern.matcher(email2);

        System.out.println(email1 + " is valid: " + matcher1.matches());
        System.out.println(email2 + " is valid: " + matcher2.matches());
    }
}

解释

  1. 定义正则表达式:字符串emailRegex定义了一个简单的电子邮件正则表达式。
  2. 编译正则表达式:调用Pattern.compile(emailRegex)将正则表达式编译为一个Pattern对象。
  3. 创建匹配器:调用pattern.matcher(email1)pattern.matcher(email2)创建Matcher对象,用于分别验证两个字符串。
  4. 执行匹配:调用matcher1.matches()matcher2.matches()方法检查字符串是否符合指定的正则表达式。

常用的方法

  • matches: 检查整个字符串是否完全匹配正则表达式。
  • find: 检查输入字符串中是否存在与正则表达式匹配的子字符串。
  • group: 获取匹配的子字符串。
  • replaceAll: 替换所有匹配的子字符串。

源码分析

Java中的正则表达式由java.util.regex包提供,该包的核心类是PatternMatcher。这些类在底层实现中使用了一些先进的算法和数据结构来高效地解析和匹配正则表达式。在深入分析其源码之前,先简单介绍一下这两个类的作用:

  • Pattern:表示一个编译后的正则表达式。
  • Matcher:利用这一正则表达式进行字符串匹配操作。

Pattern 类

Pattern类的主要职责是将正则表达式编译成一个内部的状态机表示,这样可以高效地进行字符匹配。下面我们来看一下Pattern类的一些关键方法:

compile 方法

Pattern.compile(String regex)方法用于将字符串形式的正则表达式编译为一个Pattern对象。这是正则表达式匹配过程的第一步。

public static Pattern compile(String regex) {
    return new Pattern(regex, 0);
}

这个方法创建了一个新的Pattern对象,并调用其构造函数进行初始化。

构造函数

Pattern类的构造函数负责解析输入的正则表达式并构建内部的状态机。以下是简化后的源码片段:

private Pattern(String regex, int flags) {
    this.regex = regex;
    this.flags = flags;

    // 调用PatternCompiler进行编译
    PatternCompiler compiler = new PatternCompiler();
    this.root = compiler.compile(regex, flags);

    // 初始化可能的其他成员变量
    this.matchRoot = root;
}

这里,PatternCompiler类负责将正则表达式解析为内部的节点树表示。

节点树模型

Pattern类中,正则表达式被解析为一组嵌套的节点,每个节点表示正则表达式的一部分。以下是一些常见节点类型:

abstract class Node {
    abstract boolean match(Matcher matcher, int i, CharSequence seq);
}

class CharNode extends Node {
    private final char c;

    CharNode(char c) {
        this.c = c;
    }

    @Override
    boolean match(Matcher matcher, int i, CharSequence seq) {
        if (i >= seq.length()) {
            return false;
        }
        return seq.charAt(i) == c && next.match(matcher, i + 1, seq);
    }
}

// 其他节点,比如StartNode, EndNode, GroupNode等

每个节点都有一个match方法,用于尝试匹配输入字符串的一部分,如果匹配成功,则继续匹配下一个节点。

Matcher 类

Matcher类是执行匹配操作的主要类。它使用Pattern类生成的节点树来对输入字符串进行匹配。

matches 方法

matches方法用于检查整个字符串是否与正则表达式完全匹配。

public boolean matches() {
    return match(0, ENDANCHOR);
}

match方法调用节点树的match方法,开始从头到尾的匹配过程。

match 方法

这是Matcher类中的核心匹配方法:

boolean match(int from, int anchor) {
    this.first = from;
    this.last = from;

    boolean result = parentPattern.matchRoot.match(this, from, text);

    if (result && anchor == ENDANCHOR && last != getTextLength()) {
        return false;
    }

    return result;
}

该方法从first位置开始,通过调用parentPattern.matchRoot.match方法来匹配节点树。如果匹配成功且锚点要求匹配到字符串末尾,则返回true

深入分析

编译过程

Pattern类的编译过程由PatternCompiler类完成。这个类负责将正则表达式解析为内部的节点树表示。编译过程涉及大量的递归调用和状态管理,以便正确处理各种正则表达式特性,如分组、反向引用、零宽断言等。这里是一个简化后的编译流程:

  1. 词法分析

    • 将正则表达式字符串解析成一系列标记(tokens)。
  2. 语法分析

    • 根据解析出的标记构建抽象语法树(AST),每个节点代表正则表达式的一部分。
  3. 生成节点树

    • 将AST转换成匹配操作的节点树,每个节点包含匹配逻辑和指向下一个节点的引用。

以下是一个简化的示例代码,展示了如何从正则表达式字符串生成节点树:

class PatternCompiler {
    public Node compile(String regex, int flags) {
        // 词法分析和语法分析省略
        // 这里直接构建节点树
        
        // 示例:匹配字母a后跟一个数字
        Node start = new CharNode('a');
        Node digit = new DigitNode();
        start.next = digit;
        
        return start; // 返回根节点
    }
}

匹配过程

匹配过程由Matcher类及其嵌套的Node类完成。每个Node对象表示正则表达式的一部分,它们通过调用自己的match方法来尝试匹配字符串的一部分。如果当前节点匹配成功,则递归调用下一个节点的match方法,直至整棵节点树都匹配成功或失败。

示例节点类

abstract class Node {
    Node next;

    abstract boolean match(Matcher matcher, int i, CharSequence seq);
}

class CharNode extends Node {
    private final char c;

    CharNode(char c) {
        this.c = c;
    }

    @Override
    boolean match(Matcher matcher, int i, CharSequence seq) {
        if (i >= seq.length()) {
            return false;
        }
        if (seq.charAt(i) == c) {
            return next.match(matcher, i + 1, seq);
        }
        return false;
    }
}

class DigitNode extends Node {
    @Override
    boolean match(Matcher matcher, int i, CharSequence seq) {
        if (i >= seq.length()) {
            return false;
        }
        if (Character.isDigit(seq.charAt(i))) {
            return next.match(matcher, i + 1, seq);
        }
        return false;
    }
}

Matcher 的 match 方法

boolean match(int from, int anchor) {
    this.first = from;
    this.last = from;

    boolean result = parentPattern.matchRoot.match(this, from, text);

    if (result && anchor == ENDANCHOR && last != getTextLength()) {
        return false;
    }

    return result;
}

该方法从first位置开始,通过调用parentPattern.matchRoot.match方法来匹配节点树。如果匹配成功且锚点要求匹配到字符串末尾,则返回true

总结

Java中的正则表达式实现是基于状态机和递归匹配的内部机制。总体流程如下:

  1. Pattern.compile() :将正则表达式字符串编译成一个Pattern对象。
  2. Pattern.matcher() :创建一个Matcher对象,准备对输入字符串进行匹配操作。
  3. Matcher.matches() :使用节点树从头到尾依次匹配输入字符串。
  4. 节点树:每个节点执行对应的匹配逻辑,并递归调用下一个节点,直到整个表达式匹配成功或失败。