首先,我想说明一下,我知道Java类正在被其他更好的库所取代。也许我偶然发现了“日历”不受欢迎的原因之一。
我在日历中遇到了令人沮丧的行为,因为它涉及到夏令时结束时的重叠时间。
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"
}按照注释,您将看到清除日历中的分钟字段实际上将其移动了一个小时!该死的!?
当我使用oneAMPDT.set(Calendar.MINUTE, 0)时也会发生这种情况。
这是预期的行为吗?有什么办法可以防止这种情况发生吗?
发布于 2021-11-25 00:24:09
避免旧式日期-时间类;需要时进行转换
正如您注意到的,Calendar几年前被JSR 310中定义的java.time类所取代(一致通过)。正如您注意到的,避免使用Calendar和Date等有很多原因。
如果您必须有一个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美国/洛杉矶
但你又跳到了另一个时刻,凌晨1点
ZonedDateTime oneAmOnThe7thLosAngeles = firstMomentOfThe7thInLosAngeles.with( LocalTime.of( 1 , 0 ) ) ;oneAmOnThe7thLosAngeles.toString():2021-11-07T01:00-07:00美国/洛杉矶
该日的时间可能存在,也可能不存在于该区域的那个日期。如果需要,ZonedDateTime类将进行调整。
您使用了变量的名称midnightPDT。我建议避免使用midnight这个术语,因为它的使用混淆了日期时间的处理,而没有一个精确的定义。如果这是你的意思,我建议使用“一天中的第一分钟”这个词。
您提取自1970年第一时刻的划时代引用以来的毫秒数,如世界协调时,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
我们应该看到一个小时的差别。1小时=3 600 000= 60 * 60 *1 000。
long diff = ( millisSinceEpoch_OneAmOn7thLosAngeles - millisSinceEpoch_FirstMomentOf7thLosAngeles ); // 3,600,000 = 60 * 60 * 1,000.差夫= 3600000
切割器
然后继续提到夏令时(DST)切割器。美国DST的截止日期是凌晨2点,而不是凌晨1点。在凌晨2点到达的那一刻,时钟又回到凌晨1点,第二次是凌晨1点-2点。
为了达到切入点,让我们加一个小时。
ZonedDateTime cutover_Addition = oneAmOnThe7thLosAngeles.plusHours( 1 );cutover_Addition =2021-11-07T01:00-08:00美国/洛杉矶
请注意,一天的时间显示相同的(上午1),但与世界协调时的偏移已经从世界协调时后的7小时改为现在的世界协调时后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类确实提供了在切入的这些时刻使用的一对方法:withEarlierOffsetAtOverlap和withLaterOffsetAtOverlap。
ZonedDateTime cutover_OverlapEarlier =
cutover_Addition
.withEarlierOffsetAtOverlap();
ZonedDateTime cutover_OverlapLater =
cutover_Addition
.withLaterOffsetAtOverlap();cutover_OverlapEarlier =2021-11-07T01:00-07:00美国/洛杉矶 cutover_OverlapLater =2021-11-07T01:00-08:00美国/洛杉矶
Calendar
如果您确实需要一个Calendar对象,只需转换即可。
Calendar x = GregorianCalendar.from( firstMomentOfThe7thInLosAngeles ) ;
Calendar y = GregorianCalendar.from( oneAmOnThe7thLosAngeles ) ;
Calendar z = GregorianCalendar.from( cutover_Addition );如果您的目标仅仅是挣扎于理解Calendar类行为,我建议您停止受虐行为。没有任何意义。Sun、Oracle和JCP社区都放弃了对这些可怕的遗留日期-时间类。我建议你也这么做。
示例代码
把上面所有的代码拼凑在一起。
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]发布于 2021-11-25 07:31:28
java.time
这是预期的行为吗?不是的。我认为这是一只虫子。
有什么办法可以防止这种情况发生吗?是的,您已经提到或至少暗示的方式:使用ZonedDateTime而不是Calendar。巴兹尔·伯克已经说过了。作为一个适度的补充,我想展示从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());输出:
星期日07 01:00:00 PDT 2021太阳11月07 01:00 PDT 2021
但我用的是GregorianCalendar而不是Calendar?你也是。GregorianCalendar是从Calendar.getIntance()获得的Calendar的子类。在某些环境中,您可能会得到一个不同的子类来反映那里使用的日历系统,并且您对set的初始调用不会给出您预期的结果。在这种情况下,您需要一个GregorianCalendar (如果从一开始就没有ZonedDateTime )。
在修改旧代码时,即使不是为了规避旧Calendar或GregorianCalendar类中的错误,我也可能以上述方式进行修改。这是向java.time长期过渡的一小步。
https://stackoverflow.com/questions/70104308
复制相似问题