Java 正则表达式基础

第一部分:Java 正则表达式核心概念与底层原理

在进入实战场景前,我们必须先厘清正则表达式的底层基本逻辑,以及 Java 平台的实现机制 ------ 这是后续编写高性能匹配模式、定位和解决性能问题的基础前提。

1.1 正则表达式的本质

正则表达式(Regular Expression,常简写为 regex)是一种用于匹配字符串模式的领域专用语言 ------ 它用一种结构化的语法规则,来描述一类符合特定规则的字符串样本,而不是用代码逻辑逐个字符进行判断。在业务开发中,它是文本处理场景的终极解决方案,核心应用场景可以归纳为四类:

  1. 验证:检查输入字符串的格式是否符合业务规则 ------ 比如校验用户输入的手机号、邮箱地址、身份证号的格式是否合法;
  2. 提取:从指定文本中批量获取符合规则的子串 ------ 比如从用户提交的文本中提取所有符合规则的订单号,或者从 HTML 或日志文件中提取特定业务节点的关键信息;
  3. 替换:将匹配到的子串统一替换为指定的新内容 ------ 比如隐藏用户敏感信息、过滤文本中的敏感词、或者将文本中的 HTML 标签统一清除;
  4. 分割:根据复杂的规则分隔符,将字符串切分为多个子串 ------ 比如将前端传入的多分隔符数据,统一切分为标准的字符串数组。

与其他文本处理方式相比,正则表达式的核心优势在于「用简短的语法逻辑,表达一套完整的、灵活的字符串匹配规则」------ 对于业务开发中绝大多数文本处理需求,都可以通过正则表达式的标准匹配逻辑来解决,而无需编写冗余的业务级代码。

第二部分:Java 正则 API 使用指南 ------ 从基础到高级

理解了底层原理,我们再来看 Java 正则 API 的具体使用方式。Java 中的正则表达式使用方式,可以分为「便捷方式」和「标准方式」两类 ------ 不同的方式,适用于不同的业务场景。

2.1 两类使用方式

2.1.1 便捷方式:利用 String 类的内置方法

对于简单、低频的正则匹配场景,JDK 提供了一种极简的使用方式 ------ 直接调用String类中内置的三个正则相关方法,就可以完成匹配操作,而无需手动创建Pattern和Matcher实例。这三个方法的核心功能和适用场景如下:

  • String.matches(String regex) :校验当前完整字符串,是否匹配传入的正则表达式规则;
  • String.replaceAll(String regex, String replacement) :将当前字符串中,所有匹配正则表达式规则的子串,替换为指定的新内容;
  • String.replaceFirst(String regex, String replacement) :只替换当前字符串中,第一个匹配正则表达式规则的子串;
  • String.split(String regex) :根据传入的正则表达式规则作为分隔符,将当前字符串分割为一个字符串数组。

这类方法的核心优势是编码简单,只需一行代码即可完成匹配操作,适合逻辑简单、执行频率较低的业务场景。但很多开发者不知道的是,这类便捷方法底层实际上仍会去编译Pattern对象------ 每次调用String类的正则方法时,都会隐式调用Pattern.compile()编译一次正则表达式,生成一个新的Pattern实例。如果在循环中、或者高频业务场景中频繁调用这类方法,就会导致大量重复的编译操作,急剧增加 CPU 资源消耗,引发严重的性能问题。

2.1.2 标准方式:Pattern 与 Matcher 组合

对于高频、复杂的正则匹配场景,必须使用标准的 API 调用方式 ------ 手动处理正则表达式的编译和匹配逻辑。这一方式的核心流程,可以拆解为三个明确的步骤:

  1. 编译正则表达式:使用Pattern.compile()方法,将字符串形式的正则表达式,编译为一个Pattern实例 ------ 这一过程只需要执行一次;
  2. 创建匹配器:调用Pattern.matcher()方法,传入需要匹配的目标字符串,获取一个Matcher实例;
  3. 执行匹配逻辑:调用Matcher实例的相关方法,执行具体的匹配操作 ------ 匹配成功后,还可以通过Matcher的group()方法,获取匹配到的具体结果。

