☰
Java日期类与BigDecimal工具类实战:从LocalDateTime到精度控制
2026/10/15 6:06:22 网站建设 项目流程

从入门到实战,Java 日期类与 BigDecimal 工具类到底怎么用?这个话题我在带新人的时候几乎每次都会聊到。很多初学者把精力都放在语法、集合、继承这些大块头上,结果一到真实业务开发就被日期格式化和金额计算磨得头疼——时间戳转字符串翻车、月份莫名少一天、两个 double 相加跑出一长串小数,甚至金额越算越"歪"。这些问题的根源,几乎都在于没把 Java 的日期类和 BigDecimal 工具类吃透。

今天这篇内容我就把这两个高频工具类一次性讲清楚:从老牌 API 的历史包袱,到新日期 API 的正确打开方式,再到 BigDecimal 的精度玄机,搭配可直接复制的示例代码和我在实际开发里踩过的坑。无论你是刚学完 Java 基础、准备做第一个项目的学生,还是已经写了一阵子 CRUD 但没系统梳理过日期和精度处理的新人程序员,这篇都值得你完整读一遍。

1. 为什么日期和精度处理是 Java 入门绕不开的两座山

先聊点实际的。回想一下你接触过或听说过的业务系统,订单要记录下单时间,用户要填写生日,日志要打时间戳,优惠活动要判断是否在有效期内——这些都是日期时间的应用场景。而只要涉及钱,无论是商品价格、订单金额、优惠折扣,还是账户余额,几乎都离不开精确的小数运算。换句话说,日期类和 BigDecimal 不是"偶尔用一下"的工具,而是你写业务代码时几乎每天都要碰的东西。

但这两类工具恰好又是 Java 里"坑点"较多的部分。老日期 API 设计得比较粗糙,月份从 0 开始计数、格式化对象线程不安全,这些反直觉的设定很容易让新人莫名其妙地拿到错误结果。而浮点数计算更是天然有精度问题,如果直接拿double去做金额加减,轻则四舍五入不准确,重则财务对账不平。正因为它们的使用频率高、出错率也不低,才值得在入门阶段就系统地把原理和用法一并弄清,而不是等出了问题才回头补课。

我当时带的一个新人同学,曾经用double去计算订单总额,又用SimpleDateFormat把日期格式化成"yyyy-MM-dd HH:mm",结果上线没过多久就发现有几张订单的金额差了几分钱,有一批订单的日期显示提前了一天。排查到最后,就是这两个工具类的使用姿势不对。所以我一直认为,这两个知识点值得单独拿出来讲透。

2. 老牌日期 API:不到万不得已不推荐

2.1 Date:一个设计上有些"随性"的类

在 Java 8 之前,日常处理日期时间主要靠java.util.Date。这个类表面上只是"一个时间点",但它隐藏了很多容易踩的地方:

Date now = new Date(); System.out.println(now); // 输出类似 Tue Jun 25 14:30:22 CST 2025

输出结果里带了星期几、几月几日、时区缩写,格式看起来"不伦不类",而且很难按自己的想法控制。更麻烦的是,Date的很多方法都已被标记为过时,比如getYear()、getMonth()、getDay()等,官方早就不建议使用了。很多老教程里还会出现now.getMonth()这种写法,如果你在某个月份的 1 号运行,一不留神就会发现返回结果偏小——因为Date中的月份同样是从 0 开始计数的。

2.2 Calendar:月份从 0 开始的"反直觉"设计

Calendar是Date的"升级版",专门用来做日期运算。它的典型用法是这样的:

Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DAY_OF_MONTH, 7); Date afterWeek = calendar.getTime();

看起来没什么毛病,但你试试读取月份:

int month = calendar.get(Calendar.MONTH);

如果当前是 6 月,这里返回的其实是5。因为Calendar延续了"月份从 0 开始"的设计,Calendar.JANUARY等于0,十二月才等于11。别说新手,我见过有三年经验的老手偶尔也会在这个地方愣一下。

Calendar还有一个让人头疼的用法——设置时间。你需要挨个设置年月日时分秒:

