Java 日历 clear() 更改夏令时
Java Calendar clear() changes DST
首先,我想声明我知道 Java Calendar class 正在被其他可以说更好的库所取代。也许我偶然发现了日历失宠的原因之一。
我 运行 对日历中令人沮丧的行为感到沮丧,因为它涉及夏令时结束时的重叠时间。
public void annoying_issue()
{
Calendar midnightPDT = Calendar.getInstance(TimeZone.getTimeZone("US/Pacific"));
midnightPDT.set(Calendar.YEAR, 2021);
midnightPDT.set(Calendar.MONTH, 10);
midnightPDT.set(Calendar.DAY_OF_MONTH, 7);
midnightPDT.set(Calendar.HOUR_OF_DAY, 0);
midnightPDT.set(Calendar.MINUTE, 0);
midnightPDT.set(Calendar.SECOND, 0);
midnightPDT.set(Calendar.MILLISECOND, 0);
Calendar oneAMPDT = Calendar.getInstance(TimeZone.getTimeZone("US/Pacific"));
oneAMPDT.setTimeInMillis(midnightPDT.getTimeInMillis() + (60*60*1000));//this is the easiest way I've found to get to the first 1am hour at DST overlap
System.out.println(new Date(midnightPDT.getTimeInMillis()));//prints the expected "Sun Nov 7 00:00:00 PDT 2021"
System.out.println(new Date(oneAMPDT.getTimeInMillis()));//prints "Sun Nov 7 01:00:00 PDT 2021" also expected
oneAMPDT.clear(Calendar.MINUTE);//minute is already 0 so no change should occur... RIGHT!?
//WRONG!!!!
//The time is now in PST! The millisecond value has increased by 3600000, too!!
System.out.println(new Date(oneAMPDT.getTimeInMillis()));//prints "Sun Nov 7 01:00:00 PST 2021"
}
根据评论,您会发现清除日历中的 MINUTE 字段实际上将它提前了一个小时!见鬼!?
当我使用oneAMPDT.set(Calendar.MINUTE, 0)
时也会出现这种情况
这是预期的行为吗?有什么办法可以避免这种情况吗?
避免遗留日期时间classes;需要时转换
如您所述,Calendar
几年前被 JSR 310 中定义的 java.time classes 取代(一致采用)。正如您所注意到的,有 许多 原因可以避免使用 Calendar
和 Date
等
如果您必须有一个 Calendar
对象来与尚未更新到 java.time 的旧代码进行互操作,请在完成 [=104] 中的工作后进行转换=]java.time.
java.time
指定您想要的时区。请注意 US/Pacific
只是实际时区的别名,America/Los_Angeles
.
ZoneId zLosAngeles = ZoneId.of( "America/Los_Angeles" ) ;
指定您想要的时刻。
LocalDate ld = LocalDate.of( 2021 , Month.NOVEMBER , 7 ) ;
在您的代码中,您似乎假设一天的第一时刻发生在 00:00。情况并非总是如此。某些时区的某些日期可能会在另一个时间开始。所以让java.time确定一天的第一个时刻。
ZonedDateTime firstMomentOfThe7thInLosAngeles = ld.atStartOfDay( zLosAngeles ) ;
firstMomentOfThe7thInLosAngeles.toString(): 2021-11-07T00:00-07:00[America/Los_Angeles]
然后你跳到另一个时刻,到凌晨 1 点。
ZonedDateTime oneAmOnThe7thLosAngeles = firstMomentOfThe7thInLosAngeles.with( LocalTime.of( 1 , 0 ) ) ;
oneAmOnThe7thLosAngeles.toString(): 2021-11-07T01:00-07:00[America/Los_Angeles]
那个时间在那个日期可能存在也可能不存在。 ZonedDateTime
class 将根据需要进行调整。
您为变量使用了名称 midnightPDT
。我建议避免使用术语 midnight
,因为它的使用会混淆日期时间处理而没有精确的定义。如果您是这个意思,我建议您使用术语“一天中的第一刻”。
您提取自 1970 年第一时刻的纪元参考以来的毫秒数,如 UTC,1970-01-01T00:00Z.
Instant firstMomentOfThe7thInLosAngelesAsSeenInUtc = firstMomentOfThe7thInLosAngeles.toInstant() ;
long millisSinceEpoch_FirstMomentOf7thLosAngeles = firstMomentOfThe7thInLosAngelesAsSeenInUtc.toEpochMilli() ;
firstMomentOfThe7thInLosAngelesAsSeenInUtc.toString(): 2021-11-07T07:00:00Z
millisSinceEpoch_FirstMomentOf7thLosAngeles = 1636268400000
你也为我们的凌晨 1 点做同样的事情。
Instant oneAmOnThe7thLosAngelesAsSeenInUtc = oneAmOnThe7thLosAngeles.toInstant() ;
long millisSinceEpoch_OneAmOn7thLosAngeles = oneAmOnThe7thLosAngelesAsSeenInUtc.toEpochMilli() ;
oneAmOnThe7thLosAngelesAsSeenInUtc.toString(): 2021-11-07T08:00:00Z
millisSinceEpoch_OneAmOn7thLosAngeles = 1636272000000
我们应该看到一小时的差异。一个小时 = 3,600,000 = 60 * 60 * 1,000.
long diff = ( millisSinceEpoch_OneAmOn7thLosAngeles - millisSinceEpoch_FirstMomentOf7thLosAngeles ); // 3,600,000 = 60 * 60 * 1,000.
diff = 3600000
转换
然后你继续提到 Daylight Saving Time (DST) 切换。那天美国夏令时的切换时间是凌晨 2 点,而不是凌晨 1 点。在凌晨 2 点到来的那一刻,时钟摆回凌晨 1 点,持续了第二个 1:00-2:00 AM 小时。
为了达到那个切换点,让我们加一个小时。
ZonedDateTime cutover_Addition = oneAmOnThe7thLosAngeles.plusHours( 1 );
cutover_Addition = 2021-11-07T01:00-08:00[America/Los_Angeles]
请注意,一天中的时间显示相同(凌晨 1 点),但与 UTC 的偏移量已从比 UTC 晚 7 小时更改为现在比 UTC 晚 8 小时。这就是你要找的时差。
让我们计算第三时刻自纪元以来的毫秒数。在我们有一天的第一时刻 (00:00) 之前,第一个发生在凌晨 1 点,现在我们有第二个发生在 2021 年 11 月 7 日这个“回退”日期的凌晨 1 点。
long millisSinceEpoch_Cutover = cutover_Addition.toInstant().toEpochMilli();
1636275600000
Duration.between( firstMomentOfThe7thInLosAngelesAsSeenInUtc , cutover_Addition.toInstant() ) = PT2H
Duration.between( oneAmOnThe7thLosAngelesAsSeenInUtc , cutover_Addition.toInstant() ) = PT1H
ZonedDateTime
class 确实提供了在这些切换时刻的一对使用方法: withEarlierOffsetAtOverlap
和 withLaterOffsetAtOverlap
.
ZonedDateTime cutover_OverlapEarlier =
cutover_Addition
.withEarlierOffsetAtOverlap();
ZonedDateTime cutover_OverlapLater =
cutover_Addition
.withLaterOffsetAtOverlap();
cutover_OverlapEarlier = 2021-11-07T01:00-07:00[America/Los_Angeles]
cutover_OverlapLater = 2021-11-07T01:00-08:00[America/Los_Angeles]
Calendar
如果您确实需要一个 Calendar
对象,只需转换即可。
Calendar x = GregorianCalendar.from( firstMomentOfThe7thInLosAngeles ) ;
Calendar y = GregorianCalendar.from( oneAmOnThe7thLosAngeles ) ;
Calendar z = GregorianCalendar.from( cutover_Addition );
如果您的目标只是努力理解 Calendar
class 行为,我建议您停止自虐。无关紧要。 Sun、Oracle 和 JCP 社区 all gave up 对那些可怕的遗留日期时间 classes。我建议你也这样做。
示例代码
将上面的所有代码放在一起。
ZoneId zLosAngeles = ZoneId.of( "America/Los_Angeles" );
LocalDate ld = LocalDate.of( 2021 , Month.NOVEMBER , 7 );
ZonedDateTime firstMomentOfThe7thInLosAngeles = ld.atStartOfDay( zLosAngeles );
ZonedDateTime oneAmOnThe7thLosAngeles = firstMomentOfThe7thInLosAngeles.with( LocalTime.of( 1 , 0 ) );
Instant firstMomentOfThe7thInLosAngelesAsSeenInUtc = firstMomentOfThe7thInLosAngeles.toInstant();
long millisSinceEpoch_FirstMomentOf7thLosAngeles = firstMomentOfThe7thInLosAngelesAsSeenInUtc.toEpochMilli();
Instant oneAmOnThe7thLosAngelesAsSeenInUtc = oneAmOnThe7thLosAngeles.toInstant();
long millisSinceEpoch_OneAmOn7thLosAngeles = oneAmOnThe7thLosAngelesAsSeenInUtc.toEpochMilli();
long diff = ( millisSinceEpoch_OneAmOn7thLosAngeles - millisSinceEpoch_FirstMomentOf7thLosAngeles ); // 3,600,000 = 60 * 60 * 1,000.
ZonedDateTime cutover_Addition = oneAmOnThe7thLosAngeles.plusHours( 1 );
long millisSinceEpoch_Cutover = cutover_Addition.toInstant().toEpochMilli();
ZonedDateTime cutover_OverlapEarlier =
cutover_Addition
.withEarlierOffsetAtOverlap();
ZonedDateTime cutover_OverlapLater =
cutover_Addition
.withLaterOffsetAtOverlap();
转换为旧版 classes,如果需要的话。
Calendar x = GregorianCalendar.from( firstMomentOfThe7thInLosAngeles );
Calendar y = GregorianCalendar.from( oneAmOnThe7thLosAngeles );
Calendar z = GregorianCalendar.from( cutover_Addition );
转储到控制台。
System.out.println( "firstMomentOfThe7thInLosAngeles = " + firstMomentOfThe7thInLosAngeles );
System.out.println( "oneAmOnThe7thLosAngeles = " + oneAmOnThe7thLosAngeles );
System.out.println( "firstMomentOfThe7thInLosAngelesAsSeenInUtc = " + firstMomentOfThe7thInLosAngelesAsSeenInUtc );
System.out.println( "millisSinceEpoch_FirstMomentOf7thLosAngeles = " + millisSinceEpoch_FirstMomentOf7thLosAngeles );
System.out.println( "oneAmOnThe7thLosAngelesAsSeenInUtc = " + oneAmOnThe7thLosAngelesAsSeenInUtc );
System.out.println( "millisSinceEpoch_OneAmOn7thLosAngeles = " + millisSinceEpoch_OneAmOn7thLosAngeles );
System.out.println( "diff = " + diff );
System.out.println( "x = " + x );
System.out.println( "y = " + y );
System.out.println( "z = " + z );
System.out.println( "cutover_Addition = " + cutover_Addition );
System.out.println( "millisSinceEpoch_Cutover = " + millisSinceEpoch_Cutover );
System.out.println( "Duration.between( firstMomentOfThe7thInLosAngelesAsSeenInUtc , cutover_Addition.toInstant() ) = " + Duration.between( firstMomentOfThe7thInLosAngelesAsSeenInUtc , cutover_Addition.toInstant() ) );
System.out.println( "Duration.between( oneAmOnThe7thLosAngelesAsSeenInUtc , cutover_Addition.toInstant() ) = " + Duration.between( oneAmOnThe7thLosAngelesAsSeenInUtc , cutover_Addition.toInstant() ) );
System.out.println( "cutover_OverlapEarlier = " + cutover_OverlapEarlier );
System.out.println( "cutover_OverlapLater = " + cutover_OverlapLater );
当运行.
firstMomentOfThe7thInLosAngeles = 2021-11-07T00:00-07:00[America/Los_Angeles]
oneAmOnThe7thLosAngeles = 2021-11-07T01:00-07:00[America/Los_Angeles]
firstMomentOfThe7thInLosAngelesAsSeenInUtc = 2021-11-07T07:00:00Z
millisSinceEpoch_FirstMomentOf7thLosAngeles = 1636268400000
oneAmOnThe7thLosAngelesAsSeenInUtc = 2021-11-07T08:00:00Z
millisSinceEpoch_OneAmOn7thLosAngeles = 1636272000000
diff = 3600000
x = java.util.GregorianCalendar[time=1636268400000,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=2021,MONTH=10,WEEK_OF_YEAR=44,WEEK_OF_MONTH=1,DAY_OF_MONTH=7,DAY_OF_YEAR=311,DAY_OF_WEEK=1,DAY_OF_WEEK_IN_MONTH=1,AM_PM=0,HOUR=0,HOUR_OF_DAY=0,MINUTE=0,SECOND=0,MILLISECOND=0,ZONE_OFFSET=-28800000,DST_OFFSET=3600000]
y = java.util.GregorianCalendar[time=1636272000000,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=2021,MONTH=10,WEEK_OF_YEAR=44,WEEK_OF_MONTH=1,DAY_OF_MONTH=7,DAY_OF_YEAR=311,DAY_OF_WEEK=1,DAY_OF_WEEK_IN_MONTH=1,AM_PM=0,HOUR=1,HOUR_OF_DAY=1,MINUTE=0,SECOND=0,MILLISECOND=0,ZONE_OFFSET=-28800000,DST_OFFSET=3600000]
z = java.util.GregorianCalendar[time=1636275600000,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=2021,MONTH=10,WEEK_OF_YEAR=44,WEEK_OF_MONTH=1,DAY_OF_MONTH=7,DAY_OF_YEAR=311,DAY_OF_WEEK=1,DAY_OF_WEEK_IN_MONTH=1,AM_PM=0,HOUR=1,HOUR_OF_DAY=1,MINUTE=0,SECOND=0,MILLISECOND=0,ZONE_OFFSET=-28800000,DST_OFFSET=0]
cutover_Addition = 2021-11-07T01:00-08:00[America/Los_Angeles]
millisSinceEpoch_Cutover = 1636275600000
Duration.between( firstMomentOfThe7thInLosAngelesAsSeenInUtc , cutover_Addition.toInstant() ) = PT2H
Duration.between( oneAmOnThe7thLosAngelesAsSeenInUtc , cutover_Addition.toInstant() ) = PT1H
cutover_OverlapEarlier = 2021-11-07T01:00-07:00[America/Los_Angeles]
cutover_OverlapLater = 2021-11-07T01:00-08:00[America/Los_Angeles]
java.time
这是预期的行为吗?不是。我认为这是一个错误。
有没有办法防止这种情况? 是的,您已经提到或至少暗示的方式:使用 ZonedDateTime
而不是 Calendar
。 Basil Bourque 已经说过了。作为适度的补充,我想展示从 Calendar
到 ZonedDateTime
的完整往返,将分钟设置为 0 并转换回 Calendar
。如果您需要它来与遗留代码进行互操作。
GregorianCalendar oneAmPdt = new GregorianCalendar(TimeZone.getTimeZone(ZoneId.of("America/Los_Angeles")));
oneAmPdt.clear();
oneAmPdt.set(2021, Calendar.NOVEMBER, 7, 0, 0);
oneAmPdt.add(Calendar.HOUR_OF_DAY, 1);
System.out.println(oneAmPdt.getTime());
ZonedDateTime zdt = oneAmPdt.toZonedDateTime();
// Minute is already 0 so no change should occur... RIGHT!?
zdt = zdt.withMinute(0);
oneAmPdt = GregorianCalendar.from(zdt);
System.out.println(oneAmPdt.getTime());
输出:
Sun Nov 07 01:00:00 PDT 2021
Sun Nov 07 01:00:00 PDT 2021
但是我用的是GregorianCalendar
,不是Calendar
?你也是。 GregorianCalendar
是您从 Calendar.getIntance()
得到的 Calendar
的子 class。在某些环境中,你会得到一个不同的 subclass 来反映那里使用的日历系统,而你对 set
的初始调用不会给你预期的结果。在这种情况下,您 想要 GregorianCalendar
(如果您不能从一开始就拥有 ZonedDateTime
)。
在修改我们的旧代码时,即使不是为了规避旧 Calendar
或 GregorianCalendar
class 中的错误,我也可能会按照上述方式进行操作。这是 运行 向 java.time 过渡的一小步。
首先,我想声明我知道 Java Calendar class 正在被其他可以说更好的库所取代。也许我偶然发现了日历失宠的原因之一。
我 运行 对日历中令人沮丧的行为感到沮丧,因为它涉及夏令时结束时的重叠时间。
public void annoying_issue()
{
Calendar midnightPDT = Calendar.getInstance(TimeZone.getTimeZone("US/Pacific"));
midnightPDT.set(Calendar.YEAR, 2021);
midnightPDT.set(Calendar.MONTH, 10);
midnightPDT.set(Calendar.DAY_OF_MONTH, 7);
midnightPDT.set(Calendar.HOUR_OF_DAY, 0);
midnightPDT.set(Calendar.MINUTE, 0);
midnightPDT.set(Calendar.SECOND, 0);
midnightPDT.set(Calendar.MILLISECOND, 0);
Calendar oneAMPDT = Calendar.getInstance(TimeZone.getTimeZone("US/Pacific"));
oneAMPDT.setTimeInMillis(midnightPDT.getTimeInMillis() + (60*60*1000));//this is the easiest way I've found to get to the first 1am hour at DST overlap
System.out.println(new Date(midnightPDT.getTimeInMillis()));//prints the expected "Sun Nov 7 00:00:00 PDT 2021"
System.out.println(new Date(oneAMPDT.getTimeInMillis()));//prints "Sun Nov 7 01:00:00 PDT 2021" also expected
oneAMPDT.clear(Calendar.MINUTE);//minute is already 0 so no change should occur... RIGHT!?
//WRONG!!!!
//The time is now in PST! The millisecond value has increased by 3600000, too!!
System.out.println(new Date(oneAMPDT.getTimeInMillis()));//prints "Sun Nov 7 01:00:00 PST 2021"
}
根据评论,您会发现清除日历中的 MINUTE 字段实际上将它提前了一个小时!见鬼!?
当我使用oneAMPDT.set(Calendar.MINUTE, 0)
这是预期的行为吗?有什么办法可以避免这种情况吗?
避免遗留日期时间classes;需要时转换
如您所述,Calendar
几年前被 JSR 310 中定义的 java.time classes 取代(一致采用)。正如您所注意到的,有 许多 原因可以避免使用 Calendar
和 Date
等
如果您必须有一个 Calendar
对象来与尚未更新到 java.time 的旧代码进行互操作,请在完成 [=104] 中的工作后进行转换=]java.time.
java.time
指定您想要的时区。请注意 US/Pacific
只是实际时区的别名,America/Los_Angeles
.
ZoneId zLosAngeles = ZoneId.of( "America/Los_Angeles" ) ;
指定您想要的时刻。
LocalDate ld = LocalDate.of( 2021 , Month.NOVEMBER , 7 ) ;
在您的代码中,您似乎假设一天的第一时刻发生在 00:00。情况并非总是如此。某些时区的某些日期可能会在另一个时间开始。所以让java.time确定一天的第一个时刻。
ZonedDateTime firstMomentOfThe7thInLosAngeles = ld.atStartOfDay( zLosAngeles ) ;
firstMomentOfThe7thInLosAngeles.toString(): 2021-11-07T00:00-07:00[America/Los_Angeles]
然后你跳到另一个时刻,到凌晨 1 点。
ZonedDateTime oneAmOnThe7thLosAngeles = firstMomentOfThe7thInLosAngeles.with( LocalTime.of( 1 , 0 ) ) ;
oneAmOnThe7thLosAngeles.toString(): 2021-11-07T01:00-07:00[America/Los_Angeles]
那个时间在那个日期可能存在也可能不存在。 ZonedDateTime
class 将根据需要进行调整。
您为变量使用了名称 midnightPDT
。我建议避免使用术语 midnight
,因为它的使用会混淆日期时间处理而没有精确的定义。如果您是这个意思,我建议您使用术语“一天中的第一刻”。
您提取自 1970 年第一时刻的纪元参考以来的毫秒数,如 UTC,1970-01-01T00:00Z.
Instant firstMomentOfThe7thInLosAngelesAsSeenInUtc = firstMomentOfThe7thInLosAngeles.toInstant() ;
long millisSinceEpoch_FirstMomentOf7thLosAngeles = firstMomentOfThe7thInLosAngelesAsSeenInUtc.toEpochMilli() ;
firstMomentOfThe7thInLosAngelesAsSeenInUtc.toString(): 2021-11-07T07:00:00Z
millisSinceEpoch_FirstMomentOf7thLosAngeles = 1636268400000
你也为我们的凌晨 1 点做同样的事情。
Instant oneAmOnThe7thLosAngelesAsSeenInUtc = oneAmOnThe7thLosAngeles.toInstant() ;
long millisSinceEpoch_OneAmOn7thLosAngeles = oneAmOnThe7thLosAngelesAsSeenInUtc.toEpochMilli() ;
oneAmOnThe7thLosAngelesAsSeenInUtc.toString(): 2021-11-07T08:00:00Z
millisSinceEpoch_OneAmOn7thLosAngeles = 1636272000000
我们应该看到一小时的差异。一个小时 = 3,600,000 = 60 * 60 * 1,000.
long diff = ( millisSinceEpoch_OneAmOn7thLosAngeles - millisSinceEpoch_FirstMomentOf7thLosAngeles ); // 3,600,000 = 60 * 60 * 1,000.
diff = 3600000
转换
然后你继续提到 Daylight Saving Time (DST) 切换。那天美国夏令时的切换时间是凌晨 2 点,而不是凌晨 1 点。在凌晨 2 点到来的那一刻,时钟摆回凌晨 1 点,持续了第二个 1:00-2:00 AM 小时。
为了达到那个切换点,让我们加一个小时。
ZonedDateTime cutover_Addition = oneAmOnThe7thLosAngeles.plusHours( 1 );
cutover_Addition = 2021-11-07T01:00-08:00[America/Los_Angeles]
请注意,一天中的时间显示相同(凌晨 1 点),但与 UTC 的偏移量已从比 UTC 晚 7 小时更改为现在比 UTC 晚 8 小时。这就是你要找的时差。
让我们计算第三时刻自纪元以来的毫秒数。在我们有一天的第一时刻 (00:00) 之前,第一个发生在凌晨 1 点,现在我们有第二个发生在 2021 年 11 月 7 日这个“回退”日期的凌晨 1 点。
long millisSinceEpoch_Cutover = cutover_Addition.toInstant().toEpochMilli();
1636275600000
Duration.between( firstMomentOfThe7thInLosAngelesAsSeenInUtc , cutover_Addition.toInstant() ) = PT2H
Duration.between( oneAmOnThe7thLosAngelesAsSeenInUtc , cutover_Addition.toInstant() ) = PT1H
ZonedDateTime
class 确实提供了在这些切换时刻的一对使用方法: withEarlierOffsetAtOverlap
和 withLaterOffsetAtOverlap
.
ZonedDateTime cutover_OverlapEarlier =
cutover_Addition
.withEarlierOffsetAtOverlap();
ZonedDateTime cutover_OverlapLater =
cutover_Addition
.withLaterOffsetAtOverlap();
cutover_OverlapEarlier = 2021-11-07T01:00-07:00[America/Los_Angeles]
cutover_OverlapLater = 2021-11-07T01:00-08:00[America/Los_Angeles]
Calendar
如果您确实需要一个 Calendar
对象,只需转换即可。
Calendar x = GregorianCalendar.from( firstMomentOfThe7thInLosAngeles ) ;
Calendar y = GregorianCalendar.from( oneAmOnThe7thLosAngeles ) ;
Calendar z = GregorianCalendar.from( cutover_Addition );
如果您的目标只是努力理解 Calendar
class 行为,我建议您停止自虐。无关紧要。 Sun、Oracle 和 JCP 社区 all gave up 对那些可怕的遗留日期时间 classes。我建议你也这样做。
示例代码
将上面的所有代码放在一起。
ZoneId zLosAngeles = ZoneId.of( "America/Los_Angeles" );
LocalDate ld = LocalDate.of( 2021 , Month.NOVEMBER , 7 );
ZonedDateTime firstMomentOfThe7thInLosAngeles = ld.atStartOfDay( zLosAngeles );
ZonedDateTime oneAmOnThe7thLosAngeles = firstMomentOfThe7thInLosAngeles.with( LocalTime.of( 1 , 0 ) );
Instant firstMomentOfThe7thInLosAngelesAsSeenInUtc = firstMomentOfThe7thInLosAngeles.toInstant();
long millisSinceEpoch_FirstMomentOf7thLosAngeles = firstMomentOfThe7thInLosAngelesAsSeenInUtc.toEpochMilli();
Instant oneAmOnThe7thLosAngelesAsSeenInUtc = oneAmOnThe7thLosAngeles.toInstant();
long millisSinceEpoch_OneAmOn7thLosAngeles = oneAmOnThe7thLosAngelesAsSeenInUtc.toEpochMilli();
long diff = ( millisSinceEpoch_OneAmOn7thLosAngeles - millisSinceEpoch_FirstMomentOf7thLosAngeles ); // 3,600,000 = 60 * 60 * 1,000.
ZonedDateTime cutover_Addition = oneAmOnThe7thLosAngeles.plusHours( 1 );
long millisSinceEpoch_Cutover = cutover_Addition.toInstant().toEpochMilli();
ZonedDateTime cutover_OverlapEarlier =
cutover_Addition
.withEarlierOffsetAtOverlap();
ZonedDateTime cutover_OverlapLater =
cutover_Addition
.withLaterOffsetAtOverlap();
转换为旧版 classes,如果需要的话。
Calendar x = GregorianCalendar.from( firstMomentOfThe7thInLosAngeles );
Calendar y = GregorianCalendar.from( oneAmOnThe7thLosAngeles );
Calendar z = GregorianCalendar.from( cutover_Addition );
转储到控制台。
System.out.println( "firstMomentOfThe7thInLosAngeles = " + firstMomentOfThe7thInLosAngeles );
System.out.println( "oneAmOnThe7thLosAngeles = " + oneAmOnThe7thLosAngeles );
System.out.println( "firstMomentOfThe7thInLosAngelesAsSeenInUtc = " + firstMomentOfThe7thInLosAngelesAsSeenInUtc );
System.out.println( "millisSinceEpoch_FirstMomentOf7thLosAngeles = " + millisSinceEpoch_FirstMomentOf7thLosAngeles );
System.out.println( "oneAmOnThe7thLosAngelesAsSeenInUtc = " + oneAmOnThe7thLosAngelesAsSeenInUtc );
System.out.println( "millisSinceEpoch_OneAmOn7thLosAngeles = " + millisSinceEpoch_OneAmOn7thLosAngeles );
System.out.println( "diff = " + diff );
System.out.println( "x = " + x );
System.out.println( "y = " + y );
System.out.println( "z = " + z );
System.out.println( "cutover_Addition = " + cutover_Addition );
System.out.println( "millisSinceEpoch_Cutover = " + millisSinceEpoch_Cutover );
System.out.println( "Duration.between( firstMomentOfThe7thInLosAngelesAsSeenInUtc , cutover_Addition.toInstant() ) = " + Duration.between( firstMomentOfThe7thInLosAngelesAsSeenInUtc , cutover_Addition.toInstant() ) );
System.out.println( "Duration.between( oneAmOnThe7thLosAngelesAsSeenInUtc , cutover_Addition.toInstant() ) = " + Duration.between( oneAmOnThe7thLosAngelesAsSeenInUtc , cutover_Addition.toInstant() ) );
System.out.println( "cutover_OverlapEarlier = " + cutover_OverlapEarlier );
System.out.println( "cutover_OverlapLater = " + cutover_OverlapLater );
当运行.
firstMomentOfThe7thInLosAngeles = 2021-11-07T00:00-07:00[America/Los_Angeles]
oneAmOnThe7thLosAngeles = 2021-11-07T01:00-07:00[America/Los_Angeles]
firstMomentOfThe7thInLosAngelesAsSeenInUtc = 2021-11-07T07:00:00Z
millisSinceEpoch_FirstMomentOf7thLosAngeles = 1636268400000
oneAmOnThe7thLosAngelesAsSeenInUtc = 2021-11-07T08:00:00Z
millisSinceEpoch_OneAmOn7thLosAngeles = 1636272000000
diff = 3600000
x = java.util.GregorianCalendar[time=1636268400000,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=2021,MONTH=10,WEEK_OF_YEAR=44,WEEK_OF_MONTH=1,DAY_OF_MONTH=7,DAY_OF_YEAR=311,DAY_OF_WEEK=1,DAY_OF_WEEK_IN_MONTH=1,AM_PM=0,HOUR=0,HOUR_OF_DAY=0,MINUTE=0,SECOND=0,MILLISECOND=0,ZONE_OFFSET=-28800000,DST_OFFSET=3600000]
y = java.util.GregorianCalendar[time=1636272000000,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=2021,MONTH=10,WEEK_OF_YEAR=44,WEEK_OF_MONTH=1,DAY_OF_MONTH=7,DAY_OF_YEAR=311,DAY_OF_WEEK=1,DAY_OF_WEEK_IN_MONTH=1,AM_PM=0,HOUR=1,HOUR_OF_DAY=1,MINUTE=0,SECOND=0,MILLISECOND=0,ZONE_OFFSET=-28800000,DST_OFFSET=3600000]
z = java.util.GregorianCalendar[time=1636275600000,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=2021,MONTH=10,WEEK_OF_YEAR=44,WEEK_OF_MONTH=1,DAY_OF_MONTH=7,DAY_OF_YEAR=311,DAY_OF_WEEK=1,DAY_OF_WEEK_IN_MONTH=1,AM_PM=0,HOUR=1,HOUR_OF_DAY=1,MINUTE=0,SECOND=0,MILLISECOND=0,ZONE_OFFSET=-28800000,DST_OFFSET=0]
cutover_Addition = 2021-11-07T01:00-08:00[America/Los_Angeles]
millisSinceEpoch_Cutover = 1636275600000
Duration.between( firstMomentOfThe7thInLosAngelesAsSeenInUtc , cutover_Addition.toInstant() ) = PT2H
Duration.between( oneAmOnThe7thLosAngelesAsSeenInUtc , cutover_Addition.toInstant() ) = PT1H
cutover_OverlapEarlier = 2021-11-07T01:00-07:00[America/Los_Angeles]
cutover_OverlapLater = 2021-11-07T01:00-08:00[America/Los_Angeles]
java.time
这是预期的行为吗?不是。我认为这是一个错误。
有没有办法防止这种情况? 是的,您已经提到或至少暗示的方式:使用 ZonedDateTime
而不是 Calendar
。 Basil Bourque 已经说过了。作为适度的补充,我想展示从 Calendar
到 ZonedDateTime
的完整往返,将分钟设置为 0 并转换回 Calendar
。如果您需要它来与遗留代码进行互操作。
GregorianCalendar oneAmPdt = new GregorianCalendar(TimeZone.getTimeZone(ZoneId.of("America/Los_Angeles")));
oneAmPdt.clear();
oneAmPdt.set(2021, Calendar.NOVEMBER, 7, 0, 0);
oneAmPdt.add(Calendar.HOUR_OF_DAY, 1);
System.out.println(oneAmPdt.getTime());
ZonedDateTime zdt = oneAmPdt.toZonedDateTime();
// Minute is already 0 so no change should occur... RIGHT!?
zdt = zdt.withMinute(0);
oneAmPdt = GregorianCalendar.from(zdt);
System.out.println(oneAmPdt.getTime());
输出:
Sun Nov 07 01:00:00 PDT 2021 Sun Nov 07 01:00:00 PDT 2021
但是我用的是GregorianCalendar
,不是Calendar
?你也是。 GregorianCalendar
是您从 Calendar.getIntance()
得到的 Calendar
的子 class。在某些环境中,你会得到一个不同的 subclass 来反映那里使用的日历系统,而你对 set
的初始调用不会给你预期的结果。在这种情况下,您 想要 GregorianCalendar
(如果您不能从一开始就拥有 ZonedDateTime
)。
在修改我们的旧代码时,即使不是为了规避旧 Calendar
或 GregorianCalendar
class 中的错误,我也可能会按照上述方式进行操作。这是 运行 向 java.time 过渡的一小步。