正则表达式(Regular Expression)是一种用于描述字符串模式的强大工具,广泛应用于字符串匹配、查找、替换、验证等场景。无论是Java开发中的数据校验、日志解析,还是SQL中的模糊查询,亦或是日常的文本处理,掌握正则表达式都能大幅提升效率。本文将从底层逻辑出发,由浅入深、系统全面地讲解正则表达式,结合JDK 17环境下的可运行实例,帮你彻底吃透这门技术。
一、为什么要学正则表达式?底层价值与应用场景
在讲解具体语法之前,我们先搞清楚:正则表达式的核心价值是什么?为什么不直接用字符串的indexOf、contains等方法?
1. 底层价值:用“模式”匹配无限字符串
字符串的基础方法(如contains)只能匹配固定的子串,而正则表达式通过定义“字符模式”,可以匹配一类符合规则的字符串。例如:
- 匹配所有手机号(11位数字,以13/14/15/17/18/19开头);
- 匹配所有邮箱(包含@,@前为用户名,@后为域名);
- 匹配所有日期格式(如yyyy-MM-dd、yyyy/MM/dd)。
这种“模式化匹配”的能力,是正则表达式的核心,也是其能够应对复杂字符串处理场景的根本原因。
2. 核心应用场景
正则表达式的应用遍布软件开发的各个环节,典型场景包括:
- 数据验证:用户输入的手机号、邮箱、身份证号、密码强度校验;
- 文本处理:日志解析(提取日志中的时间、错误码、用户ID)、文本内容替换(批量替换指定格式的字符串);
- 数据提取:从HTML/XML文本中提取指定标签内容、从JSON字符串中提取特定字段;
- 数据库查询:MySQL中的
REGEXP运算符,实现复杂的模糊查询; - 配置解析:解析配置文件中的特定格式配置项(如.properties文件中的键值对)。
3. 正则表达式的执行流程(底层逻辑)
正则表达式的执行本质上是“模式匹配引擎”对输入字符串的扫描与匹配过程。不同语言的正则引擎实现略有差异,但核心流程一致:
- 编译正则表达式:将正则表达式字符串转换为高效的匹配引擎(有限自动机);
- 输入字符串扫描:匹配引擎按顺序扫描输入字符串的每个字符;
- 模式匹配校验:判断当前扫描位置的字符是否符合正则表达式定义的模式;
- 匹配结果返回:若匹配成功,返回匹配到的子串、位置等信息;若失败,返回无匹配。
流程图如下:
二、正则表达式基础语法:吃透核心元字符
正则表达式的语法核心是“元字符”——具有特殊含义的字符。掌握元字符的含义和用法,是学习正则表达式的基础。我们将元字符分为“基础匹配元字符”“量词元字符”“边界匹配元字符”“分组与引用元字符”四类,逐一讲解。
1. 基础匹配元字符(匹配单个字符)
基础元字符用于匹配单个字符,是构成正则表达式的最小单位。
| 元字符 | 含义 | 示例 | 匹配结果 |
| . | 匹配任意单个字符(除换行符\n) | a.b | aab、acb、a1b(不匹配a\nb) |
| [] | 匹配括号内的任意一个字符 | [abc] | a、b、c |
| [^] | 匹配不在括号内的任意一个字符 | [^abc] | d、1、@(不匹配a、b、c) |
| \d | 匹配数字字符(0-9) | \d{3} | 123、456、789 |
| \D | 匹配非数字字符 | \D{2} | ab、@#、A1(不匹配12、34) |
| \w | 匹配单词字符(a-z、A-Z、0-9、_) | \w+ | hello、Hello123、user_name |
| \W | 匹配非单词字符 | \W{2} | @#、$%、&* |
| \s | 匹配空白字符(空格、制表符\t、换行符\n、回车符\r) | \s+ | 空格、\t、\n\r |
| \S | 匹配非空白字符 | \S{3} | abc、123、@#$ |
关键说明:
- 元字符
.不匹配换行符\n,若需匹配包括换行符在内的任意字符,在Java中需使用Pattern.DOTALL标志; - 方括号
[]内的元字符会失去特殊含义,例如[.]匹配的是字符.,而非任意字符; - 方括号
[]内可以使用-表示范围,例如[a-z]匹配小写字母,[0-9a-zA-Z]匹配字母和数字,[1-35-7]匹配1-3或5-7的数字。
Java实例:基础元字符匹配
package com.jam.demo.regex;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 基础元字符匹配示例
* @author ken
*/
@Slf4j
public class BasicMetaCharDemo {
public static void main(String[] args) {
// 1. 测试元字符 . (匹配任意单个字符,除\n)
String regex1 = "a.b";
String str1 = "aab\nacb\na1b\na\nb";
matchAndLog(regex1, str1, "元字符 . 匹配");
// 2. 测试元字符 [] (匹配括号内任意字符)
String regex2 = "[abc]";
String str2 = "a1b2c3d4";
matchAndLog(regex2, str2, "元字符 [] 匹配");
// 3. 测试元字符 [^] (匹配不在括号内的字符)
String regex3 = "[^abc]";
String str3 = "a1b2c3d4";
matchAndLog(regex3, str3, "元字符 [^] 匹配");
// 4. 测试元字符 \d (匹配数字)
String regex4 = "\\d{3}"; // 后续讲解量词,这里表示匹配3个连续数字
String str4 = "abc123def456ghi78";
matchAndLog(regex4, str4, "元字符 \\d 匹配");
// 5. 测试元字符 \s (匹配空白字符)
String regex5 = "\\s+";
String str5 = "hello world\tjava\nregex\rtest";
matchAndLog(regex5, str5, "元字符 \\s 匹配");
}
/**
* 执行正则匹配并打印结果
* @param regex 正则表达式
* @param input 输入字符串
* @param testName 测试名称
*/
private static void matchAndLog(String regex, String input, String testName) {
if (StringUtils.isEmpty(regex) || StringUtils.isEmpty(input)) {
log.error("{}:正则表达式或输入字符串不能为空", testName);
return;
}
// 编译正则表达式(推荐复用Pattern,提升性能)
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(input);
log.info("===== {} =====", testName);
log.info("正则表达式:{}", regex);
log.info("输入字符串:{}", input);
log.info("匹配结果:");
while (matcher.find()) {
// 打印匹配到的子串及其起始、结束位置
log.info("匹配到:'{}',起始位置:{},结束位置:{}",
matcher.group(), matcher.start(), matcher.end());
}
log.info("===== {} 结束 =====\n", testName);
}
}
运行结果(关键部分):
===== 元字符 . 匹配 =====
正则表达式:a.b
输入字符串:aab
acb
a1b
a
b
匹配结果:
匹配到:'aab',起始位置:0,结束位置:3
匹配到:'acb',起始位置:4,结束位置:7
匹配到:'a1b',起始位置:8,结束位置:11
===== 元字符 . 匹配 结束 =====
===== 元字符 [] 匹配 =====
正则表达式:[abc]
输入字符串:a1b2c3d4
匹配结果:
匹配到:'a',起始位置:0,结束位置:1
匹配到:'b',起始位置:2,结束位置:3
匹配到:'c',起始位置:4,结束位置:5
===== 元字符 [] 匹配 结束 =====
2. 量词元字符(匹配多个连续字符)
基础元字符只能匹配单个字符,量词元字符用于指定“前面的元素(单个字符或分组)需要匹配的次数”,是实现“模式匹配”的核心。
| 元字符 | 含义 | 示例 | 匹配结果 |
| * | 匹配前面的元素0次或多次(贪婪匹配) | ab* | a、ab、abb、abbb |
| + | 匹配前面的元素1次或多次(贪婪匹配) | ab+ | ab、abb、abbb(不匹配a) |
| ? | 匹配前面的元素0次或1次(贪婪匹配) | ab? | a、ab(不匹配abb) |
| {n} | 匹配前面的元素恰好n次 | ab{3} | abbb(恰好3个b) |
| {n,} | 匹配前面的元素至少n次(贪婪匹配) | ab{2,} | abb、abbb、abbbb |
| {n,m} | 匹配前面的元素至少n次、至多m次(贪婪) | ab{2,4} | abb、abbb、abbbb(不超过4个b) |
| *? | 匹配前面的元素0次或多次(非贪婪匹配) | ab*? | a、ab(优先匹配最少次数) |
| +? | 匹配前面的元素1次或多次(非贪婪匹配) | ab+? | ab(优先匹配最少次数) |
| ?? | 匹配前面的元素0次或1次(非贪婪匹配) | ab?? | a(优先匹配0次) |
| {n,m}? | 匹配前面的元素n到m次(非贪婪) | ab{2,4}? | abb(优先匹配最少的2次) |
关键说明:
- 贪婪匹配:默认模式,尽可能匹配最多的字符(例如
ab*匹配abbb时,会匹配整个abbb,而非a或ab); - 非贪婪匹配:在量词后加
?,尽可能匹配最少的字符(例如ab*?匹配abbb时,会优先匹配a,而非abbb); - 量词作用于“前面紧邻的单个元素”,若需作用于多个元素,需使用分组(后续讲解)。
贪婪 vs 非贪婪:核心差异实例
package com.jam.demo.regex;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 贪婪匹配与非贪婪匹配对比示例
* @author ken
*/
@Slf4j
public class GreedyVsLazyDemo {
public static void main(String[] args) {
String input = "aabbaaccbb";
// 1. 贪婪匹配:ab+ 尽可能匹配最多的b
String greedyRegex = "ab+";
matchAndLog(greedyRegex, input, "贪婪匹配 ab+");
// 2. 非贪婪匹配:ab+? 尽可能匹配最少的b(1个)
String lazyRegex = "ab+?";
matchAndLog(lazyRegex, input, "非贪婪匹配 ab+?");
// 3. 贪婪匹配:a.*b 匹配从第一个a到最后一个b的所有字符
String greedyRegex2 = "a.*b";
matchAndLog(greedyRegex2, input, "贪婪匹配 a.*b");
// 4. 非贪婪匹配:a.*?b 匹配从第一个a到最近的b的字符
String lazyRegex2 = "a.*?b";
matchAndLog(lazyRegex2, input, "非贪婪匹配 a.*?b");
}
/**
* 执行正则匹配并打印结果(复用方法)
* @param regex 正则表达式
* @param input 输入字符串
* @param testName 测试名称
*/
private static void matchAndLog(String regex, String input, String testName) {
if (StringUtils.isEmpty(regex) || StringUtils.isEmpty(input)) {
log.error("{}:正则表达式或输入字符串不能为空", testName);
return;
}
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(input);
log.info("===== {} =====", testName);
log.info("正则表达式:{}", regex);
log.info("输入字符串:{}", input);
log.info("匹配结果:");
while (matcher.find()) {
log.info("匹配到:'{}',起始位置:{},结束位置:{}",
matcher.group(), matcher.start(), matcher.end());
}
log.info("===== {} 结束 =====\n", testName);
}
}
运行结果(核心差异):
===== 贪婪匹配 ab+ =====
正则表达式:ab+
输入字符串:aabbaaccbb
匹配结果:
匹配到:'abb',起始位置:1,结束位置:4 // 匹配到1个a后面的2个b(最多)
===== 贪婪匹配 ab+ 结束 =====
===== 非贪婪匹配 ab+? =====
正则表达式:ab+?
输入字符串:aabbaaccbb
匹配结果:
匹配到:'ab',起始位置:1,结束位置:3 // 匹配到1个a后面的1个b(最少)
===== 非贪婪匹配 ab+? 结束 =====
===== 贪婪匹配 a.*b =====
正则表达式:a.*b
输入字符串:aabbaaccbb
匹配结果:
匹配到:'aabbaaccbb',起始位置:0,结束位置:10 // 从第一个a到最后一个b
===== 贪婪匹配 a.*b 结束 =====
===== 非贪婪匹配 a.*?b =====
正则表达式:a.*?b
输入字符串:aabbaaccbb
匹配结果:
匹配到:'aab',起始位置:0,结束位置:3 // 从第一个a到最近的b
===== 非贪婪匹配 a.*?b 结束 =====
3. 边界匹配元字符(匹配字符串的边界位置)
边界匹配元字符不匹配具体字符,而是匹配“字符串的边界位置”(如字符串开头、结尾、单词边界),常用于精准匹配(避免部分匹配)。
| 元字符 | 含义 | 示例 | 匹配结果 |
| ^ | 匹配字符串的开头(多行模式下匹配行开头) | ^abc | abc(字符串以abc开头,不匹配xabc) |
| $ | 匹配字符串的结尾(多行模式下匹配行结尾) | abc$ | abc(字符串以abc结尾,不匹配abcx) |
| \b | 匹配单词边界(单词字符与非单词字符之间) | \bhello\b | hello(单独的hello单词,不匹配helloworld) |
| \B | 匹配非单词边界 | \Bhello\B | hello(在单词内部,如helloworld中的hello) |
关键说明:
^和$在默认模式下(单行模式)匹配整个字符串的开头和结尾;在多行模式(Pattern.MULTILINE)下,^匹配每一行的开头,$匹配每一行的结尾;\b的“单词边界”是指:一侧是单词字符(\w),另一侧是非单词字符(\W)或字符串边界(开头/结尾)。
边界匹配实例(精准验证场景)
package com.jam.demo.regex;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 边界匹配元字符示例(精准验证场景)
* @author ken
*/
@Slf4j
public class BoundaryMatchDemo {
public static void main(String[] args) {
// 1. 验证字符串是否为纯数字(精准匹配,整个字符串都是数字)
String numberRegex = "^\\d+$";
String[] numberTests = {"12345", "123a45", " 12345 ", "0"};
for (String test : numberTests) {
boolean isMatch = Pattern.matches(numberRegex, test);
log.info("字符串'{}'是否为纯数字:{}", test, isMatch);
}
// 2. 验证字符串是否以abc开头(单行模式 vs 多行模式)
String startWithAbc = "^abc";
String multiLineInput = "abc123\nabc456\nxabc789";
// 单行模式(默认):只匹配整个字符串的开头
Matcher singleLineMatcher = Pattern.compile(startWithAbc).matcher(multiLineInput);
// 多行模式:匹配每一行的开头
Matcher multiLineMatcher = Pattern.compile(startWithAbc, Pattern.MULTILINE).matcher(multiLineInput);
log.info("\n===== 单行模式下匹配 ^abc =====");
while (singleLineMatcher.find()) {
log.info("匹配到:'{}',位置:{}~{}", singleLineMatcher.group(), singleLineMatcher.start(), singleLineMatcher.end());
}
log.info("\n===== 多行模式下匹配 ^abc =====");
while (multiLineMatcher.find()) {
log.info("匹配到:'{}',位置:{}~{}", multiLineMatcher.group(), multiLineMatcher.start(), multiLineMatcher.end());
}
// 3. 匹配单独的hello单词(不匹配helloworld或hello123)
String wordRegex = "\\bhello\\b";
String wordInput = "hello helloworld hello123 hello@world @hello@";
Matcher wordMatcher = Pattern.compile(wordRegex).matcher(wordInput);
log.info("\n===== 匹配单独的hello单词 =====");
while (wordMatcher.find()) {
log.info("匹配到:'{}',位置:{}~{}", wordMatcher.group(), wordMatcher.start(), wordMatcher.end());
}
}
}
运行结果:
字符串'12345'是否为纯数字:true
字符串'123a45'是否为纯数字:false
字符串' 12345 '是否为纯数字:false
字符串'0'是否为纯数字:true
===== 单行模式下匹配 ^abc =====
匹配到:'abc',位置:0~3
===== 多行模式下匹配 ^abc =====
匹配到:'abc',位置:0~3
匹配到:'abc',位置:7~10
===== 匹配单独的hello单词 =====
匹配到:'hello',位置:0~5
匹配到:'hello',位置:24~29
4. 分组与引用元字符(匹配多个字符的组合)
当需要将多个字符作为一个整体(组合)进行匹配时,需要使用“分组”元字符。分组还支持“引用”(重复匹配已匹配的分组内容)和“命名分组”(更清晰地获取分组结果)。
| 元字符 | 含义 | 示例 | 匹配结果 |
| (pattern) | 捕获组:将pattern作为一个分组,可引用 | (ab)+ | ab、abab、ababab |
| \n | 引用第n个捕获组的内容(n为正整数) | (ab)c\1 | abcab(\1引用第一个分组的ab) |
| (?:pattern) | 非捕获组:只分组,不捕获(无法引用) | (?:ab)+ | ab、abab(无法用\1引用) |
| (?p) | 命名捕获组:给分组命名为name | (?ab)c\k | abcab(\k引用命名分组ab) |
| (?=pattern) | 正向预查:匹配后面紧跟pattern的位置 | abc(?=123) | abc(后面紧跟123,不匹配abc456) |
| (?!pattern) | 负向预查:匹配后面不紧跟pattern的位置 | abc(?!123) | abc(后面不紧跟123,匹配abc456) |
| (?<=pattern) | 正向后查:匹配前面紧跟pattern的位置 | (?<=123)abc | abc(前面紧跟123,不匹配456abc) |
| (?<!pattern) | 负向后查:匹配前面不紧跟pattern的位置 | (?<!123)abc | abc(前面不紧跟123,匹配456abc) |
关键说明:
- 捕获组:会将匹配到的分组内容保存到内存中,可通过
Matcher.group(n)获取(n从1开始,0表示整个匹配结果); - 非捕获组:仅用于将多个字符视为一个整体,不保存分组内容,性能优于捕获组(无需内存存储);
- 预查(零宽断言):只匹配“位置”,不匹配具体字符(匹配结果长度为0),用于限定匹配的上下文环境;
- 命名分组在Java 7及以上支持,通过
Matcher.group("name")获取分组内容,比数字引用更易读。
分组与引用实例
package com.jam.demo.regex;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 分组与引用元字符示例
* @author ken
*/
@Slf4j
public class GroupReferenceDemo {
public static void main(String[] args) {
// 1. 捕获组:匹配连续重复的ab(ab、abab、ababab)
String captureGroupRegex = "(ab)+";
String captureInput = "ab abab ababab abc";
Matcher captureMatcher = Pattern.compile(captureGroupRegex).matcher(captureInput);
log.info("===== 捕获组 (ab)+ 匹配 =====");
while (captureMatcher.find()) {
log.info("完整匹配:'{}',第1个分组(ab):'{}'",
captureMatcher.group(0), captureMatcher.group(1));
}
// 2. 引用分组:匹配ab cab(第一个分组ab,后面重复ab)
String referenceRegex = "(ab)c\\1";
String referenceInput = "abcab abcxab abcabab";
Matcher referenceMatcher = Pattern.compile(referenceRegex).matcher(referenceInput);
log.info("\n===== 引用分组 (ab)c\\1 匹配 =====");
while (referenceMatcher.find()) {
log.info("完整匹配:'{}',引用的分组内容:'{}'",
referenceMatcher.group(0), referenceMatcher.group(1));
}
// 3. 命名分组:匹配日期(yyyy-MM-dd),并提取年、月、日
String namedGroupRegex = "(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})";
String dateInput = "今天是2024-05-20,昨天是2024-05-19,明天是2024-05-21";
Matcher namedMatcher = Pattern.compile(namedGroupRegex).matcher(dateInput);
log.info("\n===== 命名分组匹配日期 =====");
while (namedMatcher.find()) {
log.info("完整日期:'{}',年:'{}',月:'{}',日:'{}'",
namedMatcher.group(0),
namedMatcher.group("year"),
namedMatcher.group("month"),
namedMatcher.group("day"));
}
// 4. 正向预查:匹配后面紧跟@163.com的用户名
String positiveLookaheadRegex = "\\w+(?=@163\\.com)";
String emailInput = "user1@163.com user2@gmail.com user3@163.com";
Matcher positiveMatcher = Pattern.compile(positiveLookaheadRegex).matcher(emailInput);
log.info("\n===== 正向预查匹配163邮箱用户名 =====");
while (positiveMatcher.find()) {
log.info("匹配到用户名:'{}'", positiveMatcher.group());
}
// 5. 负向后查:匹配前面不是http://的URL
String negativeLookbehindRegex = "(?<!http://)\\w+\\.com";
String urlInput = "http://www.baidu.com www.google.com https://www.github.com www.163.com";
Matcher negativeMatcher = Pattern.compile(negativeLookbehindRegex).matcher(urlInput);
log.info("\n===== 负向后查匹配非http开头的.com域名 =====");
while (negativeMatcher.find()) {
log.info("匹配到域名:'{}'", negativeMatcher.group());
}
}
}
运行结果:
===== 捕获组 (ab)+ 匹配 =====
完整匹配:'ab',第1个分组(ab):'ab'
完整匹配:'abab',第1个分组(ab):'ab'
完整匹配:'ababab',第1个分组(ab):'ab'
===== 引用分组 (ab)c\1 匹配 =====
完整匹配:'abcab',引用的分组内容:'ab'
完整匹配:'abcab',引用的分组内容:'ab'
===== 命名分组匹配日期 =====
完整日期:'2024-05-20',年:'2024',月:'05',日:'20'
完整日期:'2024-05-19',年:'2024',月:'05',日:'19'
完整日期:'2024-05-21',年:'2024',月:'05',日:'21'
===== 正向预查匹配163邮箱用户名 =====
匹配到用户名:'user1'
匹配到用户名:'user3'
===== 负向后查匹配非http开头的.com域名 =====
匹配到域名:'www.google.com'
匹配到域名:'www.163.com'
三、正则表达式进阶:Java中的正则引擎与核心API
掌握了正则表达式的语法后,我们需要结合Java的正则API进行实际开发。Java的正则引擎基于“有限自动机”实现,核心API位于java.util.regex包下,主要包括Pattern(正则表达式编译后的对象)和Matcher(匹配器对象)。
1. Java正则API核心类关系
2. 核心API详解
(1)Pattern类
Pattern是正则表达式的编译表示,线程安全,可复用(推荐复用,避免重复编译提升性能)。
核心方法:
static Pattern compile(String regex):编译正则表达式字符串,返回Pattern对象;static Pattern compile(String regex, int flags):带标志的编译(如Pattern.CASE_INSENSITIVE忽略大小写、Pattern.DOTALL让.匹配换行符);Matcher matcher(CharSequence input):创建匹配器对象,用于匹配输入字符串;static boolean matches(String regex, CharSequence input):静态方法,直接判断输入字符串是否匹配正则表达式(等价于compile(regex).matcher(input).matches());String[] split(CharSequence input):根据正则表达式分割输入字符串,返回字符串数组。
(2)Matcher类
Matcher是匹配器对象,非线程安全,用于执行具体的匹配操作。
核心方法:
boolean find():查找输入字符串中是否有匹配的子串(可多次调用,从上次匹配结束位置继续查找);boolean matches():判断整个输入字符串是否完全匹配正则表达式(等价于^regex$);boolean lookingAt():判断输入字符串的开头是否匹配正则表达式(等价于^regex);String group():返回当前匹配到的子串(等价于group(0));String group(int group):返回第n个捕获组的内容(n从1开始);String group(String name):返回命名捕获组的内容(Java 7+);int start():返回当前匹配子串的起始位置;int end():返回当前匹配子串的结束位置( exclusive);String replaceAll(String replacement):将所有匹配的子串替换为指定字符串;String replaceFirst(String replacement):将第一个匹配的子串替换为指定字符串;Matcher reset():重置匹配器,可重新从输入字符串开头查找。
(3)常用标志(flags)
| 标志常量 | 含义 | 简写 |
| Pattern.CASE_INSENSITIVE | 忽略大小写匹配(默认只匹配ASCII字符) | (?i) |
| Pattern.DOTALL | 让.匹配包括换行符在内的任意字符 |
(?s) |
| Pattern.MULTILINE | 多行模式,^匹配行开头,$匹配行结尾 |
(?m) |
| Pattern.UNICODE_CASE | 忽略大小写匹配(支持Unicode字符) | (?u) |
| Pattern.COMMENTS | 允许正则表达式中添加注释(#开头到行尾) | (?x) |
标志使用实例
package com.jam.demo.regex;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 正则标志使用示例
* @author ken
*/
@Slf4j
public class RegexFlagsDemo {
public static void main(String[] args) {
// 1. CASE_INSENSITIVE:忽略大小写匹配
String regex1 = "abc";
String input1 = "AbC aBc ABC xyz";
Pattern pattern1 = Pattern.compile(regex1, Pattern.CASE_INSENSITIVE);
Matcher matcher1 = pattern1.matcher(input1);
log.info("===== 忽略大小写匹配 abc =====");
while (matcher1.find()) {
log.info("匹配到:'{}'", matcher1.group());
}
// 2. DOTALL:. 匹配换行符
String regex2 = "a.b";
String input2 = "a\nb a1b aab";
// 无DOTALL标志(不匹配换行符)
Matcher matcher2 = Pattern.compile(regex2).matcher(input2);
// 有DOTALL标志(匹配换行符)
Matcher matcher2WithDotAll = Pattern.compile(regex2, Pattern.DOTALL).matcher(input2);
log.info("\n===== 无DOTALL标志匹配 a.b =====");
while (matcher2.find()) {
log.info("匹配到:'{}'", matcher2.group());
}
log.info("\n===== 有DOTALL标志匹配 a.b =====");
while (matcher2WithDotAll.find()) {
log.info("匹配到:'{}'", matcher2WithDotAll.group());
}
// 3. 简写标志:在正则表达式中直接使用 (?i)(?s) 等
String regex3 = "(?i)abc"; // 等价于 Pattern.CASE_INSENSITIVE
String input3 = "AbC ABC aBc";
Matcher matcher3 = Pattern.compile(regex3).matcher(input3);
log.info("\n===== 简写标志 (?i)abc 匹配 =====");
while (matcher3.find()) {
log.info("匹配到:'{}'", matcher3.group());
}
}
}
运行结果:
===== 忽略大小写匹配 abc =====
匹配到:'AbC'
匹配到:'aBc'
匹配到:'ABC'
===== 无DOTALL标志匹配 a.b =====
匹配到:'a1b'
匹配到:'aab'
===== 有DOTALL标志匹配 a.b =====
匹配到:'a
b'
匹配到:'a1b'
匹配到:'aab'
===== 简写标志 (?i)abc 匹配 =====
匹配到:'AbC'
匹配到:'ABC'
匹配到:'aBc'
3. Java正则性能优化技巧
- 复用Pattern对象:
Pattern.compile()是耗时操作,若正则表达式固定,应将Pattern对象定义为静态常量,避免重复编译; - 优先使用非捕获组:若无需引用分组内容,使用
(?:pattern)而非(pattern),减少内存占用; - 避免过度使用贪婪匹配:贪婪匹配可能导致回溯过多,性能下降,必要时使用非贪婪匹配或更精准的正则;
- 使用预查替代不必要的分组:例如验证密码强度时,用正向预查
(?=.*[A-Z])替代捕获组,性能更优; - 限制匹配范围:尽量缩小正则表达式的匹配范围,避免无限制的
.*(例如用[^@]*匹配邮箱用户名,而非.*)。
四、实战场景:正则表达式在Java开发中的高频应用
结合前面讲解的语法和API,我们针对Java开发中的高频场景,提供可直接复用的实战代码。
1. 场景1:用户输入验证(手机号、邮箱、身份证号、密码)
需求:
- 手机号:11位数字,以13/14/15/17/18/19开头;
- 邮箱:符合
用户名@域名格式,用户名可包含字母、数字、下划线、点,域名可包含多级(如xxx.xxx.com); - 身份证号(18位):前6位为地址码,中间8位为出生日期(yyyyMMdd),后4位为顺序码和校验码(最后一位可为X);
- 密码强度:8-20位,包含大小写字母、数字、特殊字符(至少三种)。
实战代码:
package com.jam.demo.regex.validator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.util.regex.Pattern;
/**
* 用户输入验证工具类(正则表达式实现)
* @author ken
*/
@Slf4j
public class RegexValidatorUtil {
/**
* 手机号正则(11位数字,以13/14/15/17/18/19开头)
*/
private static final Pattern MOBILE_PHONE_PATTERN = Pattern.compile("^1[345789]\\d{9}$");
/**
* 邮箱正则(简化版,覆盖大部分场景)
* 用户名:字母、数字、下划线、点、减号
* 域名:字母、数字、下划线、点、减号,至少包含一个点
*/
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*\\.[a-zA-Z]{2,}$");
/**
* 18位身份证号正则
*/
private static final Pattern ID_CARD_18_PATTERN = Pattern.compile("^[1-9]\\d{5}(19|20)\\d{2}((0[1-9])|(1[0-2]))((0[1-9])|([12]\\d)|(3[01]))\\d{3}[0-9Xx]$");
/**
* 密码强度正则(8-20位,包含大小写字母、数字、特殊字符至少三种)
* 特殊字符:!@#$%^&*()_+-=[]{}|;':",./<>?
*/
private static final Pattern PASSWORD_STRONG_PATTERN = Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)|(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()_+-=[]{}|;':\",./<>?])|(?=.*[a-z])(?=.*\\d)(?=.*[!@#$%^&*()_+-=[]{}|;':\",./<>?])|(?=.*[A-Z])(?=.*\\d)(?=.*[!@#$%^&*()_+-=[]{}|;':\",./<>?])).{8,20}$");
/**
* 验证手机号
* @param mobile 手机号字符串
* @return true:验证通过,false:验证失败
*/
public static boolean validateMobile(String mobile) {
if (StringUtils.isEmpty(mobile)) {
log.error("手机号验证失败:输入为空");
return false;
}
boolean isMatch = MOBILE_PHONE_PATTERN.matcher(mobile).matches();
if (!isMatch) {
log.error("手机号验证失败:{} 格式不正确", mobile);
}
return isMatch;
}
/**
* 验证邮箱
* @param email 邮箱字符串
* @return true:验证通过,false:验证失败
*/
public static boolean validateEmail(String email) {
if (StringUtils.isEmpty(email)) {
log.error("邮箱验证失败:输入为空");
return false;
}
boolean isMatch = EMAIL_PATTERN.matcher(email).matches();
if (!isMatch) {
log.error("邮箱验证失败:{} 格式不正确", email);
}
return isMatch;
}
/**
* 验证18位身份证号
* @param idCard 身份证号字符串
* @return true:验证通过,false:验证失败
*/
public static boolean validateIdCard18(String idCard) {
if (StringUtils.isEmpty(idCard)) {
log.error("身份证号验证失败:输入为空");
return false;
}
// 先验证格式
boolean isMatch = ID_CARD_18_PATTERN.matcher(idCard).matches();
if (!isMatch) {
log.error("身份证号验证失败:{} 格式不正确", idCard);
return false;
}
// (可选)验证校验码(身份证号最后一位)
boolean checkCodeValid = validateIdCardCheckCode(idCard);
if (!checkCodeValid) {
log.error("身份证号验证失败:{} 校验码错误", idCard);
return false;
}
return true;
}
/**
* 验证密码强度
* @param password 密码字符串
* @return true:验证通过,false:验证失败
*/
public static boolean validatePasswordStrong(String password) {
if (StringUtils.isEmpty(password)) {
log.error("密码验证失败:输入为空");
return false;
}
boolean isMatch = PASSWORD_STRONG_PATTERN.matcher(password).matches();
if (!isMatch) {
log.error("密码验证失败:{} 不符合要求(8-20位,包含大小写字母、数字、特殊字符至少三种)", password);
}
return isMatch;
}
/**
* 验证身份证号校验码(18位)
* 校验规则:前17位数字加权求和,权重为7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2
* 求和结果对11取余,余数对应校验码:0-1,1-0,2-X,3-9,4-8,5-7,6-6,7-5,8-4,9-3,10-2
* @param idCard 18位身份证号
* @return true:校验码正确,false:校验码错误
*/
private static boolean validateIdCardCheckCode(String idCard) {
// 权重数组
int[] weights = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2};
// 校验码对应表
char[] checkCodes = {'1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'};
// 计算前17位加权和
int sum = 0;
for (int i = 0; i < 17; i++) {
sum += (idCard.charAt(i) - '0') * weights[i];
}
// 计算校验码
char expectedCheckCode = checkCodes[sum % 11];
// 比较校验码(忽略大小写)
char actualCheckCode = Character.toUpperCase(idCard.charAt(17));
return actualCheckCode == expectedCheckCode;
}
// 测试方法
public static void main(String[] args) {
log.info("手机号验证:13812345678 → {}", validateMobile("13812345678"));
log.info("手机号验证:12345678901 → {}", validateMobile("12345678901"));
log.info("\n邮箱验证:user1@163.com → {}", validateEmail("user1@163.com"));
log.info("邮箱验证:user@.com → {}", validateEmail("user@.com"));
log.info("\n身份证号验证:110101199001011234 → {}", validateIdCard18("110101199001011234"));
log.info("身份证号验证:11010119900101123X → {}", validateIdCard18("11010119900101123X"));
log.info("身份证号验证:110101199001011235 → {}", validateIdCard18("110101199001011235")); // 校验码错误
log.info("\n密码验证:Abc123!@# → {}", validatePasswordStrong("Abc123!@#"));
log.info("密码验证:abc123456 → {}", validatePasswordStrong("abc123456")); // 缺少大写和特殊字符
}
}
运行结果:
手机号验证:13812345678 → true
手机号验证:12345678901 → false
邮箱验证:user1@163.com → true
邮箱验证:user@.com → false
身份证号验证:110101199001011234 → true
身份证号验证:11010119900101123X → true
身份证号验证:110101199001011235 → false
密码验证:Abc123!@# → true
密码验证:abc123456 → false
2. 场景2:日志解析(提取日志中的时间、错误码、用户ID)
需求:
- 日志格式:
[2024-05-20 14:30:25.123] [ERROR] [userId:1001] [errorCode:500] - 数据库查询失败; - 提取字段:时间、日志级别、用户ID、错误码、错误信息。
实战代码:
package com.jam.demo.regex.logparse;
import com.alibaba.fastjson2.JSON;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 日志解析工具类(正则表达式实现)
* @author ken
*/
@Slf4j
public class LogParseUtil {
/**
* 日志正则表达式(匹配格式:[时间] [级别] [userId:xxx] [errorCode:xxx] - 信息)
*/
private static final Pattern LOG_PATTERN = Pattern.compile(
"^\\[(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{3})\\] \\[(\\w+)\\] \\[userId:(\\d+)\\] \\[errorCode:(\\d+)\\] - (.*)$"
);
/**
* 解析日志字符串
* @param logStr 日志字符串
* @return LogInfo 日志信息对象(解析失败返回null)
*/
public static LogInfo parseLog(String logStr) {
if (StringUtils.isEmpty(logStr)) {
log.error("日志解析失败:输入日志为空");
return null;
}
Matcher matcher = LOG_PATTERN.matcher(logStr);
if (!matcher.matches()) {
log.error("日志解析失败:日志格式不匹配,日志内容:{}", logStr);
return null;
}
// 提取分组内容
LogInfo logInfo = new LogInfo();
logInfo.setTime(matcher.group(1));
logInfo.setLevel(matcher.group(2));
logInfo.setUserId(matcher.group(3));
logInfo.setErrorCode(matcher.group(4));
logInfo.setMessage(matcher.group(5));
return logInfo;
}
// 日志信息封装类
@Data
public static class LogInfo {
/** 日志时间 */
private String time;
/** 日志级别(INFO/ERROR/WARN/DEBUG) */
private String level;
/** 用户ID */
private String userId;
/** 错误码 */
private String errorCode;
/** 日志信息 */
private String message;
}
// 测试方法
public static void main(String[] args) {
String log1 = "[2024-05-20 14:30:25.123] [ERROR] [userId:1001] [errorCode:500] - 数据库查询失败";
String log2 = "[2024-05-20 15:40:10.456] [INFO] [userId:1002] [errorCode:0] - 用户登录成功";
String log3 = "2024-05-20 16:50:30.789 ERROR userId:1003 errorCode:404 页面不存在"; // 格式错误
LogInfo logInfo1 = parseLog(log1);
LogInfo logInfo2 = parseLog(log2);
LogInfo logInfo3 = parseLog(log3);
log.info("日志1解析结果:{}", JSON.toJSONString(logInfo1));
log.info("日志2解析结果:{}", JSON.toJSONString(logInfo2));
log.info("日志3解析结果:{}", logInfo3);
}
}
运行结果:
日志1解析结果:{"errorCode":"500","level":"ERROR","message":"数据库查询失败","time":"2024-05-20 14:30:25.123","userId":"1001"}
日志2解析结果:{"errorCode":"0","level":"INFO","message":"用户登录成功","time":"2024-05-20 15:40:10.456","userId":"1002"}
日志解析失败:日志格式不匹配,日志内容:2024-05-20 16:50:30.789 ERROR userId:1003 errorCode:404 页面不存在
日志3解析结果:null
3. 场景3:字符串替换与格式化(批量替换、脱敏处理)
需求:
- 批量替换:将字符串中的所有手机号中间4位替换为
****(脱敏); - 格式标准化:将字符串中的日期格式统一为
yyyy-MM-dd(如将2024/05/20、2024.05.20转换为2024-05-20); - 去除空格:去除字符串中的所有空白字符(空格、制表符、换行符)。
实战代码:
package com.jam.demo.regex.replace;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 字符串替换与格式化工具类(正则表达式实现)
* @author ken
*/
@Slf4j
public class StringReplaceUtil {
/**
* 手机号脱敏正则(匹配11位手机号,捕获前3位和后4位)
*/
private static final Pattern MOBILE_MASK_PATTERN = Pattern.compile("(1[345789])(\\d{4})(\\d{4})");
/**
* 日期格式标准化正则(匹配 yyyy/MM/dd 或 yyyy.MM.dd 格式)
*/
private static final Pattern DATE_NORMALIZE_PATTERN = Pattern.compile("(\\d{4})[/.](\\d{2})[/.](\\d{2})");
/**
* 空白字符匹配正则(匹配所有空白字符:空格、制表符、换行符、回车符)
*/
private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+");
/**
* 手机号脱敏:中间4位替换为****
* @param input 包含手机号的字符串
* @return 脱敏后的字符串
*/
public static String maskMobile(String input) {
if (StringUtils.isEmpty(input)) {
log.warn("手机号脱敏失败:输入字符串为空");
return input;
}
// 使用分组引用替换,$1表示前3位,$3表示后4位
return MOBILE_MASK_PATTERN.matcher(input).replaceAll("$1****$3");
}
/**
* 日期格式标准化:将 yyyy/MM/dd 或 yyyy.MM.dd 转换为 yyyy-MM-dd
* @param input 包含日期的字符串
* @return 日期格式标准化后的字符串
*/
public static String normalizeDate(String input) {
if (StringUtils.isEmpty(input)) {
log.warn("日期标准化失败:输入字符串为空");
return input;
}
// 分组引用替换,$1=年,$2=月,$3=日
return DATE_NORMALIZE_PATTERN.matcher(input).replaceAll("$1-$2-$3");
}
/**
* 去除所有空白字符(空格、制表符、换行符、回车符)
* @param input 输入字符串
* @return 去除空白后的字符串
*/
public static String removeAllWhitespace(String input) {
if (StringUtils.isEmpty(input)) {
log.warn("去除空白失败:输入字符串为空");
return input;
}
return WHITESPACE_PATTERN.matcher(input).replaceAll("");
}
/**
* 测试方法
* @param args 命令行参数
*/
public static void main(String[] args) {
// 测试手机号脱敏
String mobileStr = "联系电话:13812345678 或 13987654321,备用电话:15011112222";
String maskedMobile = maskMobile(mobileStr);
log.info("手机号脱敏前:{}", mobileStr);
log.info("手机号脱敏后:{}", maskedMobile);
// 测试日期格式标准化
String dateStr = "今天是2024/05/20,昨天是2024.05.19,明天是2024-05-21";
String normalizedDate = normalizeDate(dateStr);
log.info("\n日期标准化前:{}", dateStr);
log.info("日期标准化后:{}", normalizedDate);
// 测试去除所有空白字符
String whitespaceStr = " hello \t world \n java \r regex ";
String noWhitespace = removeAllWhitespace(whitespaceStr);
log.info("\n去除空白前:{}", whitespaceStr);
log.info("去除空白后:{}", noWhitespace);
}
}
运行结果:
手机号脱敏前:联系电话:13812345678 或 13987654321,备用电话:15011112222
手机号脱敏后:联系电话:138****5678 或 139****4321,备用电话:150****2222
日期标准化前:今天是2024/05/20,昨天是2024.05.19,明天是2024-05-21
日期标准化后:今天是2024-05-20,昨天是2024-05-19,明天是2024-05-21
去除空白前: hello world
java
regex
去除空白后:helloworldjavaregex
4. 场景4:MySQL中的正则表达式应用(REGEXP运算符)
在MySQL中,REGEXP(或RLIKE)运算符用于执行正则表达式匹配,适用于复杂的模糊查询场景,功能比LIKE更强大。
语法:
SELECT column1, column2 FROM table_name WHERE column REGEXP 'regex_pattern';
关键说明:
- MySQL的正则表达式默认不区分大小写,若需区分大小写,使用
REGEXP BINARY; ^匹配字符串开头,$匹配字符串结尾,.匹配任意单个字符;*匹配0次或多次,+匹配1次或多次,?匹配0次或1次;[]匹配括号内的任意字符,[^]匹配括号外的任意字符。
实战案例(MySQL 8.0环境验证)
准备测试表和数据
-- 创建用户表
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` VARCHAR(50) NOT NULL COMMENT '用户名',
`mobile` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
`email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户测试表';
-- 插入测试数据
INSERT INTO `t_user` (`username`, `mobile`, `email`) VALUES
('zhangsan', '13812345678', 'zhangsan@163.com'),
('lisi', '13987654321', 'lisi@gmail.com'),
('wangwu', '15011112222', 'wangwu@qq.com'),
('zhaoliu', '18099998888', 'zhaoliu@company.com'),
('tianqi', '02012345678', 'tianqi@126.com');
案例1:查询手机号以138或139开头的用户
SELECT id, username, mobile FROM t_user WHERE mobile REGEXP '^13[89]';
查询结果:
| id | username | mobile |
| 1 | zhangsan | 13812345678 |
| 2 | lisi | 13987654321 |
案例2:查询邮箱为163或126域名的用户
SELECT id, username, email FROM t_user WHERE email REGEXP '@(163|126)\\.com$';
查询结果:
| id | username | |
| 1 | zhangsan | zhangsan@163.com |
| 5 | tianqi | tianqi@126.com |
案例3:查询用户名包含字母且长度为6的用户(区分大小写)
SELECT id, username FROM t_user WHERE username REGEXP BINARY '^[a-zA-Z]{6}$';
查询结果:
| id | username |
| 1 | zhangsan |
| 2 | lisi |
| 3 | wangwu |
| 4 | zhaoliu |
案例4:查询手机号不符合11位规范的用户
SELECT id, username, mobile FROM t_user WHERE mobile NOT REGEXP '^1[345789]\\d{9}$';
查询结果:
| id | username | mobile |
| 5 | tianqi | 02012345678 |
五、正则表达式避坑指南:易混淆点与常见错误
1. 易混淆点明确区分
| 易混淆语法 | 正确含义 | 错误理解 | 典型错误示例 |
^ 和 $ |
字符串开头/结尾(多行模式为行开头/结尾) | 匹配字符^或$ |
用^abc$匹配包含abc的字符串(实际是精准匹配abc) |
. |
匹配任意单个字符(除换行符) | 匹配字符. |
用a.b匹配a.b(正确写法应为a\\.b) |
\d 和 [0-9] |
在Java中等价,匹配数字字符 | \d匹配任意十进制数 |
无差异,但需注意转义:Java中写\\d,正则原生写\d |
| 贪婪匹配 vs 非贪婪匹配 | 贪婪:尽可能多匹配;非贪婪:尽可能少匹配 | 非贪婪匹配速度一定更快 | a.*b匹配aabbaacbb时贪婪匹配整个字符串,非贪婪匹配aab |
| 捕获组 vs 非捕获组 | 捕获组(pattern)可引用,非捕获组(?:pattern)不可引用 |
非捕获组无法分组 | 无需引用分组时用非捕获组,提升性能 |
2. 常见错误及解决方案
错误1:Java中忘记转义反斜杠
错误代码:
// 错误:Java中\需要转义为\\,否则编译报错
Pattern pattern = Pattern.compile("\d{3}");
解决方案:Java字符串中反斜杠需要转义,正则中的\d在Java中需写为\\d。
// 正确写法
Pattern pattern = Pattern.compile("\\d{3}");
错误2:用matches()方法进行部分匹配
错误代码:
// 需求:判断字符串是否包含数字,错误使用matches()
String input = "abc123def";
boolean hasNumber = Pattern.matches("\\d+", input); // 返回false
解决方案:matches()方法判断整个字符串是否匹配,部分匹配应使用find()方法。
boolean hasNumber = Pattern.compile("\\d+").matcher(input).find(); // 返回true
错误3:正则表达式过于复杂导致回溯爆炸
错误场景:用(a+)+b匹配超长字符串aaaaaaaaaaaaaaaaaaaaaaaaaaaaa(无b结尾),导致程序卡顿。解决方案:优化正则表达式,避免嵌套量词,缩小匹配范围。
// 优化后:避免嵌套量词
Pattern pattern = Pattern.compile("a+b");
错误4:忽略Pattern的线程安全性
错误代码:
// 每次匹配都编译Pattern,性能低下且浪费资源
public boolean isMobile(String input) {
return Pattern.compile("^1[345789]\\d{9}$").matcher(input).matches();
}
解决方案:将Pattern定义为静态常量,复用编译后的对象。
private static final Pattern MOBILE_PATTERN = Pattern.compile("^1[345789]\\d{9}$");
public boolean isMobile(String input) {
return MOBILE_PATTERN.matcher(input).matches();
}
六、正则表达式性能优化与最佳实践
1. 性能优化技巧
(1)复用Pattern对象
Pattern.compile()是耗时操作,正则表达式固定时,应将Pattern定义为静态常量,避免重复编译。
(2)优先使用非捕获组
当不需要引用分组内容时,使用非捕获组(?:pattern)替代捕获组(pattern),减少内存占用和匹配时间。
(3)避免过度使用贪婪匹配
贪婪匹配可能导致大量回溯,必要时使用非贪婪匹配或更精准的正则表达式。例如:
- 不好的写法:
a.*b(匹配a到最后一个b) - 更好的写法:
a[^b]*b(匹配a到第一个b,无回溯)
(4)限制匹配范围
尽量使用精准的字符集替代宽泛的匹配。例如:
- 匹配邮箱用户名:用
[a-zA-Z0-9_-]+替代.* - 匹配日期:用
\\d{4}-\\d{2}-\\d{2}替代.*
(5)使用预查替代不必要的分组
预查(零宽断言)只匹配位置,不消耗字符,性能优于分组。例如验证密码强度:
// 正向预查:密码包含大写字母、小写字母、数字
String regex = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,20}$";
2. 最佳实践
(1)明确正则表达式的适用场景
正则表达式适合处理结构化字符串(如手机号、邮箱、日期),不适合处理非结构化字符串(如HTML/XML解析,推荐使用专门的解析库)。
(2)编写可维护的正则表达式
- 复杂正则表达式添加注释(使用
Pattern.COMMENTS标志,注释以#开头); - 拆分复杂正则为多个简单正则,分步匹配。
(3)测试正则表达式的边界情况
针对以下边界情况进行测试:
- 空字符串;
- 最大长度字符串;
- 临界值(如手机号10位、12位,身份证号17位、19位)。
(4)使用工具辅助编写正则表达式
推荐工具:
- Regex101:在线正则表达式测试工具,支持Java、Python等多种语言;
- RegexBuddy:桌面端正则表达式编辑和测试工具,适合复杂正则编写。
七、总结
正则表达式是处理字符串的“瑞士军刀”,掌握其语法和底层逻辑,能大幅提升字符串处理的效率和准确性。本文从基础语法、Java核心API、实战场景、避坑指南、性能优化五个维度,全面讲解了正则表达式的知识体系,结合JDK 17和MySQL 8.0环境下的可运行实例,帮助读者夯实基础、解决实际问题。
正则表达式的学习没有捷径,关键在于多练、多测、多优化。希望本文能成为你开发路上的实用指南,让你在处理字符串场景时游刃有余。
是否需要我帮你整理正则表达式高频场景的工具类清单,方便你直接集成到项目中?