为什么 SimpleDateFormat.format(Date) 会忽略配置的时区?

Why does SimpleDateFormat.format(Date) ignore the configured TimeZone?

鉴于我的默认时区是 Europe/Paris:

System.out.println("Current Timezone: " + TimeZone.getDefault());
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX");

String dateStrIn = "2016-08-01T00:00:00Z";
Date date = dateFormat.parse(dateStrIn);
String dateStrOut = dateFormat.format(date);

System.out.println("Input date String: "+dateStrIn);
System.out.println("Date.toString() "+date);
System.out.println("Output date String: "+dateStrOut);

输出为:

Current Timezone: sun.util.calendar.ZoneInfo[id="Europe/Paris",offset=3600000,dstSavings=3600000,useDaylight=true,transitions=184,lastRule=java.util.SimpleTimeZone[id=Europe/Paris,offset=3600000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=2,startMonth=2,startDay=-1,startDayOfWeek=1,startTime=3600000,startTimeMode=2,endMode=2,endMonth=9,endDay=-1,endDayOfWeek=1,endTime=3600000,endTimeMode=2]]
Input date String: 2016-08-01T00:00:00Z
Date.toString() Mon Aug 01 02:00:00 CEST 2016
Output date String: 2016-08-01T02:00:00+02

现在,我重复执行但设置不同的默认时区 (UTC):

TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
System.out.println("Current Timezone: " + TimeZone.getDefault());
...

这是输出:

Current Timezone: sun.util.calendar.ZoneInfo[id="UTC",offset=0,dstSavings=0,useDaylight=false,transitions=0,lastRule=null]
Input date String: 2016-08-01T00:00:00Z
Date.toString() Mon Aug 01 00:00:00 UTC 2016
Output date String: 2016-08-01T02:00:00+02

Date.toString() 已正确考虑时区更改。但是,从 dateFormat.format(date); 获取的字符串仍然显示 +02 而不是 Z 或 +00,为什么?

使用标准 Java API,有没有办法根据选定的时区强制格式化?

更新:

Jesper 的解决方案几乎适用于所有情况,但我遇到过这个(使用加那利岛西部夏令时区)它不适用:

SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss z");
dateFormat.setTimeZone(TimeZone.getTimeZone("Europe/Paris"));
TimeZone.setDefault(TimeZone.getTimeZone("Europe/Paris"));
System.out.println("Current Timezone: " + TimeZone.getDefault());

String dateStrIn = "2016-08-01T08:00:00 WEST";
Date date = dateFormat.parse(dateStrIn);
String dateStrOut = dateFormat.format(date);

System.out.println("Input date String: "+dateStrIn);
System.out.println("Date.toString() "+date);
System.out.println("Output date String: "+dateStrOut);

输出:

Current Timezone: sun.util.calendar.ZoneInfo[id="Europe/Paris",offset=3600000,dstSavings=3600000,useDaylight=true,transitions=184,lastRule=java.util.SimpleTimeZone[id=Europe/Paris,offset=3600000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=2,startMonth=2,startDay=-1,startDayOfWeek=1,startTime=3600000,startTimeMode=2,endMode=2,endMonth=9,endDay=-1,endDayOfWeek=1,endTime=3600000,endTimeMode=2]]
Input date String: 2016-08-01T08:00:00 WEST
Date.toString() Mon Aug 01 09:00:00 CEST 2016
Output date String: 2016-08-01T08:00:00 WEST

可以看到Output date String还是用WEST表示的

例如,如果我将初始 dateStrIn 更改为:String dateStrIn = "2016-08-01T08:00:00 GMT";,则输出日期字符串按预期以 CEST 表示:

Output date String: 2016-08-01T10:00:00 CEST

会不会是bug?

更新 2:

另一个例子

Default TimeZone for both Date and SimpleDateFormat: "Europe/Paris"
Input String: "2016-08-01T08:00:00 WET"

输出:

Date.toString() Mon Aug 01 10:00:00 CEST 2016
Output date String: 2016-08-01T09:00:00 WEST

注意 dateFormat.format(date); 产生了一个 WEST 日期字符串。从 WET -> WEST 应该是 WET -> CEST。

不要设置默认时区,而是在您的 SimpleDateFormat 对象上设置时区:

dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));

这将使 SimpleDateFormat 对象在 UTC 时区格式化您的 Date 对象。

如果您使用的是 Java 8,请考虑在包 java.time 中使用新的日期和时间 API 而不是旧的 java.util.Datejava.text.SimpleDateFormat.

没有,这不是错误。

您必须先了解 Date class 的工作原理。它只不过是自 epoch 以来的毫秒数的包装,以 long 表示。因此,无论在哪个时区,日期对象的基础值都保持不变。您永远无法真正更改 Date class 的时区。您只能使用 SimpleDateFormat class 表示日期实例的 String 格式。此表示可能具有不同的时区,具体取决于您在创建 SimpleDateFormat 对象时使用的时区。

