【问题标题】:Behaviour of GregorianCalendar.set() with daylight saving timesGregorianCalendar.set() 与夏令时的行为
【发布时间】:2018-01-09 19:26:49
【问题描述】:

在排除故障时,此方法行为中出现了一些奇怪的现象。


上下文

一些国家通过改变时间来节省一些日光。例如,在“欧洲/巴黎”时区,每年 3 月底的时间提前 1 小时,10 月底的时间提前 1 小时,时间都是在凌晨 2 点到凌晨 3 点之间。例如,这会导致 2016 年 10 月 30 日凌晨 02:15 的日期存在两次。

幸运的是,两个日期的时间戳(毫秒)数量不同,也没有可读的表示:

  • 时移之前:2016 年 10 月 30 日星期日 02:15:00 CEST(UTC +0200)
  • 时移后:2016 年 10 月 30 日星期日 02:15:00 CET(UTC +0100)

问题

在巴黎时区(使用SimpleDateFormat)实例化GregorianCalendar 对象后,我们得到了02:15 AM,然后按预期向后移动。

但如果我们想使用.set() 为这个对象设置分钟,+0200 偏移信息会损坏为 +0100(“相同时间”,但在时间偏移之后) 有没有办法这样做,因为方法.add()实际上保留了偏移信息?

示例

    // Instantiation
    GregorianCalendar gc1 = new GregorianCalendar(TimeZone.getTimeZone("Europe/Paris"));
    gc1.setTime(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss Z").parse("2016-10-30 02:15:00 +0200"));
    GregorianCalendar gc2 = (GregorianCalendar) gc1.clone();
    
    System.out.println(gc1.getTime()); // Output : Sun Oct 30 02:15:00 CEST 2016 ; OK
    System.out.println(gc2.getTime()); // Output : Sun Oct 30 02:15:00 CEST 2016 ; OK
    
    // Set/add minutes
    gc1.set(Calendar.MINUTE, 10);
    gc2.add(Calendar.MINUTE, 10);
    
    System.out.println(gc1.getTime()); // Output : Sun Oct 30 02:10:00 CET 2016 ; Unexpected
    System.out.println(gc2.getTime()); // Output : Sun Oct 30 02:25:00 CEST 2016 ; OK

【问题讨论】:

    标签: java datetime timezone-offset gregorian-calendar


    【解决方案1】:

    一个丑陋的替代方法是在设置值后从gc1 中减去 1 小时:

    gc1.set(Calendar.MINUTE, 10);
    gc1.add(Calendar.HOUR, -1);
    

    结果将是Sun Oct 30 02:10:00 CEST 2016。

    不幸的是,这似乎是Calendar API 可用的最佳解决方案。 set 方法的这种行为被报告为错误,the recommendation in JDK bug tracker 是使用 add 来“修复”它:

    在“回退”期间,日历不支持消歧,给定的本地时间被解释为标准时间。

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

    我使用的是 Java 8,所以这个错误似乎从未修复过。


    Java 新的日期/时间 API

    旧的类(Date、Calendar 和 SimpleDateFormat)有 lots of problems 和 design issues,它们正在被新的 API 取代。

    如果您使用的是 Java 8,请考虑使用 new java.time API。更简单,less bugged and less error-prone than the old APIs。

    如果您使用的是 Java ,则可以使用 ThreeTen Backport,它是 Java 8 新日期/时间类的一个很好的向后移植。对于Android,还有ThreeTenABP(更多关于如何使用它here)。

    下面的代码适用于两者。 唯一的区别是:

    • 包名称(在 Java 8 中为 java.time,在 ThreeTen Backport(或 Android 的 ThreeTenABP)中为 org.threeten.bp),但类和方法名称是相同的。
    • 从/到旧的Calendar API 的转换

    要将GregorianCalendar 转换为新的 API,您可以:

    // Paris timezone
    ZoneId zone = ZoneId.of("Europe/Paris");
    // convert GregorianCalendar to ZonedDateTime
    ZonedDateTime z = Instant.ofEpochMilli(gc1.getTimeInMillis()).atZone(zone);
    

    在 Java 8 中,您还可以这样做:

    ZonedDateTime z = gc1.toInstant().atZone(zone);
    

    要以java.util.Date 生成的相同格式获取日期,您可以使用DateTimeFormatter:

    DateTimeFormatter fmt = DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss z yyyy", Locale.ENGLISH);
    System.out.println(fmt.format(z));
    

    输出是:

    2016 年 10 月 30 日星期日 02:15:00 CEST

    我使用java.util.Locale 将区域设置强制为英语,因此月份名称和星期几的格式正确。如果您不指定区域设置,它将使用系统的默认设置,并且不保证始终为英语(并且默认设置也可以在不通知的情况下更改,即使在运行时也是如此,因此最好始终明确说明哪一个你正在使用)。

    使用此 API,您可以轻松添加或设置分钟:

    // change the minutes to 10
    ZonedDateTime z2 = z.withMinute(10);
    System.out.println(fmt.format(z2)); // Sun Oct 30 02:10:00 CEST 2016
    
    // add 10 minutes
    ZonedDateTime z3 = z.plusMinutes(10);
    System.out.println(fmt.format(z3)); // Sun Oct 30 02:25:00 CEST 2016
    

    输出将是:

    2016 年 10 月 30 日星期日 02:10:00 CEST
    2016 年 10 月 30 日星期日 02:25:00 CEST

    请注意,在新 API 中,类是不可变的,因此 with 和 plus 方法返回一个新实例。

    要将ZonedDateTime 转换回GregorianCalendar,您可以:

    gc1.setTimeInMillis(z.toInstant().toEpochMilli());
    

    在java 8中,你也可以这样做:

    gc1 = GregorianCalendar.from(z);
    

    要解析输入2016-10-30 02:15:00 +0200,您必须使用另一个格式化程序:

    DateTimeFormatter parser = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss XX")
        .withZone(ZoneId.of("Europe/Paris"));
    ZonedDateTime z = ZonedDateTime.parse("2016-10-30 02:15:00 +0200", parser);
    

    我必须使用withZone 方法设置时区,因为仅偏移量+0200 不足以确定它(more than one timezone can use this same offset 和 API 无法决定使用哪一个)

    【讨论】:

    • 很好,所以这种奇怪的行为已经众所周知并且是一个实际的错误。我实际上已经通过双重使用 .add() 解决了我的问题,它产生了预期的结果。现在,我使用的是 JDK 1.7,你描述的 API 看起来很不错,但是我目前没有时间进行代码重构(我花了整整一周的时间才找到问题),但我会马上回到 ZonedDateTime对于任何其他问题。
    猜你喜欢
    • 2012-11-23
    • 2012-03-20
    • 2021-09-24
    • 2019-01-03
    • 2019-12-27
    • 2014-04-18
    • 1970-01-01
    • 1970-01-01
    • 2011-11-12
    相关资源
    最近更新 更多