【问题标题】:Changing time zone with Calendar object from EEST to HST changes the time-of-day unexpectedly将日历对象的时区从 EEST 更改为 HST 会意外更改时间
【发布时间】:2018-12-17 20:53:49
【问题描述】:

我有一些TimeStamp 和日期格式"EEEE, MMM dd, yyyy hh:mm a zzz"。但我不知道如何用时区显示这个时间戳。当我试图显示它时,我得到了错误的 DateTime 或错误的时区

例子1:

Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date(device.getUpdatedDate().getTime())); // here is 2018-07-09 20:02:26.506000
SimpleDateFormat sdf = new SimpleDateFormat(EMAIL_DATE_FORMAT);
sdf.format(calendar.getTime()); // i have wrong timezone

我得到Monday, Jul 09, 2018 08:02 PM EEST

但是当我添加sdf.setTimeZone(TimeZone.getTimeZone("HST")); 我有正确的时区和错误的时间 Monday, Jul 09, 2018 07:02 AM HST

预期结果Monday, Jul 09, 2018 08:02 PM HST

实际结果:Monday, Jul 09, 2018 08:02 PM EESTMonday, Jul 09, 2018 07:02 AM HST

【问题讨论】:

  • 您的代码中可能有错误。你想展示它吗?这里的一些人能够在他们正在查看的代码中找到错误。但是没有代码,这很难做到……
  • 到目前为止您尝试过的内容请发布您的代码
  • 请学习并应用制作minimal reproducible example的概念。
  • 请阅读 MCVE 链接并努力创建一个。
  • 我建议您避免使用 CalendarDateSimpleDateFormat 类。它们不仅早已过时,而且设计也很糟糕,SimpleDateFormat 特别是出了名的麻烦。今天我们在java.time, the modern Java date and time API 的表现要好得多。

标签: java date timezone timestamp simpledateformat


【解决方案1】:

tl;博士

  • 您看到的是功能,而不是错误,正在将同一时刻调整到另一个时区。
    • 东欧晚上 8 点也是夏威夷早上 7 点。
  • 使用实时时区名称,而不是 3-4 个字母的伪时区。
  • 使用现代的 java.time 类,而不是设计不佳的遗留类。

同一时刻:拨打withZoneSameInstant

您从比 UTC提前 小时的一个时区调整到比 UTC 落后 10 小时的另一个时区,总共相差 13 小时。了解一个地方的晚上 8 点同时在另一个地方是早上 7 点。

请注意我们在以下代码中对withZoneSameInstant(“即时”)的调用。

ZonedDateTime.of( 
    2018 , 7 , 9 , 20 , 2 , 0 , 0 , ZoneId.of( "Europe/Athens" )  // 8 PM in Greece.
)
.withZoneSameInstant(                                             // Adjust into another time zone to view the same moment with another wall-clock time.
    ZoneId.of( "Pacific/Honolulu" )                               // 7 AM in Hawaii.
)
.toString()

2018-07-09T07:02-10:00[太平洋/檀香山]

不同时刻:拨打withZoneSameLocal

显然您想要一个不同的时刻,时间轴上的不同点,具有相同的日期和时间,但时区不同。

请注意我们在以下代码中对withZoneSameLocal(“本地”,而不是“即时”)的调用。

ZonedDateTime.of( 
    2018 , 7 , 9 , 20 , 2 , 0 , 0 , ZoneId.of( "Europe/Athens" )  // 8 PM in Greece.
)
.withZoneSameLocal(                                               // Different moment, coincidentally having the same date and same time-of-day. But different time zone means this is a different point on the timeline.
    ZoneId.of( "Pacific/Honolulu" )                               // Also 8 PM in Hawaii, which happens many hours later than 8 PM in Greece. Different moment, same wall-clock time.
)
.toString()

2018-07-09T20:02-10:00[太平洋/檀香山]


详情

实时时区