同样,您需要检查日期 class 的 toString 方法。它始终使用默认时区打印日期。

编辑

您还应该查看 SimpleDateFormat.parse() 定义。 JDK 表示,

The TimeZone value may be overwritten, depending on the given pattern and the time zone value in text. Any TimeZone value that has previously been set by a call to setTimeZone may need to be restored for further operations.

这个谜团的答案在 Javadoc 上:

DateFormat.parse(String source, ParsePosition pos)

This parsing operation uses the calendar to produce a Date. As a result, the calendar's date-time fields and the TimeZone value may have been overwritten, depending on subclass implementations. Any TimeZone value that has previously been set by a call to setTimeZone may need to be restored for further operations.

解析后设置时区:

SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss z");
TimeZone.setDefault(TimeZone.getTimeZone("Europe/Paris"));

System.out.println("Current Timezone: " + TimeZone.getDefault());

String dateStrIn = "2016-08-01T08:00:00 WET";
Date date = dateFormat.parse(dateStrIn);
dateFormat.setTimeZone(TimeZone.getTimeZone("Europe/Paris"));
String dateStrOut = dateFormat.format(date);

System.out.println("Input date String: "+dateStrIn);
System.out.println("Date.toString() "+date);
System.out.println("Output date String: "+dateStrOut);

右输出:

Current Timezone: sun.util.calendar.ZoneInfo[id="Europe/Paris",offset=3600000,dstSavings=3600000,useDaylight=true,transitions=184,lastRule=java.util.SimpleTimeZone[id=Europe/Paris,offset=3600000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=2,startMonth=2,startDay=-1,startDayOfWeek=1,startTime=3600000,startTimeMode=2,endMode=2,endMonth=9,endDay=-1,endDayOfWeek=1,endTime=3600000,endTimeMode=2]]
Input date String: 2016-08-01T08:00:00 WET
Date.toString() Mon Aug 01 10:00:00 CEST 2016
Output date String: 2016-08-01T10:00:00 CEST

您使用麻烦的旧式日期时间 class 来折磨自己,现在已被 java.time 框架淘汰。

tl;博士

ZonedDateTime.ofInstant( Instant.parse( "2016-08-01T00:00:00Z" ) , ZoneId.of( "Europe/Paris" ) )

java.time

java.time framework is built into Java 8 and later. These classes supplant the old troublesome date-time classes such as java.util.Date, .Calendar, & java.text.SimpleDateFormat. The Joda-Time 团队还建议迁移到 java.time。

要了解更多信息,请参阅 Oracle Tutorial。并在 Stack Overflow 中搜索许多示例和解释。

许多 java.time 功能被移植到 ThreeTen-Backport and further adapted to Android in ThreeTenABP 中的 Java 6 和 7。

Instant

输入字符串 2016-08-01T00:00:00Z 末尾的 ZZulu 的缩写,意思是 UTC。在 java.time 中由 Instant class 表示,分辨率高达纳秒。

Instant instant = Instant.parse ( "2016-08-01T00:00:00Z" );

ZonedDateTime

通过 ZoneId 指定时区以获得 ZonedDateTime

在夏季,Paris time 比 UTC 早两个小时。因此,在巴黎墙上的时钟上看到的同一时刻是 8 月 1 日同一天的凌晨 2 点,而不是午夜。

ZoneId zoneId_Paris = ZoneId.of ( "Europe/Paris" );
ZonedDateTime zdt_Paris = ZonedDateTime.ofInstant ( instant , zoneId_Paris );

您可以调整到另一个时区,例如Atlantic/Canary。对于夏季的这个日期,时间比 UTC 早一小时。结果是凌晨 1 点而不是午夜。

ZoneId zoneId_Canary = ZoneId.of ( "Atlantic/Canary" );
ZonedDateTime zdt_Canary = zdt_Paris.withZoneSameInstant ( zoneId_Canary );

转储到控制台。

System.out.println ( "instant: " + instant + " | zdt_Paris: " + zdt_Paris + " | zdt_Canary: " + zdt_Canary );

instant: 2016-08-01T00:00:00Z | zdt_Paris: 2016-08-01T02:00+02:00[Europe/Paris] | zdt_Canary: 2016-08-01T01:00+01:00[Atlantic/Canary]

所有这三个对象(UTC、巴黎、金丝雀)代表历史上非常相同的同时时刻,时间轴上的相同单点。每一个都是通过不同的 wall-clock time.

的镜头来观察的

实时时区

避免使用 3-4 个字母的区域缩写,例如 WETWEST。这些是不是实时时区,不是标准化的,甚至不是唯一的(!)。

Europe/ParisAtlantic/Canary正确的时区名称,格式为continent/region.