GregorianCalendar DST问题

时间:2014-03-20 08:55:52

标签: java calendar dst gregorian-calendar

如果我在秋季日光照时间结束时(2014-10-26 02:00:00 CET在丹麦)并减去一小时(所以我希望回到02:00 CEST )然后将分钟设置为零,我得到一些奇怪的结果:

Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("CET"));

cal.set(Calendar.YEAR, 2014);
cal.set(Calendar.MONTH, Calendar.OCTOBER);
cal.set(Calendar.DAY_OF_MONTH, 26);
cal.set(Calendar.HOUR_OF_DAY, 2);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);

System.out.println(cal.getTimeInMillis()); // 1414285200000 : 01:00:00 UTC

cal.add(Calendar.HOUR_OF_DAY, -1);

System.out.println(cal.getTimeInMillis()); // 1414281600000 : 00:00:00 UTC (as expected)

cal.set(Calendar.MINUTE, 0);
// or: cal.set(Calendar.MINUTE, cal.get(Calendar.MINUTE));
// both should be NOPs.

System.out.println(cal.getTimeInMillis()); // 1414285200000 : 01:00:00 UTC (Why?!)

这是一个错误吗?我知道Java Calendar有一些奇怪的约定,但我看不出这是怎么回事。

使用Calendar类减去一小时并将分钟设置为0的正确方法是什么?

3 个答案:

答案 0 :(得分:10)

以下是来自Java Bug跟踪器的comments to the similar "bug"

  

在"回落期间"期间,日历不支持消歧,给定的当地时间被解释为标准时间。

     

要避免意外的DST达到标准时间更改,请调用add()以重置值。

这意味着您应该将set()替换为

cal.add(Calendar.MINUTE, -cal.get(Calendar.MINUTE));

答案 1 :(得分:4)

apangin's response中的链接解释了问题并提出了解决方案。我确实试图通过代码来查看它。

如果我们在设置之前和之后检查DST_OFFSET:

System.out.println(cal.get(Calendar.DST_OFFSET));
cal.set(Calendar.MINUTE, 0);
System.out.println(cal.get(Calendar.DST_OFFSET));

打印:

3600000
0

查看methodgetTimeInMillis,我们看到他们使用一个标志(isTimeSet)来检查以毫秒重新计算时间:

public long getTimeInMillis() {
   if (!isTimeSet) {
       updateTime();
   }
   return time;
}

调用设置时,标志 isTimeSet 会重置为false。来自GregorianCalendar的来源:

public void set(int field, int value)
{
    if (isLenient() && areFieldsSet && !areAllFieldsSet) {
        computeFields();
    }
    internalSet(field, value);
    isTimeSet = false;             // <-------------- here
    areFieldsSet = false;
    isSet[field] = true;
    stamp[field] = nextStamp++;
    if (nextStamp == Integer.MAX_VALUE) {
        adjustStamp();
    }
}

所以设置重置DST偏移量。 This line获得抵消:

zoneOffset = ((ZoneInfo)tz).getOffsets(time, zoneOffsets);

little down值设置为:

internalSet(DST_OFFSET, zoneOffsets[1]); 

以毫秒为单位的时间在下一次调用getTimeInMillis时被更改。

作为apangin suggest,我们不应在初始化日期后使用设置,否则我们会强制重置日期。这是OpenJDK的bug跟踪器中的帖子中的建议。

cal.add(Calendar.MINUTE, -cal.get(Calendar.MINUTE));

更好的替代方案是使用Joda Time。这个库的作者一直在研究新的Java 8的日期时间api,我希望不会有这样的问题。

答案 2 :(得分:1)

Calendar.add方法必须按如下方式执行:

  

在指定的日历字段中添加或减去指定的时间,根据日历的规则

所以我们在这里遇到了神秘的日历规则。我们Calendar.getInstance(TimeZone.getTimeZone("CET"))获得的日历类型是什么?

执行

System.out.println(Calendar.getInstance(TimeZone.getTimeZone("CET")).getClass());

我得到了答案:class java.util.GregorianCalendar。让我们尝试查找GregorianCalendar的规则。我在add method找到了它们(为什么在这里?)。其中一条规则适合您的情况。

  

添加规则2.如果预期较小的字段不变,但由于字段更改后其最小值或最大值的变化,它不可能等于其先前值,则将其值调整为尽可能接近其预期值

01:00:00和23:00:00具有相同的距离,因此这两个规则都有效。

另请注意GregorianCalendar.add规范

中的下一行
  

添加指定的(签名)时间

它表示您使用错误调用cal.add(Calendar.HOUR_OF_DAY, -1);因此,您无需责怪您的add仅在下次set调用时显示结果。但它某种程度上有效;你可以用它!

这里有什么要做的?如果您确定2014-10-26 02:00:00 CET减去一小时等于自己 - 那么您可以在set之后添加愚蠢的add。像那样:

    cal.add(Calendar.HOUR_OF_DAY, -1);
    cal.set(Calendar.HOUR, cal.get(Calendar.HOUR)); //apply changes
    cal.set(Calendar.MINUTE, 0);

如果您希望在-1 hour操作后看到差异,那么我建议您更改TimeZone。 TimeZone.getTimeZone("GMT")(例如)没有夏令时偏移量。