16.Java常用工具类精讲:日期处理与精确运算
目录写在前面第一部分日期时间处理一、从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 新时代的选择LocalDateTimeLocalDateTime是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→-2DOWN靠近零方向舍入1.5→1-1.5→-1CEILING向正无穷大方向舍入1.5→2-1.5→-1FLOOR向负无穷大方向舍入1.5→1-1.5→-2HALF_UP四舍五入0.5向上1.5→21.4→1HALF_DOWN五舍六入0.5向上1.5→11.6→2HALF_EVEN银行家舍入法1.5→22.5→2UNNECESSARY不允许舍入否则抛异常—实际开发建议绝大多数场景使用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是最常用的四舍五入模式掌握了这些工具类的使用你在处理日期时间和数值计算时就能游刃有余了。