本篇是 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");
System.out.println(s); // abc:$1 引用第 1 组的内容

Java 中使用正则的四种姿势

// 1. 整体校验:String.matches,要求整个字符串都符合规则
boolean ok = "12345".matches("[1-9]\\d{4,14}");

// 2. 切割:split,按匹配项把字符串拆开
String[] parts = "a1b22c333".split("\\d+"); // [a, b, c]

// 3. 替换:replaceAll,把匹配项替换掉
String phone = "我的电话是13800138000".replaceAll("1[3-9]\\d{9}", "***");

// 4. 查找提取:Pattern + Matcher,从文本中"捞"出所有匹配片段
Pattern p = Pattern.compile("1[3-9]\\d{9}");
Matcher m = p.matcher("联系我13800138000或者13912345678");
while (m.find()) {
System.out.println(m.group()); // 依次输出两个手机号
}

关键区分: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();
// ……被测代码……
System.out.println("耗时:" + (System.currentTimeMillis() - start) + "ms");

BigDecimal:为什么不能用 double 算钱

先看两个”灵异”结果:

System.out.println(0.1 + 0.2);   // 0.30000000000000004
System.out.println(1.0 - 0.9); // 0.09999999999999998

原因在于 double 是二进制浮点数:0.1 这类十进制小数在二进制下是无限循环小数,计算机只能截断后近似存储,误差由此产生。涉及金额等精确计算,必须用 BigDecimal——不可变的、任意精度的十进制数:

BigDecimal a = new BigDecimal("0.1");          // 必须用字符串构造!
BigDecimal b = new BigDecimal("0.2");
System.out.println(a.add(b)); // 0.3
BigDecimal c = a.divide(b, 2, RoundingMode.HALF_UP); // 除法指定精度与舍入

两个必踩的坑:其一,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();                                   // 当前瞬间
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String s = sdf.format(now); // 日期 → 字符串
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DAY_OF_MONTH, 7); // 一周后

现代写法(java.time,不可变、线程安全、语义清晰):

LocalDate today = LocalDate.now();
LocalDate nextWeek = today.plusDays(7);
LocalDateTime dt = LocalDateTime.of(2026, 9, 1, 12, 30);
String s = dt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));

java.time 的设计思路是把概念拆开:只要日期用 LocalDate,只要时间用 LocalTime,都要用 LocalDateTime,带时区用 ZonedDateTime——各司其职,再没有 Date 那种”什么都装、什么都不清不楚”的混沌。所有类都不可变,plusDays 之类的”修改”方法返回新对象,DateTimeFormatter 也可以安全地做成全局常量复用。

经典练习

正则校验

boolean qq = "12345".matches("[1-9]\\d{4,14}");       // QQ 号:5-15 位,非 0 开头
boolean tel = "13800138000".matches("1[3-9]\\d{9}"); // 手机号

计算出生到现在多少天

LocalDate birthday = LocalDate.of(1995, 6, 15);
long days = ChronoUnit.DAYS.between(birthday, LocalDate.now());
System.out.println("你来到这个世界 " + days + " 天了");

当年课堂上的解法是用两个 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。