【问题标题】:Java Calendar clear() changes DSTJava Calendar clear() 更改夏令时
【发布时间】:2022-01-03 07:52:59
【问题描述】:

首先,我想声明,我知道 Java Calendar 类正在被其他可以说更好的库所取代。也许我偶然发现了日历失宠的原因之一。

我在日历中遇到了令人沮丧的行为,因为它涉及到夏令时结束时的重叠时间。

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"
}

跟随 cmets,您会发现清除日历中的 MINUTE 字段实际上将其提高了一个小时!见鬼!?

当我使用oneAMPDT.set(Calendar.MINUTE, 0)时也会发生这种情况

这是预期的行为吗?有没有办法防止这种情况发生?

【问题讨论】:

  • clear 将字段设置为未定义,而不是零,尽管我希望 MINUTE 的默认值无论如何都为零。
  • 这是现实。当地时间在某处有一个小时的间隔,或重复一个小时。确切时间应以 UTC 为准。一天后再试。
  • 我转载了。同样使用oneAMPDT.set(Calendar.MINUTE, 0);,结果相同。
  • 我花了一个小时查看 Calendar 和 GregorianCalendar 的来源,但我仍然无法弄清楚为什么会发生这种情况。我知道 clear(MINUTE) 会将 DST_OFFSET 字段更改为零,但我不知道为什么。

标签: java datetime calendar dst java.util.calendar


【解决方案1】:

避免使用旧的日期时间类;根据需要转换

如您所述,Calendar 几年前已被 JSR 310 中定义的 java.time 类取代(一致通过)。正如您所注意到的,有许多理由避免使用CalendarDate 等。

如果您必须有一个Calendar 对象才能与尚未更新到java.time 的旧代码互操作,请在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 类将根据需要进行调整。

您为变量使用了名称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.

差异 = 3600000

切换

然后你继续提到Daylight Saving Time (DST) 转换。那天美国 DST 的切换时间是凌晨 2 点,而不是凌晨 1 点。在凌晨 2 点到达的那一刻,时钟拨回凌晨 1 点,第二个时间是凌晨 1:00-2:00。

为了达到那个转换点,让我们增加一个小时。

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 类在这些切换时刻确实提供了两种使用方法:withEarlierOffsetAtOverlapwithLaterOffsetAtOverlap

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 类行为,我建议您停止受虐狂。无关紧要。 Sun、Oracle 和 JCP 社区 all gave up 讨论那些可怕的遗留日期时间类。我建议你也这样做。

示例代码

将上面的所有代码汇总在一起。

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();

如果需要,转换为旧类。

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]

【讨论】:

  • 感谢您的详细解答!有助于确认我们需要转换为 JSR 310 规范。我最喜欢的部分是“我建议你停止受虐狂”。无关紧要。'说的好,重点!对于无法进行切换的任何人,请查看下面 Ole 的答案。
【解决方案2】:

java.time

这是预期的行为吗?不。我认为这是一个错误。

有没有办法防止这种情况发生? 是的,你已经提到或至少暗示的方式:使用ZonedDateTime 而不是Calendar。 Basil Bourque 已经说过了。作为一个适度的补充,我想展示从CalendarZonedDateTime 的完整往返,将分钟设置为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 的子类。在某些环境中,您会得到一个不同的子类来反映那里使用的日历系统,并且您对set 的初始调用不会给您预期的结果。在这种情况下,您想要GregorianCalendar(如果您从一开始就无法拥有ZonedDateTime)。

在修改我们的旧代码时,即使不是为了规避旧 CalendarGregorianCalendar 类中的错误,我也可能会以上述方式进行。这是向 java.time 长期过渡的一小步。

【讨论】:

    猜你喜欢
    • 2018-04-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-06-27
    • 2011-10-09
    • 1970-01-01
    • 2017-11-10
    • 2015-08-15
    相关资源
    最近更新 更多