1. 简介

本文将带你快速掌握 java.util.GregorianCalendar 的核心用法。作为 Java 日期处理的老将,虽然 Java 8 引入了更现代的 java.time 包,但在维护旧项目或对接遗留系统时,GregorianCalendar 依然是绕不开的知识点。掌握它,能帮你少踩不少坑。

2. GregorianCalendar 详解

GregorianCalendar 是抽象类 java.util.Calendar 的一个具体实现。顾名思义,它基于公历(格里高利历),这也是目前全球最广泛使用的民用日历。

2.1. 获取实例

创建 GregorianCalendar 实例有两种方式:使用 Calendar.getInstance() 工厂方法,或直接调用其构造函数。

⚠️ **强烈不推荐使用 Calendar.getInstance()**。因为它会根据 JVM 的默认 Locale 返回不同类型的 Calendar 实例。比如在泰国可能返回 BuddhistCalendar,在日本可能返回 JapaneseImperialCalendar。如果你盲目强转为 GregorianCalendar,就会引发 ClassCastException

@Test(expected = ClassCastException.class)
public void test_Class_Cast_Exception() {
    TimeZone tz = TimeZone.getTimeZone("GMT+9:00");
    Locale loc = new Locale("ja", "JP", "JP");
    Calendar calendar = Calendar.getInstance(loc);
    GregorianCalendar gc = (GregorianCalendar) calendar; // 踩坑点:强转失败
}

推荐使用构造函数,可以精确控制时区和区域设置。GregorianCalendar 提供了 7 个重载构造函数,常用的有以下几种:

  • 默认构造函数:使用操作系统默认的时区和 Locale 初始化为当前时间。

    new GregorianCalendar();
    
  • 指定年月日时分秒:使用默认时区和 Locale

    new GregorianCalendar(2018, 6, 27, 16, 16, 47);
    

    注意:月份从 0 开始(0=1月,11=12月),这是个经典坑点。

  • 指定时区:使用默认 Locale

    new GregorianCalendar(TimeZone.getTimeZone("GMT+5:30"));
    
  • **指定区域 (Locale)**:使用默认时区。

    new GregorianCalendar(new Locale("en", "IN"));
    
  • 同时指定时区和区域

    new GregorianCalendar(TimeZone.getTimeZone("GMT+5:30"), new Locale("en", "IN"));
    

2.2. Java 8 新增方法

Java 8 为 GregorianCalendar 添加了与 java.time 包互操作的方法,让新旧 API 之间可以平滑过渡。

  • **from(ZonedDateTime zdt)**:静态方法,从一个 ZonedDateTime 对象创建 GregorianCalendar 实例。
  • **getCalendarType()**:获取日历类型,返回字符串如 'gregory''buddhist''japanese'。可用于校验实例类型。
    @Test
    public void test_Calendar_Return_Type_Valid() {
        Calendar calendar = Calendar.getInstance();
        assert ("gregory".equals(calendar.getCalendarType())); // 确保是公历
    }
    
  • **toZonedDateTime()**:将 GregorianCalendar 转换为 ZonedDateTime,表示时间线上的同一个时刻。这是与现代时间 API 交互的关键方法。
    ZonedDateTime zdt = gregorianCalendar.toZonedDateTime();
    

2.3. 日期修改操作

GregorianCalendar 提供了三种修改日期的核心方法:add()roll()set()

  • **add(int field, int amount)**:

    • 在指定字段上增加/减少指定量。
    • 会触发进位或借位,影响更高层级的字段(如加一天导致月份变更)。
    • 执行后立即重新计算所有字段
      @Test
      public void test_whenAddOneDay_thenMonthIsChanged() {
        GregorianCalendar calendar = new GregorianCalendar(2018, 5, 30); // 2018-06-30
        calendar.add(Calendar.DATE, 1); // 加1天
        // 结果是 2018-07-01,月份从5(6月)变为6(7月)
        assertEquals(1, calendar.get(Calendar.DATE));
        assertEquals(6, calendar.get(Calendar.MONTH));
      }
      
      减法也很简单,传负数即可:
      calendar.add(Calendar.DATE, -1); // 减1天
      
  • **roll(int field, int amount)**:

    • 在指定字段上滚动(增加/减少),但不会影响更高层级的字段
    • 比如对月份 roll,年份不会变。
      @Test
      public void test_whenRollUpOneMonth_thenYearIsUnchanged() {
        GregorianCalendar calendar = new GregorianCalendar(2018, 6, 28); // 2018-07-28
        calendar.roll(Calendar.MONTH, 1); // 月份+1
        // 结果是 2018-08-28,年份仍是2018
        assertEquals(7, calendar.get(Calendar.MONTH)); // 8月
        assertEquals(2018, calendar.get(Calendar.YEAR));
      }
      
      同样,传负数可实现“滚动向下”。
  • **set(int field, int value)**:

    • 直接设置指定字段的值。
    • 关键点:set 操作是延迟计算的。它不会立即重新计算整个日历,直到你调用 get()getTime()add()roll() 时才会触发。
    • 这个特性允许你连续调用多个 set 而不产生性能开销。
      @Test
      public void test_setMonth() {
        GregorianCalendar calendar = new GregorianCalendar(2018, 6, 28); // 2018-07-28
        calendar.set(Calendar.MONTH, 3); // 设置为4月
        // 此时内部状态可能还未完全更新,但下次获取时会正确计算
        assertEquals(3, calendar.get(Calendar.MONTH)); // 最终获取时是正确的
      }
      

