Java 正则表达式与常用工具类
本篇是 2017 年 Java 学习笔记系列的第十四天,是一篇工具类大合集:先用正则表达式解决”字符串匹配”问题,再快速过一遍 Math、Random、System 三小件,接着用 BigDecimal 讲清浮点数的精度问题,最后把 Date 与 Calendar 这对遗留 API 和现代的 java.time 替代方案一次说清。
本篇目标
- 掌握正则的字符类、量词、分组三块核心语法
- 会在 Java 中用 matches、split、replaceAll 与 Pattern/Matcher 四种方式使用正则
- 了解 Math、Random、System 的常用静态方法
- 明白为什么算钱必须用 BigDecimal 而不是 double
- 知道 Date/Calendar 是遗留 API,会用 LocalDateTime 等 java.time 类替代
正则表达式:用规则描述字符串
正则表达式是一门微型的”匹配语言”:用一个字符串描述”什么样的字符串才算合法”。比如校验 QQ 号——要求 5 到 15 位、不能以 0 开头、必须全是数字——不用正则要写一串循环和判断,用正则只需一行。
字符类:定义”每一位可以是什么”
[abc]:a、b、c 中的任意一个[^abc]:除 a、b、c 之外的任意字符[a-zA-Z]:范围写法,匹配任意字母- 预定义类:
\d数字(等价[0-9])、\w单词字符(字母、数字、下划线)、\s空白、.任意字符
注意:在 Java 字符串里反斜杠本身要转义,正则的 \d 写成 Java 代码是 "\\d"。
量词:定义”可以出现几次”
X? 零或一次;X* 零或多次;X+ 一或多次;X{n} 恰好 n 次;X{n,} 至少 n 次;X{n,m} n 到 m 次。组合起来,QQ 号的正则就是 [1-9]\d{4,14}:首位 1-9,后面再跟 4 到 14 个数字。
分组:把一部分当作整体并编号
圆括号即分组,从左到右数左括号依次编为第 1、2、3 组,第 0 组永远代表整个表达式。例如 ((A)(B(C))) 中共有四组:整体是一组,(A)、(B(C))、(C) 依次是二、三、四组。分组配合反向引用可以匹配叠词——(.)\1+ 表示”任意字符连续重复出现”,\1 引用第 1 组捕获到的内容,能找出 "aaabbbccc" 中的每个叠字:
String s = "aaabbbccc".replaceAll("(.)\\1+", "$1"); |
Java 中使用正则的四种姿势
// 1. 整体校验:String.matches,要求整个字符串都符合规则 |
关键区分:matches 要求整体匹配,多一个字符都不行;而想从一大段文本里提取所有符合规则的片段,必须用 Pattern.compile + matcher.find() + group() 这套组合。find 每调用一次就定位到下一个匹配处,group 取出本次匹配的内容。工程上还有一个细节:同一条正则要反复使用时,把 Pattern 对象提出来只编译一次,比每次都走 String.matches 内部的临时编译更省。
Math、Random、System 三小件
Math 全是静态方法,直接类名调用:abs 绝对值、ceil/floor 向上/向下取整、round 四舍五入、pow 幂运算、sqrt 开平方、max/min 取大小。
Random 用于生成随机数:nextInt() 覆盖 int 全范围,nextInt(n) 生成 [0, n) 区间的整数最常用。用相同种子创建的两个 Random 会产生完全相同的序列——这不是 bug,而是”可复现”特性,写测试时很有用。日常开发直接 new Random() 不传种子即可,它会用当前时间作为种子,保证每次运行结果不同。
System 不能被实例化,常用成员:currentTimeMillis() 返回当前毫秒时间戳,常用来测代码耗时;arraycopy 做数组拷贝;exit(0) 终止 JVM;gc() 只是”建议”垃圾回收,不保证立即执行。
long start = System.currentTimeMillis(); |
BigDecimal:为什么不能用 double 算钱
先看两个”灵异”结果:
System.out.println(0.1 + 0.2); // 0.30000000000000004 |
原因在于 double 是二进制浮点数:0.1 这类十进制小数在二进制下是无限循环小数,计算机只能截断后近似存储,误差由此产生。涉及金额等精确计算,必须用 BigDecimal——不可变的、任意精度的十进制数:
BigDecimal a = new BigDecimal("0.1"); // 必须用字符串构造! |
两个必踩的坑:其一,new BigDecimal(0.1) 会把 double 已有的误差原封不动带进来,结果照样失真——只认字符串构造(或 BigDecimal.valueOf);其二,divide 遇到除不尽的情况(如 1 除以 3)若不指定小数位和舍入模式,会直接抛 ArithmeticException。加减乘对应 subtract、multiply,且都返回新对象——BigDecimal 和 String 一样不可变。比较大小请用 compareTo(返回 -1、0、1)而不是 equals,因为 equals 连标度一起比:new BigDecimal("0.1") 与 new BigDecimal("0.10") 数值相等,equals 却返回 false。
Date 与 Calendar:遗留 API,新代码请用 java.time
先亮结论:Date、Calendar、SimpleDateFormat 都是 Java 8 之前的遗留 API。老代码里要能看懂,新代码请一律使用 java.time 包。
痛点盘点:Date 的 getYear() 返回的是”当前年份减 1900”、月份从 0 开始计数,反人类;Date 对象本身可变,多线程共享不安全;SimpleDateFormat 同样线程不安全,并发格式化会解析出错;Calendar 的 add/set 依赖一堆 int 字段常量,写起来繁琐还容易传错。老代码中的典型写法(认识即可,别再主动使用):
Date now = new Date(); // 当前瞬间 |
现代写法(java.time,不可变、线程安全、语义清晰):
LocalDate today = LocalDate.now(); |
java.time 的设计思路是把概念拆开:只要日期用 LocalDate,只要时间用 LocalTime,都要用 LocalDateTime,带时区用 ZonedDateTime——各司其职,再没有 Date 那种”什么都装、什么都不清不楚”的混沌。所有类都不可变,plusDays 之类的”修改”方法返回新对象,DateTimeFormatter 也可以安全地做成全局常量复用。
经典练习
正则校验
boolean qq = "12345".matches("[1-9]\\d{4,14}"); // QQ 号:5-15 位,非 0 开头 |
计算出生到现在多少天
LocalDate birthday = LocalDate.of(1995, 6, 15); |
当年课堂上的解法是用两个 Date 的毫秒差再除以 24 * 60 * 60 * 1000,还要手工处理各种边界;java.time 一行搞定,这就是新 API 的价值。顺带一提,判断闰年也不再需要 Calendar 绕圈:LocalDate.of(year, 1, 1).isLeapYear() 直接给出答案。
提取文本中的所有手机号
直接使用上文第四种姿势的 Pattern/Matcher 代码,把正则换成别的规则(如邮箱 \w+@\w+(\.\w+)+),即可复用为通用提取器。这个”编译规则、循环 find、取出 group”的三步套路适用于所有文本提取场景,比如从日志里捞出所有 IP 地址、从网页里收集所有链接,练熟这一道题等于学会了一整类问题。
小结
- 正则三板斧:字符类定范围、量词定次数、分组定结构;Java 里校验用
matches,切割用split,替换用replaceAll,提取用Pattern + Matcher。 - double 算钱必丢精度;BigDecimal 要配字符串构造和指定的舍入模式。
- Math、Random、System 查表即用;Random 固定种子序列可复现,是特性不是 bug。
- Date、Calendar、SimpleDateFormat 均为遗留 API,存在可变性、线程不安全、API 反人类等问题,新代码一律使用 java.time。