HST & EEST 是伪时区,不是实时时区。避免使用这些 3-4 字母代码,因为它们不是标准化的,甚至不是唯一的(!)。

使用tzdata 中由IANA 定义的实时时区名称。请参阅list in Wikipedia(可能已过时)。这些名称采用Continent/Region 格式,例如America/MontrealEurope/Tallinn

ZoneId z = ZoneId.of( "Europe/Vilnius" ) ;

避免使用旧的日期时间类

避免使用非常麻烦的课程CalendarSimpleDateFormat。这些在几年前被 java.time 类所取代。使用 java.time 更加清晰和容易。

让我们开始吧。我猜EEST 你想到的是东欧时区之一。我随意选择一个。

ZoneId zAthens = ZoneId.of( "Europe/Athens" ) ;
LocalDate ld = LocalDate.of( 2018 , Month.JULY , 9 ) ;  // 2018-07-09.
LocalTime lt = LocalTime.of( 20 , 2 ) ;  // 8:02 PM.
ZonedDateTime zdtAthens = ZonedDateTime.of( ld , lt , zAthens ) ;

生成一个String 表示该ZonedDateTime 对象的值。默认情况下,使用标准的ISO 8601 格式,明智地扩展以在方括号中附加时区名称。

String outputAthens = zdtAthens.toString() ;  // Generate `String` in a format extending standard ISO 8601 format.

2018-07-09T20:02+03:00[欧洲/雅典]

HST 我猜你的意思是夏威夷时间。该区域的正确名称是Pacific/Honolulu

ZoneId zHonolulu = ZoneId.of( "Pacific/Honolulu" ) ;

让我们将雅典时刻调整到夏威夷的另一个区域。同一时刻,时间轴上的同一点,但挂钟时间不同。想象一下,每个地方都有一对朋友打电话给每个人,同时抬头看着墙上的时钟。每个人都看到不同的时间,也可能是不同的日期,但他们同时经历了相同的时刻,时间轴上的相同点。

ZonedDateTime zdtHonolulu = zdtAthens.withZoneSameInstant( zHonolulu ) ;  // Same moment (same `Instant` inside the `ZonedDateTime`) but a different time zone.

在那一天,檀香山比世界标准时间晚十小时,而雅典则比世界标准时间早三小时。那是十三个小时的总增量。因此,晚上 8 点(20:00)减去 13 是早上 7 点。我们预计会在夏威夷看到早上 7 点。让我们通过生成另一个 ISO 8601 格式的字符串来验证。

String outputHonolulu = zdtHonolulu.toString() ;  // Generate `String` representing the value of the `ZonedDateTime` object.

2018-07-09T07:02-10:00[太平洋/檀香山]

果然,早上 7 点。

也许您想要的是位于夏威夷的同一日期和同一时间。这意味着您代表同一个同时的时刻。您将代表时间轴上的不同点,相差几个小时。

ZonedDateTime 确实提供了这个功能。调用ZonedDateTime::withZoneSameLocal 概念上的含义:使用相同的内部LocalDate 和相同的内部LocalTime,但使用不同的分配ZoneId

ZonedDateTime eightPmOnJuly9InPacificHonolulu = zdtAthens.withZoneSameLocal( zHonolulu) ; 
String outputDifferentMoment= eightPmOnJuly9InPacificHonolulu.toString() ;

2018-07-09T20:02-10:00[太平洋/檀香山]

UTC

所有这些在时区之间来回切换都会让人发疯。通过专注于 UTC 来扎根。将 UTC 视为 真正的时间,而所有其他时区只是变体。

要从时区调整为 UTC,请从我们的 ZonedDateTime 对象中提取 Instant 对象。根据定义,Instant 始终采用 UTC。

Instant instantAthens = zdtAthens.toInstant() ;
Instant instantHonolulu = zdtHonolulu.toInstant() ;
Instant instantDifferentMoment = eightPmOnJuly9InPacificHonolulu.toInstant() ;

2018-07-09T17:02:00Z