2.4. 与 XMLGregorianCalendar 交互

在使用 JAXB 进行 XML 序列化时,javax.xml.datatype.XMLGregorianCalendar 是处理 xsd:datexsd:timexsd:dateTime 等类型的关键桥梁。

  • **GregorianCalendarXMLGregorianCalendar**:

    @Test
    public void test_toXMLGregorianCalendar() throws Exception {
        DatatypeFactory factory = DatatypeFactory.newInstance();
        GregorianCalendar gcal = new GregorianCalendar(2018, 6, 28);
        XMLGregorianCalendar xmlGcal = factory.newXMLGregorianCalendar(gcal);
        // xmlGcal 可用于JAXB注解的字段
    }
    
  • **XMLGregorianCalendarGregorianCalendar**:

    @Test
    public void test_toDate() throws DatatypeConfigurationException {
        DatatypeFactory factory = DatatypeFactory.newInstance();
        GregorianCalendar gcal = new GregorianCalendar(2018, 6, 28);
        XMLGregorianCalendar xmlGcal = factory.newXMLGregorianCalendar(gcal);
        
        GregorianCalendar result = xmlGcal.toGregorianCalendar();
        assertEquals(gcal.getTime(), result.getTime());
    }
    

2.5. 日期比较

使用 compareTo() 方法比较两个 GregorianCalendar 实例:

  • 返回 1:当前对象时间在参数对象之后(更大)。
  • 返回 -1:当前对象时间在参数对象之前(更小)。
  • 返回 0:两个时间相等。
@Test
public void test_Compare_Date_FirstDate_Greater_SecondDate() {
    GregorianCalendar first = new GregorianCalendar(2018, 6, 28); // 7月
    GregorianCalendar second = new GregorianCalendar(2018, 5, 28); // 6月
    assertTrue(first.compareTo(second) > 0); // 7月 > 6月
}

@Test
public void test_Compare_Date_Both_Dates_Equal() {
    GregorianCalendar first = new GregorianCalendar(2018, 6, 28);
    GregorianCalendar second = new GregorianCalendar(2018, 6, 28);
    assertTrue(first.compareTo(second) == 0);
}

2.6. 日期格式化

GregorianCalendar 本身没有强大的格式化能力。推荐做法是先转换为 ZonedDateTime,再用 DateTimeFormatter 进行格式化。

@Test
public void test_dateFormatdMMMuuuu() {
    String formatted = new GregorianCalendar(2018, 6, 28)
        .toZonedDateTime()
        .format(DateTimeFormatter.ofPattern("d MMM uuuu"));
    assertEquals("28 Jul 2018", formatted);
}

2.7. 获取日历信息

GregorianCalendar 提供了一系列 get 方法来查询日历的边界和属性:

  • getActualMaximum(field):考虑当前年/月,返回该字段的实际最大值。如 DAY_OF_MONTH 在6月返回30。

    assertTrue(30 == calendar.getActualMaximum(Calendar.DAY_OF_MONTH));
    
  • getActualMinimum(field):考虑当前年/月,返回该字段的实际最小值。通常为1。

    assertTrue(1 == calendar.getActualMinimum(Calendar.DAY_OF_MONTH));
    
  • getGreatestMinimum(field):返回该字段在整个日历系统中可能的最高最小值。对于 DAY_OF_MONTH 是1。

  • getLeastMaximum(field):返回该字段在整个日历系统中可能的最低最大值。对于 DAY_OF_MONTH 是28(2月最少28天)。

  • getMaximum(field):返回该字段可能的最大值。对于 DAY_OF_MONTH 是31。

  • getMinimum(field):返回该字段可能的最小值。对于 DAY_OF_MONTH 是1。

  • getWeekYear():返回该日期所在周的年份(周日历年)。

  • getWeeksInWeekYear():返回该周日历年包含的周数。

  • isLeapYear(int year):判断指定年份是否为闰年。

    assertFalse(calendar.isLeapYear(2018)); // 2018不是闰年
    

3. 总结

本文系统梳理了 GregorianCalendar 的核心用法,包括实例化、日期操作、与现代API互操作、序列化及信息查询。虽然在新项目中应优先使用 java.time 包,但理解 GregorianCalendar 对于维护和升级现有系统至关重要。

文中所有示例代码均可在 GitHub 上找到:https://github.com/baeldung/core-java-modules/tree/master/core-java-date-operations-1


原始标题:Guide to java.util.GregorianCalendar | Baeldung