从ZonedDateTime创建GregorianCalendar时的行为不一致问题
问题:
GregorianCalendar.from(ZonedDateTime)与其他创建方式的行为差异 问题背景
在集成两段无法修改的遗留代码时,一段使用GregorianCalendar,另一段使用ZonedDateTime,通过GregorianCalendar.from(ZonedDateTime)创建的实例,与直接构造、时间戳解析等方式创建的GregorianCalendar行为不一致——比如调用set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY)时得到的结果不同,且WEEK_OF_YEAR值也存在差异。
演示代码
import java.util.*; import java.time.*; public class Main { public static void main(String[] args) { final GregorianCalendar calendarPure = new GregorianCalendar(TimeZone.getTimeZone("America/Los_Angeles")); printInfo(calendarPure); ZoneId zone = ZoneId.of("America/Los_Angeles"); final ZonedDateTime zdtNow = ZonedDateTime.now(zone); final GregorianCalendar calendarFromZdt = GregorianCalendar.from(zdtNow); printInfo(calendarFromZdt); final GregorianCalendar calendarFromZdtEpoch = new GregorianCalendar(); calendarFromZdtEpoch.setTimeInMillis(zdtNow.toInstant().toEpochMilli()); printInfo(calendarFromZdtEpoch); } private static void printInfo(GregorianCalendar cal) { System.out.printf("Now: %tc%n", cal); System.out.printf("Last Sunday: %tc%n", getLastSunday(cal)); System.out.printf("Week of Year: %d%n", cal.get(Calendar.WEEK_OF_YEAR)); System.out.printf("Time in Millis: %d%n", cal.getTimeInMillis()); System.out.printf("toString(): %s%n", cal); System.out.println(); } public static GregorianCalendar getLastSunday(GregorianCalendar referenceDate) { final GregorianCalendar lastSundayMidNight = (GregorianCalendar)referenceDate.clone(); lastSundayMidNight.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY); return lastSundayMidNight; } }
输出结果
Now: Wed Dec 28 16:45:36 PST 2022 Last Sunday: Sun Dec 25 16:45:36 PST 2022 Week of Year: 53 Time in Millis: 1672274736631 toString(): java.util.GregorianCalendar[time=1672274736631,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="America/Los_Angeles",offset=-28800000,dstSavings=3600000,useDaylight=true,transitions=185,lastRule=java.util.SimpleTimeZone[id=America/Los_Angeles,offset=-28800000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2022,MONTH=11,WEEK_OF_YEAR=53,WEEK_OF_MONTH=5,DAY_OF_MONTH=28,DAY_OF_YEAR=362,DAY_OF_WEEK=4,DAY_OF_WEEK_IN_MONTH=4,AM_PM=1,HOUR=4,HOUR_OF_DAY=16,MINUTE=45,SECOND=36,MILLISECOND=631,ZONE_OFFSET=-28800000,DST_OFFSET=0] Now: Wed Dec 28 16:45:36 PST 2022 Last Sunday: Sun Jan 01 16:45:36 PST 2023 Week of Year: 52 Time in Millis: 1672274736668 toString(): java.util.GregorianCalendar[time=1672274736668,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="America/Los_Angeles",offset=-28800000,dstSavings=3600000,useDaylight=true,transitions=185,lastRule=java.util.SimpleTimeZone[id=America/Los_Angeles,offset=-28800000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]],firstDayOfWeek=2,minimalDaysInFirstWeek=4,ERA=1,YEAR=2022,MONTH=11,WEEK_OF_YEAR=52,WEEK_OF_MONTH=5,DAY_OF_MONTH=28,DAY_OF_YEAR=362,DAY_OF_WEEK=4,DAY_OF_WEEK_IN_MONTH=4,AM_PM=1,HOUR=4,HOUR_OF_DAY=16,MINUTE=45,SECOND=36,MILLISECOND=668,ZONE_OFFSET=-28800000,DST_OFFSET=0] Now: Thu Dec 29 00:45:36 UTC 2022 Last Sunday: Sun Dec 25 00:45:36 UTC 2022 Week of Year: 53 Time in Millis: 1672274736668 toString(): java.util.GregorianCalendar[time=1672274736668,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="Etc/UTC",offset=0,dstSavings=0,useDaylight=false,transitions=0,lastRule=null],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2022,MONTH=11,WEEK_OF_YEAR=53,WEEK_OF_MONTH=5,DAY_OF_MONTH=29,DAY_OF_YEAR=363,DAY_OF_WEEK=5,DAY_OF_WEEK_IN_MONTH=5,AM_PM=0,HOUR=0,HOUR_OF_DAY=0,MINUTE=45,SECOND=36,MILLISECOND=668,ZONE_OFFSET=0,DST_OFFSET=0]
疑问
通过toString()输出可见,firstDayOfWeek和minimalDaysInFirstWeek字段值存在差异,推测这是行为不一致的原因。想知道:
- 这两个字段值的来源是什么?
- 是否可以通过设置
ZonedDateTime的属性,让GregorianCalendar.from(ZonedDateTime)创建的实例与其他方式创建的行为一致?
解答
1. firstDayOfWeek和minimalDaysInFirstWeek的来源
这两个字段是**区域设置(Locale)**相关的核心属性:
firstDayOfWeek:定义一周的起始日,例如美国Locale默认是周日(对应Calendar.SUNDAY,值为1),欧洲多数Locale默认是周一(对应Calendar.MONDAY,值为2)minimalDaysInFirstWeek:定义一年中第一周需要包含的最少天数,例如ISO 8601标准要求第一周至少包含4天(值为4),而美国标准是至少1天(值为1)
不同创建方式的差异根源:
- 直接
new GregorianCalendar(TimeZone):默认使用系统默认Locale的周规则 GregorianCalendar.from(ZonedDateTime):内部遵循java.time体系的默认规范(ISO 8601),因此会设置firstDayOfWeek=2、minimalDaysInFirstWeek=4- 通过时间戳
setTimeInMillis创建的实例:同样继承系统默认Locale的周规则
2. 让from(ZonedDateTime)创建的实例行为一致的方法
无法通过修改ZonedDateTime的属性实现,因为ZonedDateTime本身不携带Locale相关的周规则信息。但可以在创建GregorianCalendar后,手动覆盖这两个字段的值,对齐到目标规则:
方法一:对齐到系统默认Locale的规则
ZoneId zone = ZoneId.of("America/Los_Angeles"); ZonedDateTime zdtNow = ZonedDateTime.now(zone); GregorianCalendar calendarFromZdt = GregorianCalendar.from(zdtNow); // 从默认构造的GregorianCalendar中复制周规则 GregorianCalendar template = new GregorianCalendar(); calendarFromZdt.setFirstDayOfWeek(template.getFirstDayOfWeek()); calendarFromZdt.setMinimalDaysInFirstWeek(template.getMinimalDaysInFirstWeek());
方法二:手动设置指定规则(比如美国标准)
GregorianCalendar calendarFromZdt = GregorianCalendar.from(zdtNow); // 设置一周起始日为周日,第一周至少1天 calendarFromZdt.setFirstDayOfWeek(Calendar.SUNDAY); calendarFromZdt.setMinimalDaysInFirstWeek(1);
修改后,calendarFromZdt的set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY)行为和calendarPure会保持一致,WEEK_OF_YEAR值也会对齐。
内容的提问来源于stack exchange,提问作者L. Holanda
相关产品推荐
相关产品推荐

