java.text.ParseException: 无法解析日期 "yyyy-MM-dd'T'HH:mm:ss.SSSZ" - SimpleDateFormat

39

我需要帮助找出这个异常的错误:

java.text.ParseException: Unparseable date: "2007-09-25T15:40:51.0000000Z"

以下是代码:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
Date date = sdf.parse(timeValue);
long mills = date.getTime();
this.point.time = String.valueOf(mills);

使用 Date date = sdf.parse(timeValue); 会抛出异常。

timeValue = "2007-09-25T15:40:51.0000000Z";,就像异常中所示。

谢谢。


你是否需要解析 .SSSZ?如果你只需要日期或时间,那么去掉 .SSSZ 即可。 - IgorGanapolsky
2
对于这个问题的新读者,我建议您不要使用SimpleDateFormatDate。这些类设计得很差,而且已经过时很久了,前者尤其令人头疼。相反,只需使用来自java.time,现代Java日期和时间APIInstant即可。 - Ole V.V.
除了@OleV.V.所建议的之外,还要注意'Z'不同于Z - Arvind Kumar Avinash
3个回答

90

Z代表时区字符。它需要用引号括起来:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");

3
或者考虑使用X代替Z,这样Z就能被接受为ISO8601时区标识符,并且"Z"将被解析为UTC时区标识符。 - DNA
使用 X 对我来说很有效,但似乎需要在模式中精确指定 S(毫秒)字符的数量,这很奇怪 - 请看我的回答... - DNA
它在javadoc中。文本可以使用单引号(')进行引用,以避免解释。 - Reimeus
1
@Reimeus 这个解决方案对我没用。我尝试了“Z”,但它没有被解析。只有当我删除Z时才起作用。 - IgorGanapolsky
1
这个答案有两个错误:(1)Z表示UTC。引用Z会导致SimpleDateFormat使用JVM默认时区,从而产生高达14小时的错误。(2)SSS表示毫秒。问题中的字符串2007-09-25T15:40:51.0000000Z有10位小数。当它们为0时,结果当然是相同的,但是你的格式化程序解析例如2007-09-25T15:40:51.5000000Z为Tue Sep 25 17:04:11 CEST 2007,比不正确的时区多出一个多小时的错误。 - Ole V.V.
显示剩余2条评论

8

(感谢评论中的更正,现在答案进行了广泛修订)

Java 7中,您可以使用X模式来匹配ISO8601时区,其中包括特殊的Z(UTC)值。

X模式还支持显式时区,例如+01:00

这种方法正确地处理时区指示器,并避免了将其仅视为字符串并因此在本地时区而不是UTC或其他时区错误解析时间戳的问题。

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX");
Date date = sdf.parse("2007-09-25T15:40:51Z");
Date date2 = sdf.parse("2007-09-25T15:40:51+01:00");

这也可以与毫秒一起使用:

SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX");
Date date3 = sdf2.parse("2007-09-25T15:40:51.500Z");

然而,正如其他人指出的那样,您的格式有7位小数秒,这些小数秒可能是十分之一微秒。如果是这样,SimpleDateFormat无法处理它,您将获得不正确的结果,因为每个0.1微秒将被解释为一毫秒,从而可能导致总体误差高达10,000秒(几个小时)。在极端情况下,如果小数秒值为0.9999999秒,则会错误地解释为9999999毫秒,约为167分钟或2.8小时。
// Right answer, error masked for zero fractional seconds 
Date date6 = sdf2.parse("2007-09-25T15:40:51.0000000Z");
// Tue Sep 25 15:40:51 GMT 2007

// Error - wrong hour
// Should just half a second different to the previous example
Date date5 = sdf2.parse("2007-09-25T15:40:51.5000000Z");
// Tue Sep 25 17:04:11 GMT 2007

这个错误在小数秒为零时会被隐藏,就像你的例子一样,但是当它们不为零时,它就会表现出来。
通过关闭“宽松”解析(默认情况下接受超过一秒的小数部分并将其传递到秒/分/小时部分),可以在许多情况下检测到此错误并减少其影响:
sdf2.setLenient(false);
sdf2.parse("2007-09-25T15:40:51.5000000Z");
// java.text.ParseException: Unparseable date: "2007-09-25T15:40:51.5000000Z"

这将捕获毫秒值大于999的情况,但不检查数字的数量,因此仅是对毫秒/微秒不匹配的部分和间接保护措施。然而,在许多实际数据集中,这将捕获大量错误,并因此指示根本问题,即使一些值滑过去了。
我建议除非您有特定的需求,否则始终禁用宽松解析,因为它会捕获许多错误,否则这些错误将被悄悄隐藏并传播到下游数据中。
如果您的小数秒始终为零,则可以在此处使用其中一种解决方案,但有风险,即如果稍后在非零小数秒上使用该代码,则它们将无法工作。您可能希望记录此事并/或断言该值为零,以避免以后出现的错误。
否则,您可能需要将小数秒转换为毫秒,以便SimpleDateFormat可以正确解释它们。或使用较新的日期时间API之一。

IllegalArgumentException: Unknown pattern character 'X' - IgorGanapolsky
1
Igor - 你使用的Java版本是哪个出现了那个错误?'X'模式在Java 7中已经有明确的文档说明,并且在Java 8下也可以正常工作。 - DNA
我在我的Android项目(Java8)中使用了ThreeTenABP库。 - IgorGanapolsky
2
@IgorGanapolsky 听起来你需要发一篇新的问题,包括你的代码和环境的完整细节。 - Reimeus
1
同意,我会尝试澄清我所说的“在许多情况下”的含义(即millis> 999的情况)。 - DNA
显示剩余3条评论

1

java.time

我建议您在处理日期和时间时使用现代的Java日期和时间API - java.time。您的字符串采用ISO 8601格式,可以直接由java.time.Instant类解析,无需指定任何格式器:

    String timeValue = "2007-09-25T15:40:51.0000000Z";
    
    Instant i = Instant.parse(timeValue);
    long mills = i.toEpochMilli();
    String time = String.valueOf(mills);
    
    System.out.println(time);

输出:

1190734851000

如有需要,可以使用格式化程序进行输出

如果我们确定毫秒值永远不会是负数,则java.time可以将其格式化为字符串。这样可以避免首先显式转换为毫秒。

private static final DateTimeFormatter EPOCH_MILLI_FORMATTER
        = new DateTimeFormatterBuilder().appendValue(ChronoField.INSTANT_SECONDS)
                .appendValue(ChronoField.MILLI_OF_SECOND, 3)
                .toFormatter(Locale.ROOT);

现在格式化已经很简单了:
    assert ! i.isBefore(Instant.EPOCH) : i;
    String time = EPOCH_MILLI_FORMATTER.format(i);

输出结果仍然相同:

1190734851000

特别是如果您需要在程序中的多个位置将Instant对象格式化为字符串,我推荐后一种方法。

您的代码出了什么问题?

首先,SimpleDateFormat无法正确解析7位小数秒。只要小数部分为零,结果就可能正确,但想象一下,在整秒之后只有1/10秒的时间,例如2007-09-25T15:40:51.1000000Z。在这种情况下,SimpleDateFormat会将小数部分解析为一百万毫秒,导致结果偏差超过15分钟。对于更大的小数部分,误差可能达到几小时。

其次,正如其他人所说,格式模式字母Z与UTC或距离UTC的零偏移量不匹配。这导致了您观察到的异常。按照已接受答案中建议的方式将Z放在引号中也是错误的,因为它会使您错过来自字符串的关键信息,再次导致几小时的错误(在大多数时区)。

链接

Oracle教程:日期时间,介绍如何使用java.time。


我推荐这个答案。 - Arvind Kumar Avinash

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接