目录
写在前面
第一部分:日期时间处理
一、从Date到LocalDateTime:一次必要的进化
1.1 曾经的王者:Date类
1.2 新时代的选择:LocalDateTime
二、LocalDateTime的常用操作
2.1 获取日期时间的各个字段
2.2 获取更详细的时间信息
2.3 日期时间的加减运算
2.4 获取当前日期所在周的周一和周日
2.5 获取当前日期所在月的第一天和最后一天
第二部分:BigDecimal与精确运算
三、为什么需要BigDecimal?
四、BigDecimal的创建
4.1 构造方法
4.2 一个值得警惕的问题
五、BigDecimal的常用运算
5.1 加减乘运算
5.2 除法运算与精度控制
5.3 舍入模式详解
5.4 使用MathContext指定精度
六、实战练习
总结
写在前面
在Java编程的日常开发中,有两类需求几乎每个项目都会遇到:一是对日期和时间的各种操作,二是对数值进行精确计算。前者关系到日志记录、定时任务、数据统计等功能,后者则直接影响到金融交易、科学计算等对精度要求极高的场景。
Java在这两方面都提供了丰富的工具类。早期的Date类虽然功能完备,但设计上存在不少瑕疵,很多方法已被标记为过时。好在Java 8引入了全新的日期时间API,彻底改变了我们处理日期的方式。而在数值计算方面,BigDecimal类则为高精度运算提供了可靠的保障。
今天这篇文章,我们就来系统地学习这两类工具类的使用方法和最佳实践。
第一部分:日期时间处理
一、从Date到LocalDateTime:一次必要的进化
1.1 曾经的王者:Date类
在Java早期版本中,java.util.Date是处理日期和时间的核心类。它表示一个特定的时间点,精确到毫秒。
import java.util.Date; public class DateDemo { public static void main(String[] args) { // 获取当前时间 Date now = new Date(); System.out.println(now); // 输出示例:Wed Oct 09 15:30:46 CST 2024 // 创建指定日期 Date specificDate = new Date(123, 2, 5); System.out.println(specificDate); // 输出:Sun Mar 05 00:00:00 CST 2023 } }这里有几个需要注意的地方:
年份偏移:参数123代表的是"1900 + 123 = 2023年",而不是直接表示2023年
月份从0开始:2代表3月,0代表1月,以此类推
未指定时间:如果没有指定时分秒,默认是00:00:00
这样的设计显然不够直观,也容易引发错误。翻阅官方文档会发现,Date类中大部分方法都被标记为@Deprecated(已过时)。这也是为什么我们现在更推荐使用Java 8引入的新日期API。
1.2 新时代的选择:LocalDateTime
LocalDateTime是Java 8中java.time包下的核心类。它解决了Date类的诸多痛点,设计更加清晰、易用。
创建LocalDateTime对象
import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; public class LocalDateTimeDemo { public static void main(String[] args) { // 方式一:获取当前日期和时间 LocalDateTime now = LocalDateTime.now(); System.out.println("当前时间:" + now); // 输出:2024-10-15T14:17:06.747 // 方式二:指定具体日期和时间 LocalDateTime specified = LocalDateTime.of(2023, 5, 15, 10, 30); System.out.println("指定时间:" + specified); // 输出:2023-05-15T10:30 // 方式三:从字符串解析 String dateStr = "2026-10-01 10:30:21"; DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); LocalDateTime parsed = LocalDateTime.parse(dateStr, formatter); System.out.println("解析结果:" + parsed); // 输出:2026-10-01T10:30:21 } }与Date相比,LocalDateTime的设计明显更加友好:年份直接写、月份从1开始、时间可以精确指定。格式化时使用DateTimeFormatter,模式字符串中的yyyy、MM、dd等含义清晰,不需要记忆奇怪的偏移量。
二、LocalDateTime的常用操作
2.1 获取日期时间的各个字段
LocalDateTime now = LocalDateTime.now(); int year = now.getYear(); // 年 int month = now.getMonthValue(); // 月(1-12) int day = now.getDayOfMonth(); // 日 int hour = now.getHour(); // 时 int minute = now.getMinute(); // 分 int second = now.getSecond(); // 秒 System.out.println("年:" + year); System.out.println("月:" + month); System.out.println("日:" + day); System.out.println("时:" + hour); System.out.println("分:" + minute); System.out.println("秒:" + second);2.2 获取更详细的时间信息
LocalDateTime now = LocalDateTime.now(); System.out.println("本周周几:" + now.getDayOfWeek().getValue()); // 周一=1,周二=2,...,周日=7 System.out.println("本月第几天:" + now.getDayOfMonth()); System.out.println("当年第几天:" + now.getDayOfYear());2.3 日期时间的加减运算
LocalDateTime提供了丰富的plus和minus方法,支持对年、月、周、日、时、分、秒进行增减操作:
LocalDateTime now = LocalDateTime.now(); // 增加1天 LocalDateTime plusOneDay = now.plusDays(1); System.out.println("增加1天:" + plusOneDay); // 减少1天 LocalDateTime minusOneDay = now.minusDays(1); System.out.println("减少1天:" + minusOneDay); // 增加1周 LocalDateTime plusOneWeek = now.plusWeeks(1); System.out.println("增加1周:" + plusOneWeek); // 增加1个月 LocalDateTime plusOneMonth = now.plusMonths(1); System.out.println("增加1月:" + plusOneMonth); // 增加1年 LocalDateTime plusOneYear = now.plusYears(1); System.out.println("增加1年:" + plusOneYear);重要提醒:这些方法不会修改原始的LocalDateTime对象,而是返回一个新的对象。这是因为LocalDateTime被设计为不可变类,一旦创建就不能修改。这一点和String的设计思路是一致的。
2.4 获取当前日期所在周的周一和周日
在实际业务中,经常需要获取"本周一"或"本周日"的日期。TemporalAdjusters工具类提供了便捷的方法:
import java.time.DayOfWeek; import java.time.LocalDateTime; import java.time.temporal.TemporalAdjusters; LocalDateTime currentDate = LocalDateTime.now(); // 获取当前日期所在的当周周一 LocalDateTime firstDayOfWeek = currentDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); // 获取当前日期所在的当周周日 LocalDateTime lastDayOfWeek = currentDate.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)); System.out.println("本周周一:" + firstDayOfWeek); System.out.println("本周周日:" + lastDayOfWeek);previousOrSame的含义是:寻找当前日期或之前最近的指定星期几。如果今天是周三,调用previousOrSame(MONDAY)会返回本周一。
nextOrSame的含义是:寻找当前日期或之后最近的指定星期几。如果今天是周三,调用nextOrSame(SUNDAY)会返回本周日。
2.5 获取当前日期所在月的第一天和最后一天
LocalDateTime currentDate = LocalDateTime.now(); // 当月第一天 LocalDateTime firstDayOfMonth = currentDate.with(TemporalAdjusters.firstDayOfMonth()); // 当月最后一天 LocalDateTime lastDayOfMonth = currentDate.with(TemporalAdjusters.lastDayOfMonth()); System.out.println("本月第一天:" + firstDayOfMonth); System.out.println("本月最后一天:" + lastDayOfMonth);第二部分:BigDecimal与精确运算
三、为什么需要BigDecimal?
在日常编程中,我们经常会使用double或float来处理小数。但你有没有遇到过这样的情况:
double a = 0.1; double b = 0.2; System.out.println(a + b); // 期望:0.3,实际:0.30000000000000004这并不是Java的bug,而是二进制浮点数本身的局限性。计算机用二进制表示小数时,有些十进制小数无法被精确表示(就像十进制无法精确表示1/3一样)。这种精度误差在金融、科学计算等场景中是不能接受的。
BigDecimal就是为了解决这个问题而生的。它提供了任意精度的十进制运算能力。
四、BigDecimal的创建
4.1 构造方法
BigDecimal提供了多种构造方法:
import java.math.BigDecimal; BigDecimal fromInt = new BigDecimal(10); // 从整数创建 BigDecimal fromLong = new BigDecimal(100L); // 从长整数创建 BigDecimal fromDouble = new BigDecimal(0.1); // 从double创建(不推荐) BigDecimal fromString = new BigDecimal("0.1"); // 从字符串创建(推荐)4.2 一个值得警惕的问题
来看这段代码:
BigDecimal d1 = new BigDecimal(1.99); BigDecimal d2 = new BigDecimal("1.99"); System.out.println(d1); // 输出:1.9899999999999999911182158029987476766109466552734375 System.out.println(d2); // 输出:1.99同样的数值1.99,使用double构造器和字符串构造器得到了截然不同的结果。原因在于:new BigDecimal(1.99)接收的是一个已经存在精度损失的double值,BigDecimal只是忠实地还原了这个不精确的值。
官方文档明确指出:推荐优先使用字符串构造器。如果必须使用double值,可以考虑BigDecimal.valueOf(double)方法,它内部会先将double转为字符串再构造。
五、BigDecimal的常用运算
5.1 加减乘运算
BigDecimal a = new BigDecimal("1.35"); BigDecimal b = new BigDecimal("3.22"); // 加法 BigDecimal sum = a.add(b); System.out.println("加法:" + sum); // 4.57 // 减法 BigDecimal diff = a.subtract(b); System.out.println("减法:" + diff); // -1.87 // 乘法 BigDecimal product = a.multiply(b); System.out.println("乘法:" + product); // 4.3470注意:BigDecimal对象是不可变的,加减乘运算都会返回新的BigDecimal对象,不会修改原对象。
5.2 除法运算与精度控制
除法运算比加减乘要复杂一些,因为两个数相除的结果可能是无限循环小数。
BigDecimal a = new BigDecimal("1.35"); BigDecimal b = new BigDecimal("3.22"); // 直接除法:如果结果是无限循环小数,会抛出异常 // BigDecimal result = a.divide(b); // ArithmeticException! // 正确做法:指定精度和舍入模式 BigDecimal result = a.divide(b, 4, RoundingMode.HALF_UP); System.out.println(result); // 0.4193divide方法的三个参数分别是:
精度:保留几位小数
舍入模式:如何处理多余的小数位
5.3 舍入模式详解
BigDecimal提供了多种舍入模式,适用于不同的业务场景:
模式 | 说明 | 示例 |
|---|---|---|
UP | 远离零方向舍入 | 1.5→2,-1.5→-2 |
DOWN | 靠近零方向舍入 | 1.5→1,-1.5→-1 |
CEILING | 向正无穷大方向舍入 | 1.5→2,-1.5→-1 |
FLOOR | 向负无穷大方向舍入 | 1.5→1,-1.5→-2 |
HALF_UP | 四舍五入(>=0.5向上) | 1.5→2,1.4→1 |
HALF_DOWN | 五舍六入(>0.5向上) | 1.5→1,1.6→2 |
HALF_EVEN | 银行家舍入法 | 1.5→2,2.5→2 |
UNNECESSARY | 不允许舍入,否则抛异常 | — |
实际开发建议:绝大多数场景使用HALF_UP(四舍五入)即可。金融场景中有时会使用HALF_EVEN(银行家舍入法),因为它能最大程度减少舍入操作带来的统计偏差。
5.4 使用MathContext指定精度
除了直接在divide方法中指定精度,也可以使用MathContext对象:
BigDecimal a = new BigDecimal("1.35"); BigDecimal b = new BigDecimal("3.22"); MathContext mc = new MathContext(4, RoundingMode.HALF_UP); BigDecimal result = a.divide(b, mc); System.out.println(result); // 0.4193MathContext的第一个参数是精度位数,第二个参数是舍入模式。如果不指定舍入模式,默认使用HALF_UP。
六、实战练习
题目: 计算商品的总价,并对结果进行四舍五入保留两位小数。
import java.math.BigDecimal; import java.math.RoundingMode; public class ShoppingCart { public static void main(String[] args) { // 商品单价 BigDecimal price = new BigDecimal("29.99"); // 购买数量 BigDecimal quantity = new BigDecimal("3"); // 计算总价 BigDecimal total = price.multiply(quantity); System.out.println("原始总价:" + total); // 89.97 // 折扣 BigDecimal discountRate = new BigDecimal("0.88"); BigDecimal discountedTotal = total.multiply(discountRate); System.out.println("打折后总价:" + discountedTotal); // 79.1736 // 四舍五入保留两位小数 BigDecimal finalPrice = discountedTotal.setScale(2, RoundingMode.HALF_UP); System.out.println("最终支付金额:" + finalPrice); // 79.17 } }总结
今天我们学习了Java中两个非常实用的工具类体系:
日期时间处理方面:
Date类是早期的日期时间类,存在设计缺陷,很多方法已过时LocalDateTime是Java 8引入的新API,设计更清晰、使用更方便支持获取年、月、日、时、分、秒等各个字段
支持对日期时间进行加减运算,返回新的不可变对象
通过
TemporalAdjusters可以方便地获取周初、月末等特殊日期
精确数值计算方面:
double和float存在精度问题,不适合金融等敏感场景BigDecimal提供了任意精度的十进制运算能力创建BigDecimal时推荐使用字符串构造器,避免精度损失
除法运算时必须指定精度和舍入模式
舍入模式有多种选择,
HALF_UP是最常用的四舍五入模式
掌握了这些工具类的使用,你在处理日期时间和数值计算时就能游刃有余了。