Calendar birth = Calendar.getInstance(); birth.set(2000, Calendar.JANUARY, 1);

这种写法既啰嗦又容易出错,一旦月份设置错一位,整个日期就全错了。

2.3 SimpleDateFormat 的线程安全隐患

SimpleDateFormat是配合老日期 API 做格式化解析的常用类。它用起来简单:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String text = sdf.format(new Date()); Date date = sdf.parse("2025-06-25 14:30:22");

但这里有一个隐藏的大坑:SimpleDateFormat不是线程安全的。在多线程环境下,如果多个线程共用同一个SimpleDateFormat实例去parse或format,轻则结果不对,重则直接抛NumberFormatException。我印象里有个同事就是这样,在并发场景里复用一个SimpleDateFormat静态实例,上线后日志里隔三差五就出现莫名其妙的日期解析异常。

要解决倒是也有办法,比如每次新建实例,或者用ThreadLocal包一层。但既然 Java 8 已经提供了更安全、更好用的新日期 API,就没必要再去费这个劲了。

3. Java 8 新日期 API:现代开发的标准答案

3.1 LocalDate / LocalTime / LocalDateTime 的基础操作

从 Java 8 开始,官方引入了全新的日期时间 API,核心类就是LocalDate、LocalTime和LocalDateTime。它们用起来非常直观,而且不可变——所有加减操作都会返回一个新的对象,原对象不受影响,这天然就对多线程友好。

// 获取当前日期 LocalDate today = LocalDate.now(); // 获取当前时间 LocalTime nowTime = LocalTime.now(); // 获取当前日期和时间 LocalDateTime nowDateTime = LocalDateTime.now();

创建指定日期同样很直接:

LocalDate birthday = LocalDate.of(2000, 1, 1); LocalDateTime meeting = LocalDateTime.of(2025, 6, 25, 14, 30);

这个设计就比Calendar舒服太多了,年月日就是人类正常的 1~12 月和 1~31 日,不用做任何偏移。我当时让新人同学第一次用LocalDate.of(2000, 1, 1)的时候,他下意识问:"月份真的不用减一吗?"我说不用,他愣了好一会儿才适应过来。

3.2 日期运算:链式调用的优雅体验

新 API 把日期运算做成了非常自然的链式调用:

LocalDateTime now = LocalDateTime.now(); LocalDateTime nextWeek = now.plusWeeks(1); LocalDateTime twoHoursLater = now.plusHours(2); LocalDate prevMonth = today.minusMonths(1);

看到这里你会发现,plusXxx和minusXxx的语义非常清楚。更爽的是你可以直接连续调用:

LocalDateTime target = now.plusDays(3) .minusHours(2) .withHour(9) .withMinute(30);

withHour、withMinute这类方法表示"把某一位改成指定值",合并使用就能很方便地得到"三天后上午 9 点半"这种结果。做定时任务调度、报表统计区间时,这套写法能省不少代码量。

3.3 格式化与解析:DateTimeFormatter 才是线程安全的选择

新 API 对应的格式化类是DateTimeFormatter,它是线程安全的,可以放心做成静态常量复用:

DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); // 格式化:日期时间 → 字符串 String text = LocalDateTime.now().format(fmt); // 解析:字符串 → 日期时间 LocalDateTime parsed = LocalDateTime.parse("2025-06-25 14:30:22", fmt);

以后在项目里看到有人自定义日期格式,我基本都会建议他用DateTimeFormatter,而不是SimpleDateFormat。这不仅仅是代码风格的问题,更是并发安全的问题。

3.4 日期比较与区间天数计算

判断两个日期的先后,新 API 提供了非常语义化的方法:

LocalDate today = LocalDate.now(); LocalDate deadline = LocalDate.of(2025, 7, 1); boolean before = today.isBefore(deadline); // true boolean after = today.isAfter(deadline); // false boolean equal = today.isEqual(deadline); // false

计算两个日期之间相差多少天,用ChronoUnit一行搞定:

long days = ChronoUnit.DAYS.between(today, deadline);

想算月份差、年份差,把枚举换成MONTHS、YEARS就行。我在做会员有效期判断的时候就用这个,代码简洁且没有歧义。