这一标准方式的核心优势,是可以复用Pattern实例 ------ 这是提升正则表达式性能的最核心手段,下一章我们会深入讲解这一优化逻辑。

2.2 Matcher 类的核心匹配方法

Matcher类是执行匹配操作的核心入口,它提供了三个功能差异极大的匹配方法,分别对应不同的业务场景。很多开发者会混淆这三个方法的使用逻辑,导致匹配结果不符合业务预期 ------ 在使用时,必须根据实际业务场景的需求,选择正确的方法执行匹配逻辑:

  • matches() :尝试对整个目标字符串进行完整匹配 ------ 只有当整个字符串完全匹配正则表达式的规则时,才会返回true。这一方法适用于「校验整个字符串的格式是否符合业务规则」的场景,比如校验用户输入的手机号、邮箱格式是否合法;
  • lookingAt() :尝试从目标字符串的开头进行匹配 ------ 只有当字符串的前缀部分匹配正则表达式规则时,才会返回true。这一方法适用于「校验字符串的前缀是否符合特定规则」的场景,比如校验用户上传的文件名是否以指定的业务前缀开头;
  • find() :扫描整个目标字符串,查找所有匹配规则的子串 ------ 这一方法适用于「提取字符串中所有匹配规则的子串」的场景,比如从一段文本中提取所有的订单号、手机号或邮箱地址。

需要特别注意的是,这三个方法都会重置Matcher实例的内部匹配指针状态------ 在同一组匹配规则下,多个方法之间会相互影响,导致匹配结果不符合预期。在实际业务场景中,建议在创建Matcher实例后,只使用其中一种方法执行匹配逻辑,避免混合调用引发意想不到的匹配异常。

2.3 实战基础示例:校验、提取、替换、分割

接下来,我们通过四个最常见的业务场景示例,来演示标准 API 的具体使用方式。

场景一:数据校验(完全匹配)

这是后端开发中最常见的业务场景 ------ 校验用户输入的手机号格式是否合法。由于校验逻辑会被高频调用,我们需要使用标准的 API 调用方式,预先编译好Pattern对象并复用它:

复制代码
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexValidationExample {    
// 将正则表达式预编译为静态常量,类加载时只编译一次,后续可以全局复用    
private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$"); 
   /**     * 校验手机号格式是否合法  
   * @param phone 待校验的手机号字符串     
   * @return 校验结果:true表示格式合法,false表示格式不合法     */   
 public static boolean isValidPhoneNumber(String phone) {        
				// 对空值或长度不符合预期的字符串进行快速校验        
				if (phone == null || phone.length() != 11) {
            return false;        
            }
        // 创建匹配器,执行匹配操作       
				 Matcher matcher = PHONE_PATTERN.matcher(phone);        
				// 这里使用matches()方法,因为需要对整个手机号进行完整匹配       
			 return matcher.matches();    
 }}

在这个示例中,我们将Pattern对象定义为静态常量 ------ 这意味着在类加载时,正则表达式只会被编译一次,后续的每次校验逻辑,都会直接复用这一编译后的实例,避免了重复编译带来的性能损耗。

场景二:数据提取(查找子串)

从一段普通文本中,提取出所有符合规则的手机号或邮箱地址 ------ 这是日志分析、用户输入数据清洗场景下的常见需求。实现这一需求的核心,是正确使用Matcher.find()方法:

复制代码
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexExtractionExample {

    // 预编译手机号匹配规则
    private static final Pattern PHONE_PATTERN = Pattern.compile("\\b1[3-9]\\d{9}\\b");

    // 预编译邮箱匹配规则
    private static final Pattern EMAIL_PATTERN = Pattern.compile(
            "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b"
    );

    /**
     * 从输入文本中提取所有匹配规则的手机号
     *
     * @param text 待处理的目标文本
     * @return 匹配到的手机号列表
     */
    public static List<String> extractPhoneNumbers(String text) {
        List<String> phones = new ArrayList<>();
        Matcher matcher = PHONE_PATTERN.matcher(text);

        // 循环调用find方法,扫描整个文本,查找所有匹配的子串
        while (matcher.find()) {
            // 使用group()方法获取当前匹配到的子串
            phones.add(matcher.group());
        }
        return phones;
    }

    /**
     * 从输入文本中提取所有匹配规则的邮箱地址
     *
     * @param text 待处理的目标文本
     * @return 匹配到的邮箱地址列表
     */
    public static List<String> extractEmails(String text) {
        List<String> emails = new ArrayList<>();
        Matcher matcher = EMAIL_PATTERN.matcher(text);

        while (matcher.find()) {
            emails.add(matcher.group());
        }
        return emails;
    }
}

