一、 引言:告别 Date 的混乱

  • 痛点回顾: 简要回顾 java.util.Date 和 java.util.Calendar 的痛点(如可变性、线程不安全、API 设计不直观、时区处理复杂等)。
  • JSR-310 的曙光: 介绍 Java 8 引入的 java.time 包(JSR-310),强调其不可变性、线程安全、清晰的 API 设计和强大的功能。
  • 本文目标: 聚焦 java.time.YearMonthjava.time.LocalDate 和 org.apache.commons.lang3.time.DateUtils 这三个在实际开发中极为常用的工具/类,进行详细讲解和对比。

二、 java.time.YearMonth:精准掌控年月

YearMonth 类专门用于表示一个特定的年份和月份,不包含日、时、分、秒和时区信息。非常适合处理账单周期、统计报表、有效期等场景。

  1. 核心概念:

    • 表示一个年份和月份的组合,例如 "2023-10"。
    • 不可变且线程安全。
    • 常用静态工厂方法创建。
  2. 核心方法详解与示例:

    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);
        }
    }
  3. 适用场景:

    • 信用卡有效期。
    • 账单周期(如“2023年10月账单”)。
    • 按月统计的数据(如销售月报)。
    • 有效期检查(如“2024-06”是否过期)。

三、 java.time.LocalDate:处理“日”级别的利器

LocalDate 表示一个不带时区的日期,包含年、月、日信息。是处理日期逻辑最常用的类。

  1. 核心概念:

    • 表示一个日期,如 "2025-08-22"。
    • 不可变且线程安全。
    • 不包含时间或时区。
  2. 核心方法详解与示例:

    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();
        }
    }
  3. 适用场景:

    • 用户生日、纪念日。
    • 事件日期(如会议、截止日期)。
    • 日报、考勤记录。
    • 任何需要精确到“日”的日期逻辑。

四、 org.apache.commons.lang3.time.DateUtils:经典工具类的现代化应用

虽然 java.time 是首选,但 DateUtils 在处理遗留的 java.util.DateCalendar 时,以及提供一些便捷操作方面仍有价值。重点强调:新项目应优先使用 java.timeDateUtils 主要用于兼容旧代码或特定便捷方法。

  1. 核心价值:

    • 简化 Date 和 Calendar 的操作。
    • 提供 java.time 之前缺失的便捷功能(如日期截断、舍入)。
    • 与 java.time 交互的桥梁(部分方法)。
  2. 核心方法详解与示例 (需引入 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));
        }
    }
  3. 重要注意事项:

    • 可变性: 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

最佳实践建议:

  1. 拥抱 java.time 新项目或重构时,强烈推荐使用 java.time 包 (YearMonthLocalDateLocalDateTimeZonedDateTimeInstant) 作为首选。
  2. 明确需求: 根据业务需求选择合适的类。只需年月用 YearMonth,需要具体日期用 LocalDate
  3. 善用 DateUtils 在必须处理 java.util.Date 的场景下,DateUtils 的 truncateroundisSameDayparseDate 等方法非常实用。但要注意其可变性和线程安全问题。
  4. 避免混合使用: 尽量避免在同一个逻辑中频繁混用 java.time 和 java.util.Date,必要时通过 Instant 进行转换。
  5. 时区意识: 如果业务涉及不同时区,应使用 ZonedDateTime 或 OffsetDateTime,而非 LocalDateTime

六、 总结

YearMonthLocalDateDateUtils 各有其定位和优势。java.time 包的 YearMonthLocalDate 以其现代化的设计、不可变性和清晰的 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 依赖。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