4. BigDecimal:从"0.1+0.2"翻车说起

4.1 浮点精度问题到底是怎么回事

先做个小实验:

double a = 0.1; double b = 0.2; System.out.println(a + b); // 输出 0.30000000000000004

试过的人都知道,结果并不是直观的 0.3。原因在于,计算机内部是用二进制存储浮点数的,而 0.1、0.2 这类十进制小数无法用二进制有限位精确表示,就像十进制也无法用有限位精确表示 1/3 一样。于是运算结果带上了一个极小的误差。

如果这样的误差用在普通科学计算里,可能无伤大雅;但如果用在金额计算上,就会出现大问题。假设商品单价是 0.1 元,买了 3 件,用double算出来的总价可能不是你预期的 0.3 元,而是一个带长尾小数的数字。对账时差一分钱都叫人头大。

4.2 BigDecimal 的正确打开方式

BigDecimal就是为了精确表示和计算十进制小数而生的工具类。它有两个需要特别注意的构造方法:

// 推荐:优先使用字符串构造 BigDecimal a = new BigDecimal("0.1"); // 不推荐:用 double 构造会保留二进制的误差 BigDecimal b = new BigDecimal(0.1); System.out.println(b); // 输出 0.1000000000000000055511151231257827021181583404541015625

你看,new BigDecimal(0.1)得到的并不是 0.1,而是一大串"长了尾巴"的数字,因为double的误差被原样带进了BigDecimal。这是新人最容易踩的坑。实战中请一定用字符串构造,或者用BigDecimal.valueOf()方法:

BigDecimal c = BigDecimal.valueOf(0.1);

valueOf内部会先转成字符串再构造,所以用起来更省心。

4.3 四则运算与保留小数位

BigDecimal的四则运算方法分别是add、subtract、multiply、divide。注意它们的减法到底怎么写:

BigDecimal price = new BigDecimal("19.90"); BigDecimal count = new BigDecimal("3"); BigDecimal subtotal = price.multiply(count); // 乘法 BigDecimal discount = new BigDecimal("0.85"); BigDecimal pay = subtotal.multiply(discount); // 折后总价 BigDecimal difference = pay.subtract(subtotal); // 减法,注意是调用者在前

加法和减法都是"谁在前谁就是被加数/被减数",因为BigDecimal是不可变对象,不会修改自身,只能返回新对象。

算出来的结果如果小数位过多,就要用setScale保留固定位数:

BigDecimal total = pay.setScale(2, RoundingMode.HALF_UP);

这里的HALF_UP就是我们熟悉的四舍五入。它还有兄弟模式:

  • RoundingMode.UP:远离零方向舍入,即直接进位
  • RoundingMode.DOWN:趋向零方向舍入,即直接截断
  • RoundingMode.HALF_UP:四舍五入,5 进 1
  • RoundingMode.HALF_DOWN:五舍六入,5 不进 1
  • RoundingMode.HALF_EVEN:银行家舍入,5 时看前一位是奇数还是偶数决定是否进位

财务系统里偶尔会用到HALF_EVEN,日常业务用HALF_UP就够了。

4.4 除法一定要记得指定精度

BigDecimal除法有个很经典的坑:若除不尽,会直接抛异常。

BigDecimal amount = new BigDecimal("100"); BigDecimal people = new BigDecimal("3"); // 这里如果不指定精度,会抛 ArithmeticException // BigDecimal result = amount.divide(people); // 正确做法:指定小数位数和舍入模式 BigDecimal each = amount.divide(people, 2, RoundingMode.HALF_UP); System.out.println(each); // 33.33

我看到很多入门同学第一次写除法时就卡在这个异常上,其实只要记住"divide必带精度和舍入模式"这个口诀,就能避开绝大多数问题。

4.5 比较数值要用 compareTo,不要用 equals

BigDecimal的equals方法比较的是"数值 + 精度",也就是说1.0和1.00用equals比较结果是false,因为它们的精度不一样。但它们的数值明明是一样的:

BigDecimal x = new BigDecimal("1.0"); BigDecimal y = new BigDecimal("1.00"); System.out.println(x.equals(y)); // false,精度不同 System.out.println(x.compareTo(y)); // 0,数值相等

所以,只要你想比较的是"数值大小",就应该用compareTo。返回值是负数表示小于,0 表示等于,正数表示大于:

if (x.compareTo(y) == 0) { System.out.println("数值相等"); }

这个细节在我做价格阈值判断时经常用到,如果当时用equals判断,很容易得到预料之外的结果。

5. 实操演练:日常开发里最高频的几个场景

5.1 场景一:生成订单时间并格式化

DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String orderTime = LocalDateTime.now().format(fmt); System.out.println("订单时间:" + orderTime);

这个几乎每个有"下单"功能的后端都会用到。把DateTimeFormatter定义成常量,放在某个工具类里统一使用,避免到处new。

5.2 场景二:计算用户注册时长

LocalDate registerDate = LocalDate.of(2025, 1, 1); long daysRegistered = ChronoUnit.DAYS.between(registerDate, LocalDate.now()); System.out.println("注册天数:" + daysRegistered);

注意ChronoUnit.DAYS.between的第一个参数是起始日期,第二个是结束日期,顺序别写反。

5.3 场景三:金额结算与四舍五入

BigDecimal price = new BigDecimal("19.90"); BigDecimal quantity = new BigDecimal("3"); BigDecimal subtotal = price.multiply(quantity); BigDecimal discount = new BigDecimal("0.88"); BigDecimal payable = subtotal.multiply(discount) .setScale(2, RoundingMode.HALF_UP); System.out.println("应付金额:" + payable);

如果subtotal.multiply(discount)的结果小数位超过两位,setScale会帮你规整到分。这是最典型的金额处理流程。

5.4 场景四:判断活动是否在有效期内

LocalDate today = LocalDate.now(); LocalDate start = LocalDate.of(2025, 6, 1); LocalDate end = LocalDate.of(2025, 6, 30); if (!today.isBefore(start) && !today.isAfter(end)) { System.out.println("活动进行中"); }

用!isBefore和!isAfter表示"大于等于开始日期"和"小于等于结束日期",比直接比较时间戳可读性好得多。

5.5 场景五:将用户输入的日期字符串转为日期对象

DateTimeFormatter dateFmt = DateTimeFormatter.ofPattern("yyyy-MM-dd"); LocalDate parsedDate = LocalDate.parse("2025-06-25", dateFmt);

很多接口接收前端传来的日期字符串,这里用parse就能干净地转成日期对象,再丢给后续逻辑处理。

6. 常见问题与排查技巧实录

我在指导新人做项目时,把这两类工具最常见的报错和"看起来没问题但结果不对"的案例整理成了一张速查表,方便大家定位问题。

现象根因正确处理
计算new BigDecimal(0.1)得到超长小数用了double构造,二进制的误差被带入使用new BigDecimal("0.1")或BigDecimal.valueOf(0.1)
divide抛ArithmeticException除不尽又没指定精度与舍入模式写作a.divide(b, 2, RoundingMode.HALF_UP)
BigDecimal用equals比较不相等equals会连精度一起比较数值比较应使用compareTo
用SimpleDateFormat在多线程下解析异常该类线程不安全改用DateTimeFormatter
用Calendar取月份比实际小 1月份从 0 开始尽量使用LocalDate/LocalDateTime
日期格式化后多了T字母直接toString()而不是用指定格式调用format(DateTimeFormatter.ofPattern(...))

这几条基本覆盖了 90% 的入门踩坑场景。如果你在项目里遇到"日期/金额结果不对"的问题,先对着这张表排查一遍,大概率能找到答案。

最后分享一点我个人的体会:工具类学习最忌讳"背 API 用法",而是要理解"为什么这么设计"。比如搞清楚LocalDateTime为什么不可变,你就能理解为什么所有方法都会返回新对象;搞清楚double为什么有精度误差,你就能明白BigDecimal为什么推荐用字符串构造。这比记一百个方法名都管用。把这个底子打好,后面再去学Instant、Duration、Period这些扩展类,都会顺利很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询