在这个示例中,我们使用\b元字符来匹配单词的边界,确保不会提取到不符合规则的中间数字串 ------ 这是数据提取场景中常用的匹配规则优化手段,避免匹配到业务不需要的子串,导致提取结果出现逻辑错误。

场景三:敏感词替换

在用户输入的内容中,将所有匹配规则的敏感词,统一替换为固定的掩码字符 ------ 这是内容审核、用户数据脱敏场景下的常见需求。实现这一需求的核心,是正确使用Matcher.replaceAll()方法:

复制代码
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexReplacementExample {

    /**
     * 替换敏感词:将输入文本中的所有敏感词,替换为固定长度的掩码字符
     *
     * @param text           待处理的目标文本
     * @param sensitiveWords 要替换的敏感词数组
     * @return 替换后的脱敏文本
     */
    public static String filterSensitiveWords(String text, String[] sensitiveWords) {
        // 构造敏感词的正则匹配规则:将所有敏感词用 | 连接,匹配其中任意一个
        String regex = String.join("|", sensitiveWords);

        // 编译正则表达式:使用 CASE_INSENSITIVE 标志,忽略大小写进行匹配
        Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
        Matcher matcher = pattern.matcher(text);

        // 使用 replaceAll 方法,将所有匹配到的敏感词替换为 "***"
        return matcher.replaceAll("***");
    }

    public static void main(String[] args) {
        String content = "这个商品的质量很差,垃圾至极,我实在是太失望了!";
        String[] sensitiveWords = {"垃圾", "差", "失望"};

        String filteredContent = filterSensitiveWords(content, sensitiveWords);
        System.out.println("脱敏后的文本:" + filteredContent);
        
        // 输出:脱敏后的文本:这个商品的质量很***,***至极,我实在是太***了!
    }
}

需要特别注意的是,在这个场景中,我们使用了Pattern.CASE_INSENSITIVE匹配标志 ------ 这会让匹配规则忽略大小写,从而匹配到不同大小写的敏感词。此外,敏感词数组的元素,需要按较长的词优先排序 ------ 否则,部分短的敏感词会先被匹配替换,导致长敏感词无法被正确匹配替换。

场景四:复杂字符串分割

前端传入的字符串,使用了多种分隔符 ------ 比如逗号、分号、竖线,需要将其统一分割为标准的字符串数组。这是数据解析、多标签处理场景下的常见需求。实现这一需求的核心,是在split()方法中传入匹配所有分隔符的正则表达式规则:

复制代码
import java.util.Arrays;
import java.util.regex.Pattern;

public class RegexSplittingExample {

    // 预编译匹配多种分隔符的正则表达式规则:匹配逗号、分号、竖线,以及其周围的空白字符
    private static final Pattern DELIMITER_PATTERN = Pattern.compile("\\s*[,;|]\\s*");

    public static void main(String[] args) {
        String data = "  apple,banana;orange|grape  ";

        // 使用正则表达式规则分割字符串
        String[] fruits = DELIMITER_PATTERN.split(data);

        // 对结果进行过滤,去除可能存在的空白字符串
        fruits = Arrays.stream(fruits)
                .map(String::trim)
                .filter(s -> !s.isEmpty())
                .toArray(String[]::new);

        System.out.println(Arrays.toString(fruits));
        // 输出:[apple, banana, orange, grape]
    }
}

在这个示例中,我们使用\s*来匹配分隔符周围的所有空白字符 ------ 这可以避免结果中出现多余的空格;同时,在分割完成后,我们对结果数组进行了过滤,去除可能存在的空白字符串 ------ 这可以有效避免输入数据不规范导致的分割结果异常。