2018-07-09T17:02:00Z

2018-07-10T06:02:00Z

末尾的 Z 表示 UTC,发音为 Zulu,由 ISO 8601 和其他标准定义。


关于java.time

java.time 框架内置于 Java 8 及更高版本中。这些类取代了麻烦的旧 legacy 日期时间类,例如 java.util.DateCalendarSimpleDateFormat

Joda-Time 项目现在位于maintenance mode,建议迁移到java.time 类。

要了解更多信息,请参阅Oracle Tutorial。并在 Stack Overflow 上搜索许多示例和解释。规格为JSR 310

您可以直接与您的数据库交换 java.time 对象。使用符合JDBC 4.2 或更高版本的JDBC driver。不需要字符串,不需要java.sql.* 类。

从哪里获得 java.time 类?

ThreeTen-Extra 项目通过附加类扩展了 java.time。该项目是未来可能添加到 java.time 的试验场。您可以在这里找到一些有用的类,例如IntervalYearWeekYearQuartermore

【讨论】:

    【解决方案2】:

    据我了解,device.getUpdatedDate() 返回一个java.sql.Timestamp,例如2018-07-09 20:02:26.506000。我假设(与时间戳的概念相反)这个日期和时间是在没有时区的情况下产生的,但你知道它是在太平洋/檀香山时区(夏威夷标准时间或 HST,全年在夏威夷使用)。

    如果您从 SQL 数据库中获取 Timestamp(这很正常),请不要这样做。 Timestamnp 类早已过时且相当混乱。假设您至少使用 Java 8 和 JDBC 4.2,您可能会从数据库中获取现代 LocalDateTime 的实例。假设 rs 是您的结果集并且您的列名为 updated_date(我相信您可以根据自己的情况定制代码):

        DateTimeFormatter formatter
                = DateTimeFormatter.ofPattern(EMAIL_DATE_FORMAT, Locale.US);
        LocalDateTime updatedDateTime 
                = rs.getObject("updated_date", LocalDateTime.class);
        ZonedDateTime timeOnHawaii 
                = updatedDateTime.atZone(ZoneId.of("Pacific/Honolulu"));
        String formattedDateTime = timeOnHawaii.format(formatter);
    

    如果您的时间戳与时区信息一起存储在数据库中,您可以改用Instant.class 做得更好,并确保获得与数据库中相同的时间点。确切的可能性取决于您的 JDBC 驱动程序的功能。

    如果你无法避免得到Timestamp,请像这样转换它:

        ZonedDateTime timeOnHawaii = device.getUpdatedDate()
                .toLocalDateTime()
                .atZone(ZoneId.of("Pacific/Honolulu"));
    

    使用您的示例 Timestamp 和我上面的第一个代码 sn-p 中的格式化程序打印:

    2018 年 7 月 9 日星期一 08:02 PM HST

    (假设EMAIL_DATE_FORMAT被定义为"EEEE, MMM dd, yyyy hh:mm a zzz")。

    你的代码出了什么问题?

    关于Timestamp 的一个令人困惑的地方是它表示一个时间点(没有时区),但它经常被用来表示一天中的日期和时间(也没有时区)。因此,您的 2018-07-09 20:02:26.506000 的 Timestamp 确实持有等于 2018-07-09 20:02:26.506000 东欧夏令时(EEST,在 Украина/Ukraine 中使用)的值(时间点) ) 但被 device.getUpdatedDate() 混淆使用来表示夏威夷的 2018-07-09 20:02:26.506000。 DateSimpleDateFormat 都不会改变时间点,这就是为什么他们给你Monday, Jul 09, 2018 08:02 PM EEST 或等效的Monday, Jul 09, 2018 07:02 AM HST

    链接:

    【讨论】:

      猜你喜欢
      • 2014-02-16
      • 2019-01-18
      • 2019-10-04
      • 1970-01-01
      • 1970-01-01
      • 2015-01-29
      • 2017-04-08
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多