Java 正则表达式入门:20% 的知识解决 80% 的场景
用 Java 讲清正则的基本用法,字符类、量词、锚点、捕获组,加上 matches、find、group、replaceAll、split 五个方法,覆盖日常八成需求。
正则表达式入门,Java 里学会这 20% 就够用
正则表达式劝退过不少程序员。一打开教程,满屏是 (?=...)、(?!...)、反向引用、原子组,术语比正文还多,还没学会走就想着跑,结果就是收藏了再没打开过。
日常写代码高频用到的,其实是很小的一块。这篇文章用 Java 讲,学完你能处理四类活,校验格式、提取内容、批量替换、按规则切分。这四类加起来,能覆盖日常遇到的八成需求。
需要的条件很简单,一个能跑 Java 的环境就行,Java 8 以上都可以。正则相关的 API 二十多年没怎么变过,写法和十年前基本一致。文里的每个例子我都跑过,输出直接写在注释里,你照着敲一遍就能对上。
第一关,先过 Java 的转义
正则里表示”一个数字”要写 \d。但把这个字符串写进 Java 源码,要写成 "\\d"。原因在 Java 字符串自己有一套转义规则,\n 是换行,\t 是制表符,\d 它不认识,直接写会编译报错。所以正则里凡是带反斜杠的写法,进了 Java 都得把反斜杠加倍。
这是新手遇到的第一个坑。编辑器还帮不上忙,你写 "\\d",高亮显示的是一前一后两个反斜杠,看着就觉得自己写错了。记住前面那条规则就行,Java 源码里正则的反斜杠全部加倍。
先跑一个完整的例子,校验手机号。
import java.util.regex.Pattern;
public class PhoneCheck {
public static void main(String[] args) {
// 编译正则,手机号规则是 1 开头,第二位 3 到 9,后面再跟 9 位数字
Pattern pattern = Pattern.compile("^1[3-9]\\d{9}$");
// matches 要求整串都符合规则,返回布尔值
System.out.println(pattern.matcher("13800138000").matches()); // true
System.out.println(pattern.matcher("12800138000").matches()); // false,第二位是 2
System.out.println(pattern.matcher("1380013800").matches()); // false,少一位
}
}
这条正则可以拆开看。^ 和 $ 是锚点,一个钉在开头,一个钉在结尾,意思是匹配必须覆盖整串。中间的 1 是普通字符,只匹配数字 1。[3-9] 是字符类,表示这一位取 3 到 9 的任意一个。\d 代表任意数字,{9} 是量词,表示前面的数字出现 9 次。
手机号规则这里我做了简化,只限制第一位是 1、第二位是 3 到 9,剩下的交给位数约束。真要按运营商号段逐条核对,规则会越写越长,还容易过时。绝大多数项目用到这个程度已经够了。
第二关,认识三个积木
正则的大部分语法可以归到三样东西,字符类、量词、锚点。先记住这三样,遇到不认识的再查,比从头啃语法表省时间。
字符类用中括号包起来。[0-9] 是数字,[a-zA-Z] 是英文字母,[^0-9] 是”不是数字”的任意字符,中括号开头的 ^ 表示取反。几个常用写法有现成缩写,\d 是数字,\w 是字母、数字、下划线,\s 是空白字符,包括空格、制表符和换行,. 是任意字符。
量词控制前面的东西出现几次。* 是零次或多次,+ 是一次或多次,? 是零次或一次,{n} 是恰好 n 次,{n,m} 是 n 到 m 次。锚点管位置,除了开头结尾的 ^ $,还有个 \b 表示单词边界,中文文本里用处不大,先不用管。
光靠这三样,已经能写不少正经东西。比如从日志里把所有数字捞出来。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ExtractNumbers {
public static void main(String[] args) {
// 一段日志文本,目标是把里面所有数字都提取出来
String text = "订单号 2026-0805,数量 3,金额 128.5 元";
Pattern pattern = Pattern.compile("\\d+");
// find 在文本里找子串,找到一次返回 true
// 配合 while 循环可以把所有匹配挨个取出来
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
// group 返回这一次匹配到的原文
System.out.println(matcher.group());
}
}
}
输出是 2026、0805、3、128、5 五行。注意 128.5 被拆成了 128 和 5,点号不属于 \d。想连小数点一起抓,正则改成 \d+(\.\d+)?,括号加问号的意思是”小数点加数字,这一整段可以不出现”。
第三关,四个操作
积木认识了,接下来看它们在 Java 里怎么用。Java 给正则准备了两类入口,一类在 java.util.regex 包里的 Pattern 和 Matcher,另一类是 String 类上直接就能调的方法。日常使用分四种情况。
校验用 matches。String.matches 和 Pattern.matcher(...).matches() 效果一样,区别在 String.matches 每次调用都会重新编译正则,循环里反复用要先把 Pattern 编译好,这个习惯值得养成。
提取用 find 加 group。想把文本里的日期拆成年月日,用括号把各部分圈起来,group 就能按括号取。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class CaptureDate {
public static void main(String[] args) {
// 把文本里所有日期拆成年、月、日三部分
String text = "今天 2024-03-15 发货,明天 2024-04-01 到货";
Pattern pattern = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
// group(0) 是整段匹配,group(1) 开始对应各个括号,按从左到右数
String year = matcher.group(1); // 年
String month = matcher.group(2); // 月
String day = matcher.group(3); // 日
System.out.println(year + "年" + month + "月" + day + "日");
}
}
}
输出两行,2024年03月15日 和 2024年04月01日。括号的编号从左往右数,第几个左括号就是第几组,这个编号规则后面替换也要用。
替换用 replaceAll 和 replaceFirst。替换串里能用 $1、$2 引用捕获组,把横线日期改成斜杠日期,一行代码。
public class ReformatDate {
public static void main(String[] args) {
String date = "2024-03-15";
// $1 对应第一个括号,也就是年份,$2 是月份,$3 是日期
String result = date.replaceAll("(\\d{4})-(\\d{2})-(\\d{2})", "$1/$2/$3");
System.out.println(result); // 输出 2024/03/15
}
}
切分用 split,分隔规则本身就是一个正则。连续空白想当成一个分隔符,就写 \\s+。
public class SplitText {
public static void main(String[] args) {
String line = "苹果 香蕉\t橙子\n葡萄";
// \\s+ 表示一个或多个空白字符,连续空白合并成一个分隔符
String[] fruits = line.split("\\s+");
for (String fruit : fruits) {
System.out.println(fruit);
}
}
}
输出四行,每个水果一行。有个细节容易忽略,split 默认会丢掉结尾的空串,"a,b,".split(",") 只得到两个元素。想保留全部,给 split 传一个负数参数,"a,b,".split(",", -1) 得到三个。
第四关,两个高频坑
第一个坑是 matches 和 find 的区别。matches 要求整串都符合规则,find 只要求找到一段子串。"abc".matches("b") 返回 false,"abc" 里找 b 用 find 却能找到。判断字符串合不合法用 matches,从文本里捞内容用 find,两个各自的正则也要配不同的写法,matches 通常要加 ^ 和 $ 把边界钉死。
第二个坑是贪婪。量词默认都贪婪,能多吃就多吃。看一段 HTML。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class GreedyVsLazy {
public static void main(String[] args) {
String html = "<a>text</a>";
// 默认贪婪,.+ 会一路吃到最后一个 >,把整段都吞进去
Matcher greedy = Pattern.compile("<.+>").matcher(html);
greedy.find();
System.out.println(greedy.group()); // <a>text</a>
// 量词后面加 ? 变懒惰,吃到第一个 > 就停
Matcher lazy = Pattern.compile("<.+?>").matcher(html);
lazy.find();
System.out.println(lazy.group()); // <a>
// 更稳的写法,用取反字符类,[^>] 表示"不是 > 的任意字符"
Matcher negated = Pattern.compile("<[^>]+>").matcher(html);
negated.find();
System.out.println(negated.group()); // <a>
}
}
三种写法,只有第一种会出错。想抓标签之间的文本,<[^>]+> 是实战里最常用的招,比写懒惰量词更不容易踩坑。
实战清单
把积木拼起来,几个最常见的需求基本都能直接套。
先说数据清洗。把多余空白合并成一个空格,再去掉首尾空白。
// 先把首尾空白去掉,再把中间连续的空白压成一个空格
String cleaned = " a b\t c ".trim().replaceAll("\\s+", " ");
System.out.println(cleaned); // 输出 a b c
去掉 HTML 标签,只留下可见文字。
// [^>] 表示"不是 > 的字符",把 <...> 整体替换成空串
String plain = "<p>你好<strong>世界</strong></p>".replaceAll("<[^>]+>", "");
System.out.println(plain); // 输出 你好世界
给手机号打码,常见于日志脱敏。
// 把每个数字都替换成 #,比逐个字符判断省事
String masked = "电话 13800138000".replaceAll("\\d", "#");
System.out.println(masked); // 输出 电话 ###########
校验邮箱。下面这版是入门够用的简化写法,真实系统里最靠谱的验证是发一封确认邮件,正则写得再复杂,也拦不住用户把邮箱输错。
// 简化版邮箱规则,用户名部分允许字母数字和 . + -,域名部分类似
boolean ok = "tom@example.com".matches("^[\\w.+-]+@[\\w-]+(\\.[\\w-]+)+$");
System.out.println(ok); // true
还有一条中文处理,Java 里 [\u4e00-\u9fa5] 能匹配一个汉字,判断整段都是中文写成 ^[\u4e00-\u9fa5]+$。
收尾
这篇文章用到的语法,数得过来。字符类、量词、锚点、括号捕获,加上 matches、find、group、replaceAll、split 这五个方法。这些就是正则的常用部分,剩下的还有命名组、反向引用、前瞻后顾这些进阶内容,等真遇到场景再学也不迟。
遇到新需求,先问自己一句,这活属于校验、提取、替换、切分里的哪一类。分类定下来,套路就有了。八成的情况,你不会卡在正则上。
评论
使用 GitHub 登录参与讨论。