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:date
、xsd:time
、xsd:dateTime
等类型的关键桥梁。
**
GregorianCalendar
→XMLGregorianCalendar
**:@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注解的字段 }
**
XMLGregorianCalendar
→GregorianCalendar
**:@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