Java 日期时间处理三剑客:YearMonth、LocalDate 与 DateUtils 深度解析
·
一、 引言:告别 Date 的混乱
- 痛点回顾: 简要回顾
java.util.Date和java.util.Calendar的痛点(如可变性、线程不安全、API 设计不直观、时区处理复杂等)。 - JSR-310 的曙光: 介绍 Java 8 引入的
java.time包(JSR-310),强调其不可变性、线程安全、清晰的 API 设计和强大的功能。 - 本文目标: 聚焦
java.time.YearMonth、java.time.LocalDate和org.apache.commons.lang3.time.DateUtils这三个在实际开发中极为常用的工具/类,进行详细讲解和对比。
二、 java.time.YearMonth:精准掌控年月
YearMonth 类专门用于表示一个特定的年份和月份,不包含日、时、分、秒和时区信息。非常适合处理账单周期、统计报表、有效期等场景。
-
核心概念:
- 表示一个年份和月份的组合,例如 "2023-10"。
- 不可变且线程安全。
- 常用静态工厂方法创建。
-
核心方法详解与示例:
import java.time.YearMonth; import java.time.temporal.ChronoUnit; public class YearMonthExample { public static void main(String[] args) { // 1. 创建 YearMonth 实例 YearMonth currentYM = YearMonth.now(); // 当前年月 (如 2025-08) YearMonth specificYM = YearMonth.of(2023, 10); // 2023年10月 YearMonth fromString = YearMonth.parse("2024-02"); // 从字符串解析 System.out.println("当前年月: " + currentYM); System.out.println("指定年月: " + specificYM); System.out.println("解析年月: " + fromString); // 2. 获取信息 int year = currentYM.getYear(); // 2025 int monthValue = currentYM.getMonthValue(); // 8 (月份数值) String monthName = currentYM.getMonth().name(); // AUGUST (月份枚举名) int lengthOfMonth = currentYM.lengthOfMonth(); // 31 (该月天数,考虑闰年) int lengthOfYear = currentYM.lengthOfYear(); // 365 (该年天数) System.out.println(year + "-" + monthValue + " 有 " + lengthOfMonth + " 天"); System.out.println("月份名称: " + monthName); // 3. 日期运算 (返回新实例) YearMonth nextMonth = currentYM.plusMonths(1); // 下一个月 YearMonth lastYear = currentYM.minusYears(1); // 去年同月 YearMonth twoYearsLater = currentYM.plus(2, ChronoUnit.YEARS); // 两年后同月 System.out.println("下个月: " + nextMonth); System.out.println("去年同月: " + lastYear); System.out.println("两年后同月: " + twoYearsLater); // 4. 与其他日期类的转换 // YearMonth -> LocalDate (取该月第一天) LocalDate firstDayOfMonth = currentYM.atDay(1); // 2025-08-01 // YearMonth -> LocalDate (取该月最后一天) LocalDate lastDayOfMonth = currentYM.atEndOfMonth(); // 2025-08-31 System.out.println("本月第一天: " + firstDayOfMonth); System.out.println("本月最后一天: " + lastDayOfMonth); // LocalDate -> YearMonth LocalDate someDate = LocalDate.of(2023, 5, 15); YearMonth ymFromLocalDate = YearMonth.from(someDate); // 2023-05 System.out.println("从 LocalDate 转换: " + ymFromLocalDate); // 5. 比较 boolean isAfter = currentYM.isAfter(specificYM); // true boolean isBefore = currentYM.isBefore(specificYM); // false boolean isEqual = currentYM.equals(specificYM); // false int comparison = currentYM.compareTo(specificYM); // >0 System.out.println("是否在 2023-10 之后: " + isAfter); } } -
适用场景:
- 信用卡有效期。
- 账单周期(如“2023年10月账单”)。
- 按月统计的数据(如销售月报)。
- 有效期检查(如“2024-06”是否过期)。
三、 java.time.LocalDate:处理“日”级别的利器
LocalDate 表示一个不带时区的日期,包含年、月、日信息。是处理日期逻辑最常用的类。
-
核心概念:
- 表示一个日期,如 "2025-08-22"。
- 不可变且线程安全。
- 不包含时间或时区。
-
核心方法详解与示例:
import java.time.LocalDate; import java.time.DayOfWeek; import java.time.Month; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAdjusters; public class LocalDateExample { public static void main(String[] args) { // 1. 创建 LocalDate 实例 LocalDate today = LocalDate.now(); // 当前日期 LocalDate specificDate = LocalDate.of(2023, 10, 25); // 指定年月日 LocalDate fromString = LocalDate.parse("2024-02-29"); // 从字符串解析 (需有效日期) System.out.println("今天: " + today); System.out.println("指定日期: " + specificDate); System.out.println("解析日期: " + fromString); // 2. 获取信息 int year = today.getYear(); // 2025 Month month = today.getMonth(); // AUGUST (枚举) int monthValue = today.getMonthValue(); // 8 int dayOfMonth = today.getDayOfMonth(); // 22 DayOfWeek dayOfWeek = today.getDayOfWeek(); // FRIDAY (枚举) int dayOfYear = today.getDayOfYear(); // 234 (一年中的第几天) System.out.println("今天是 " + year + " 年 " + month + " 月 " + dayOfMonth + " 日"); System.out.println("星期: " + dayOfWeek + ", 一年中的第 " + dayOfYear + " 天"); // 3. 日期运算 (返回新实例) LocalDate tomorrow = today.plusDays(1); // 明天 LocalDate yesterday = today.minus(1, ChronoUnit.DAYS); // 昨天 LocalDate nextWeek = today.plusWeeks(1); // 一周后 LocalDate nextMonth = today.plusMonths(1); // 一个月后 LocalDate nextYear = today.plusYears(1); // 一年后 System.out.println("明天: " + tomorrow); System.out.println("昨天: " + yesterday); System.out.println("下周此时: " + nextWeek); // 4. 日期调整器 (TemporalAdjusters) LocalDate firstDayOfMonth = today.with(TemporalAdjusters.firstDayOfMonth()); // 本月第一天 LocalDate lastDayOfMonth = today.with(TemporalAdjusters.lastDayOfMonth()); // 本月最后一天 LocalDate firstDayOfNextMonth = today.with(TemporalAdjusters.firstDayOfNextMonth()); // 下月第一天 LocalDate firstInMonth = today.with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY)); // 本月第一个星期一 LocalDate nextFriday = today.with(TemporalAdjusters.next(DayOfWeek.FRIDAY)); // 下一个星期五 (如果今天不是周五) System.out.println("本月第一天: " + firstDayOfMonth); System.out.println("本月最后一天: " + lastDayOfMonth); System.out.println("下个月第一天: " + firstDayOfNextMonth); System.out.println("本月第一个星期一: " + firstInMonth); System.out.println("下一个星期五: " + nextFriday); // 5. 比较 boolean isAfter = today.isAfter(specificDate); // true boolean isBefore = today.isBefore(specificDate); // false boolean isEqual = today.equals(specificDate); // false long daysBetween = ChronoUnit.DAYS.between(specificDate, today); // 计算天数差 System.out.println("距离 2023-10-25 已经过了 " + daysBetween + " 天"); // 6. 与 YearMonth 的转换 (已在 YearMonth 部分展示) YearMonth currentYM = YearMonth.from(today); LocalDate firstDay = currentYM.atDay(1); LocalDate lastDay = currentYM.atEndOfMonth(); } } -
适用场景:
- 用户生日、纪念日。
- 事件日期(如会议、截止日期)。
- 日报、考勤记录。
- 任何需要精确到“日”的日期逻辑。
四、 org.apache.commons.lang3.time.DateUtils:经典工具类的现代化应用
虽然 java.time 是首选,但 DateUtils 在处理遗留的 java.util.Date 和 Calendar 时,以及提供一些便捷操作方面仍有价值。重点强调:新项目应优先使用 java.time,DateUtils 主要用于兼容旧代码或特定便捷方法。
-
核心价值:
- 简化
Date和Calendar的操作。 - 提供
java.time之前缺失的便捷功能(如日期截断、舍入)。 - 与
java.time交互的桥梁(部分方法)。
- 简化
-
核心方法详解与示例 (需引入
commons-lang3依赖):<!-- Maven 依赖 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.12.0</version> <!-- 使用最新稳定版 --> </dependency>import org.apache.commons.lang3.time.DateUtils; import java.util.Date; import java.util.Calendar; import java.util.GregorianCalendar; import java.text.SimpleDateFormat; public class DateUtilsExample { public static void main(String[] args) throws Exception { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 1. 创建 Date (通常不推荐,但有时需要) Date now = new Date(); // 当前时间 Date specificDate = sdf.parse("2023-10-25 14:30:00"); // 从字符串解析 // 2. 日期运算 (修改原对象,需注意!) // 注意:这些方法会修改传入的 Date 对象本身! Date tomorrow = DateUtils.addDays(now, 1); // 明天 (now 也被修改了!) Date yesterday = DateUtils.addHours(now, -24); // 昨天 (now 再次被修改) // 更安全的做法:先克隆 Date safeTomorrow = DateUtils.addDays((Date) now.clone(), 1); System.out.println("原始 now: " + sdf.format(now)); System.out.println("克隆后加一天: " + sdf.format(safeTomorrow)); // 3. 日期截断 (Truncate) - 非常有用 // 将时间部分截断到指定粒度 Date truncatedToDay = DateUtils.truncate(now, Calendar.DATE); // 截断到天 (00:00:00) Date truncatedToHour = DateUtils.truncate(now, Calendar.HOUR); // 截断到小时 (:00:00) Date truncatedToMinute = DateUtils.truncate(now, Calendar.MINUTE); // 截断到分钟 (:00) System.out.println("截断到天: " + sdf.format(truncatedToDay)); System.out.println("截断到小时: " + sdf.format(truncatedToHour)); // 4. 日期舍入 (Round) - 非常有用 // 将时间部分舍入到指定粒度 Date roundedToHour = DateUtils.round(now, Calendar.HOUR); // 舍入到最近的小时 Date roundedToMinute = DateUtils.round(now, Calendar.MINUTE); // 舍入到最近的分钟 System.out.println("舍入到小时: " + sdf.format(roundedToHour)); System.out.println("舍入到分钟: " + sdf.format(roundedToMinute)); // 5. 日期比较 boolean isSameDay = DateUtils.isSameDay(now, specificDate); // 比较年月日是否相同 boolean isSameInstant = DateUtils.isSameInstant(now, specificDate); // 比较毫秒数是否相同 System.out.println("是否同一天: " + isSameDay); System.out.println("是否同一时刻: " + isSameInstant); // 6. 与 Calendar 交互 Calendar cal = new GregorianCalendar(); cal.setTime(now); DateUtils.addMonths(cal, 2); // 给 Calendar 加2个月 (cal 被修改) System.out.println("Calendar 加2个月后: " + sdf.format(cal.getTime())); // 7. 便捷的 parse 方法 (可指定多种格式) // 注意:DateUtils.parseDate() 可以尝试多种预定义格式 String dateStr1 = "2023-10-25"; String dateStr2 = "10/25/2023"; Date parsed1 = DateUtils.parseDate(dateStr1, "yyyy-MM-dd"); Date parsed2 = DateUtils.parseDate(dateStr2, "MM/dd/yyyy"); // 或者使用 parseDateStrictly // Date parsedStrict = DateUtils.parseDateStrictly(dateStr1, "yyyy-MM-dd"); System.out.println("解析 " + dateStr1 + ": " + sdf.format(parsed1)); System.out.println("解析 " + dateStr2 + ": " + sdf.format(parsed2)); } } -
重要注意事项:
- 可变性:
DateUtils的addXxx方法会修改传入的Date或Calendar对象本身!这与java.time的不可变设计背道而驰。使用时务必小心,必要时先克隆 (clone())。 - 线程安全:
Date和Calendar本身不是线程安全的,DateUtils的方法也无法改变这一点。 - 推荐场景:
- 在无法立即迁移到
java.time的旧项目中。 - 需要
java.time中没有直接对应但非常方便的功能,如truncate和round(虽然java.time可通过其他方式实现,但DateUtils更简洁)。 - 需要解析多种格式的日期字符串(
parseDate的灵活性)。
- 在无法立即迁移到
- 与
java.time的协作: 可以通过Date.from(instant)和date.toInstant()在java.util.Date和java.time.Instant之间转换,再利用Instant与LocalDateTime/ZonedDateTime交互。
- 可变性:
五、 对比与最佳实践
| 特性 | java.time.YearMonth |
java.time.LocalDate |
org.apache.commons.lang3.time.DateUtils |
|---|---|---|---|
| 所属包 | java.time (Java 8+) |
java.time (Java 8+) |
org.apache.commons.lang3.time (第三方) |
| 核心用途 | 年月组合 | 日期 (年月日) | Date/Calendar 操作工具 |
| 可变性 | 不可变 | 不可变 | 可变 (修改原对象) |
| 线程安全 | 是 | 是 | 否 (因 Date/Calendar 不安全) |
| API 设计 | 清晰、直观 | 清晰、直观 | 功能性强,但设计较旧 |
| 推荐程度 | 高 (新项目) | 极高 (新项目) | 中低 (主要用于兼容或特定场景) |
| 依赖 | JDK 内置 | JDK 内置 | 需引入 commons-lang3 |
最佳实践建议:
- 拥抱
java.time: 新项目或重构时,强烈推荐使用java.time包 (YearMonth,LocalDate,LocalDateTime,ZonedDateTime,Instant) 作为首选。 - 明确需求: 根据业务需求选择合适的类。只需年月用
YearMonth,需要具体日期用LocalDate。 - 善用
DateUtils: 在必须处理java.util.Date的场景下,DateUtils的truncate,round,isSameDay,parseDate等方法非常实用。但要注意其可变性和线程安全问题。 - 避免混合使用: 尽量避免在同一个逻辑中频繁混用
java.time和java.util.Date,必要时通过Instant进行转换。 - 时区意识: 如果业务涉及不同时区,应使用
ZonedDateTime或OffsetDateTime,而非LocalDateTime。
六、 总结
YearMonth、LocalDate 和 DateUtils 各有其定位和优势。java.time 包的 YearMonth 和 LocalDate 以其现代化的设计、不可变性和清晰的 API,成为处理日期时间的黄金标准。而 DateUtils 作为经典的工具库,在处理遗留代码和提供特定便捷功能(尤其是日期截断和舍入)方面依然发挥着重要作用。
理解它们的核心概念、掌握关键方法、明确适用场景,并遵循最佳实践,你就能高效、准确地处理 Java 中的各种日期时间需求,写出更健壮、更易维护的代码。
七、 参考资料
- Oracle Java 8 Documentation: https://docs.oracle.com/javase/8/docs/api/java/time/package-summary.html
- Apache Commons Lang Documentation: https://commons.apache.org/proper/commons-lang/
- JSR-310: Date and Time API: https://jcp.org/en/jsr/detail?id=310
文章结尾:
希望本文能帮助你更好地理解和运用这些日期时间工具。你在项目中更倾向于使用哪个?或者遇到过哪些有趣的日期处理问题?欢迎在评论区分享交流!
注意: 本文示例代码均在 Java 8+ 环境下测试。使用
DateUtils时请确保正确引入commons-lang3依赖。
更多推荐



所有评论(0)