GregorianCalendar.set()的行为与夏令时

时间:2017-08-02 14:01:08

标签: java datetime timezone-offset gregorian-calendar

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

上下文

有些国家通过改变时间来节省一些日光。例如,在时区"欧洲/巴黎",每年,时间从3月结束前1小时,10月结束1小时,均在凌晨2点到凌晨3点之间。例如,这导致10月,2016年30日凌晨02:15存在两次。

幸运的是,两个日期没有相同的时间戳(ms)数量,也没有可读的表示形式:

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

问题

在巴黎时区实例化GregorianCalendar对象后(使用SimpleDateFormat),我们按照预期在向后移动之前02:15到达。

但是如果我们想使用.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

1 个答案:

答案 0 :(得分:2)

一个丑陋的选择是在设置值后从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,所以似乎这个bug从未修复过。

Java新日期/时间API

旧类(DateCalendarSimpleDateFormat)有lots of problemsdesign issues,它们将被新API取代。< / p>

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

如果您使用的是 Java&lt; = 7 ,则可以使用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));

输出结果为:

  

Sun Oct 30 02:15:00 CEST 2016

我使用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

输出将是:

  

Sun Oct 30 02:10:00 CEST 2016
  Sun Oct 30 02:25:00 CEST 2016

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

要将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无法决定使用哪一个)