1. Overview
1.概述
Daylight Saving Time, or DST, is a practice of advancing clocks during summer months in order to leverage an additional hour of the natural light (saving heating power, illumination power, enhancing the mood, and so on).
夏令时,或称DST,是指在夏季将时钟提前,以利用多一个小时的自然光的做法(节省加热功率、照明功率、提高情绪等)。
It’s used by several countries and needs to be taken into account when working with dates and timestamps.
它被几个国家使用,在处理日期和时间戳时需要考虑到。
In this tutorial, we’ll see how to correctly handle DST in Java according to different locations.
在本教程中,我们将看到如何根据不同地点在Java中正确处理夏令时。
2. JRE and DST Mutability
2.JRE和DST的突变性
First, it’s extremely important to understand that worldwide DST zones change very often and there’s no central authority coordinating it.
首先,了解全世界的夏令时区变化非常频繁而且没有中央机构进行协调,这一点极为重要。
A country, or in some cases even a city, can decide if and how to apply or revoke it.
一个国家,或者在某些情况下甚至是一个城市,可以决定是否以及如何应用或撤销它。
Everytime it happens, the change is recorded in the IANA Time Zone Database, and the update will be rolled out in a future release of the JRE.
每次发生这种情况时,都会在IANA时区数据库中记录该变化,并且将在未来的JRE版本中推出该更新。
In case it’s not possible to wait, we can force the modified Time Zone data containing the new DST settings into the JRE through an official Oracle tool called Java Time Zone Updater Tool, available on the Java SE download page.
如果无法等待,我们可以通过Java Time Zone Updater Tool的Oracle官方工具将包含新的DST设置的修改后的时区数据强制导入JRE,该工具可在Java SE下载页面获得。
3. The Wrong Way: Three-Letter Timezone ID
3.错误的方法 三个字母的时区标识
Back in the JDK 1.1 days, the API allowed three-letter time zone IDs, but this led to several problems.
早在JDK 1.1时代,API允许三个字母的时区ID,但这导致了几个问题。
First, this was because the same three-letter ID could refer to multiple time zones. For example, CST could be U.S. “Central Standard Time”, but also “China Standard Time”. The Java platform could then only recognize one of them.
首先,这是因为同一个三字母ID可以指代多个时区。例如,CST可以是美国 “中央标准时间”,但也可以是 “中国标准时间”。然后,Java平台只能识别其中一个。
Another issue was that Standard timezones never take Daylight Saving Time into an account. Multiple areas/regions/cities can have their local DST inside the same Standard time zone, so the Standard time doesn’t observe it.
另一个问题是,标准时区从未考虑到夏令时。多个地区/区域/城市可以在同一个标准时区内有其当地的夏令时,所以标准时间并不观察它。
Due to backward compatibility, it’s still possible to instantiate a java.util.Timezone with a three-letter ID. However, this method is deprecated and shouldn’t be used anymore.
由于向后兼容,仍然可以用三个字母的ID来实例化一个java.util.Timezone。但是,这种方法已经被废弃了,不应该再使用。
4. The Right Way: TZDB Timezone ID
4.正确的方法 TZDB的时区标识
The right way to handle DST in Java is to instantiate a Timezone with a specific TZDB Timezone ID, eg. “Europe/Rome”.
在Java中处理DST的正确方法是实例化一个具有特定TZDB时区ID的时区,例如“欧洲/罗马”。
Then, we’ll use this in conjunction with time-specific classes like java.util.Calendar to get a proper configuration of the TimeZone’s raw offset (to the GMT time zone), and automatic DST shift adjustments.
然后,我们将与java.util.Calendar等特定的时间类结合使用,以获得TimeZone的原始偏移量(到GMT时区)的适当配置,以及自动的DST转变调整。
Let’s see how the shift from GMT+1 to GMT+2 (which happens in Italy on March 25, 2018, at 02:00 am) is automatically handled when using the right TimeZone:
让我们看看当使用正确的TimeZone时,如何从GMT+1转移到GMT+2(发生在意大利的时间是2018年3月25日凌晨02:00)被自动处理:。
TimeZone tz = TimeZone.getTimeZone("Europe/Rome");
TimeZone.setDefault(tz);
Calendar cal = Calendar.getInstance(tz, Locale.ITALIAN);
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.ITALIAN);
Date dateBeforeDST = df.parse("2018-03-25 01:55");
cal.setTime(dateBeforeDST);
assertThat(cal.get(Calendar.ZONE_OFFSET)).isEqualTo(3600000);
assertThat(cal.get(Calendar.DST_OFFSET)).isEqualTo(0);
As we can see, ZONE_OFFSET is 60 minutes (because Italy is GMT+1) while DST_OFFSET is 0 at that time.
我们可以看到,ZONE_OFFSET是60分钟(因为意大利是GMT+1),而当时的DST_OFFSET是0。
Let’s add ten minutes to the Calendar:
让我们在Calendar上增加10分钟。
cal.add(Calendar.MINUTE, 10);
Now DST_OFFSET has become 60 minutes too, and the country has transitioned its local time from CET (Central European Time) to CEST (Central European Summer Time) which is GMT+2:
现在DST_OFFSET也变成了60分钟,该国的当地时间已经从CET(中欧时间)过渡到CEST(中欧夏令时),也就是GMT+2。
Date dateAfterDST = cal.getTime();
assertThat(cal.get(Calendar.DST_OFFSET))
.isEqualTo(3600000);
assertThat(dateAfterDST)
.isEqualTo(df.parse("2018-03-25 03:05"));
If we display the two dates in the console, we’ll see the time zone change as well:
如果我们在控制台显示这两个日期,我们也会看到时区的变化。
Before DST (00:55 UTC - 01:55 GMT+1) = Sun Mar 25 01:55:00 CET 2018
After DST (01:05 UTC - 03:05 GMT+2) = Sun Mar 25 03:05:00 CEST 2018
As a final test, we can measure the distance between the two Dates, 1:55 and 3:05:
作为最后的测试,我们可以测量两个Dates之间的距离,1:55和3:05。
Long deltaBetweenDatesInMillis = dateAfterDST.getTime() - dateBeforeDST.getTime();
Long tenMinutesInMillis = (1000L * 60 * 10);
assertThat(deltaBetweenDatesInMillis)
.isEqualTo(tenMinutesInMillis);
As we’d expect, the distance is of 10 minutes instead of 70.
正如我们所期望的,距离是10分钟,而不是70分钟。
We’ve seen how to avoid falling into the common pitfalls that we can encounter when working with Date through the correct usage of TimeZone and Locale.
我们已经看到了如何通过正确使用TimeZone和Locale来避免落入我们在使用Date时可能遇到的常见误区。
5. The Best Way: Java 8 Date/Time API
5.最好的方法 Java 8的日期/时间API
Working with these thread-unsafe and not always user-friendly java.util classes have always been tough, especially due to compatibility concerns which prevented them from being properly refactored.
使用这些线程不安全且不总是用户友好的java.util类的工作一直很艰难,特别是由于兼容性问题,使它们无法被适当重构。
For this reason, Java 8 introduced a brand new package, java.time, and a whole new API set, the Date/Time API. This is ISO-centric, fully thread-safe and heavily inspired by the famous library Joda-Time.
为此,Java 8引入了一个全新的包java.time和一个全新的API集,即Date/Time API。这是一个以ISO为中心的、完全线程安全的、深受著名库Joda-Time启发的包。
Let’s take a closer look at this new classes, starting from the successor of java.util.Date, java.time.LocalDateTime:
让我们从java.util.Date的继承者java.time.LocalDateTime开始,仔细看看这些新的类。
LocalDateTime localDateTimeBeforeDST = LocalDateTime
.of(2018, 3, 25, 1, 55);
assertThat(localDateTimeBeforeDST.toString())
.isEqualTo("2018-03-25T01:55");
We can observe how a LocalDateTime is conforming to the ISO8601 profile, a standard and widely adopted date-time notation.
我们可以观察到LocalDateTime是如何符合ISO8601配置文件的,这是一个标准的、广泛采用的日期时间符号。
It’s completely unaware of Zones and Offsets, though, that’s why we need to convert it into a fully DST-aware java.time.ZonedDateTime:
但它完全不知道Zones和Offsets,这就是为什么我们需要将其转换为完全了解DST的java.time.ZonedDateTime。
ZoneId italianZoneId = ZoneId.of("Europe/Rome");
ZonedDateTime zonedDateTimeBeforeDST = localDateTimeBeforeDST
.atZone(italianZoneId);
assertThat(zonedDateTimeBeforeDST.toString())
.isEqualTo("2018-03-25T01:55+01:00[Europe/Rome]");
As we can see, now the date incorporates two fundamental trailing pieces of information: +01:00 is the ZoneOffset, while [Europe/Rome] is the ZoneId.
我们可以看到,现在的日期包含了两个基本的尾部信息。+01:00是ZoneOffset,而[Europe/Rome]是ZoneId。
Like in the previous example, let’s trigger DST through the addition of ten minutes:
就像前面的例子一样,让我们通过增加十分钟来触发DST。
ZonedDateTime zonedDateTimeAfterDST = zonedDateTimeBeforeDST
.plus(10, ChronoUnit.MINUTES);
assertThat(zonedDateTimeAfterDST.toString())
.isEqualTo("2018-03-25T03:05+02:00[Europe/Rome]");
Again, we see how both the time and the zone offset are shifting forward, and still keeping the same distance:
我们再次看到,时间和区域的偏移都在向前移动,但仍然保持相同的距离。
Long deltaBetweenDatesInMinutes = ChronoUnit.MINUTES
.between(zonedDateTimeBeforeDST,zonedDateTimeAfterDST);
assertThat(deltaBetweenDatesInMinutes)
.isEqualTo(10);
6. Conclusion
6.结论
We’ve seen what Daylight Saving Time is and how to handle it through some practical examples in different versions of Java core API.
我们已经看到了什么是夏令时,以及如何通过不同版本的Java核心API的一些实际例子来处理它。
When working with Java 8 and above, the usage of the new java.time package is encouraged thanks to the ease of use and to its standard, thread-safe nature.
当使用Java 8及以上版本时,我们鼓励使用新的java.time包,因为它易于使用,而且具有标准、线程安全的特性。
As always, the full source code is available over on Github.
像往常一样,完整的源代码可在Github上获得。