将符合ISO 8601标准的字符串转换为java.util.Date

802
我正在尝试将一个ISO 8601格式的字符串转换为java.util.Date。如果与Locale一起使用,则我发现模式yyyy-MM-dd'T'HH:mm:ssZ符合ISO8601标准(请参考示例)。
然而,使用java.text.SimpleDateFormat时,我无法将正确格式的字符串2010-01-01T12:00:00+01:00转换。我必须先将其转换为没有冒号的2010-01-01T12:00:00+0100
因此,当前的解决方案是:
SimpleDateFormat ISO8601DATEFORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.GERMANY);
String date = "2010-01-01T12:00:00+01:00".replaceAll("\\+0([0-9]){1}\\:00", "+0$100");
System.out.println(ISO8601DATEFORMAT.parse(date));

这显然不是很好。我是否遗漏了什么或者有更好的解决方案?


回答

感谢JuanZe的评论,我找到了Joda-Time的神奇之处,这里也有描述

因此,解决方案是

DateTimeFormatter parser2 = ISODateTimeFormat.dateTimeNoMillis();
String jtdate = "2010-01-01T12:00:00+01:00";
System.out.println(parser2.parseDateTime(jtdate));

或者更简单地,通过构造函数使用默认解析器:

DateTime dt = new DateTime( "2010-01-01T12:00:00+01:00" ) ;

对我来说,这很好。


281
准备好收到很多"使用JodaTime"的回答... - JuanZe
3
如果DateTimeFormat的API文档是正确的(虽然JoDa的文档可能会有误导、错误或不完整),你在自己的“答案”中使用的模式与ISO8601不兼容。 - jarnbjo
24
我不确定这个是什么时候被添加的,但 'X' 看起来可以解决 SimpleDateFormat 中的问题。模式 "yyyy-MM-dd'T'HH:mm:ssX" 成功地解析了问题中的例子。 - mlohbihler
13
自Java 7以来,'X'已经可用。 - Lars Grammel
4
Java 8 让它变得容易了!以下答案中有 Adam 的一个隐藏宝石:https://dev59.com/wHE95IYBdhLWcg3wrP-w#27479533 - Fabian Kleiser
显示剩余8条评论
31个回答

513
很遗憾,SimpleDateFormat(Java 6及更早版本)可用的时区格式不符合ISO 8601标准。SimpleDateFormat可以理解类似于“GMT +01:00”或“+0100”这样的时区字符串,后者根据RFC#822
即使Java 7添加了对ISO 8601的时区描述符的支持,SimpleDateFormat仍无法正确解析完整的日期字符串,因为它不支持可选部分。
使用正则表达式重新格式化输入字符串当然是一种可能性,但替换规则并不像您的问题中那样简单:
  • 有些时区与UTC不足一小时,因此字符串不一定以“:00”结尾。
  • ISO8601仅允许包含小时数的时区,因此“+01”等同于“+01:00”
  • ISO8601允许使用“Z”表示UTC,而不是“+00:00”。
更简单的解决方案可能是使用JAXB中的数据类型转换器,因为JAXB必须能够根据XML Schema规范解析ISO8601日期字符串。 javax.xml.bind.DatatypeConverter.parseDateTime("2010-01-01T12:00:00Z")将给您一个Calendar对象,如果您需要一个Date对象,您可以简单地在其上使用getTime()方法。
您也可以使用Joda-Time,但我不知道为什么您要费心这样做(更新2022年; 可能是因为整个javax.xml.bind部分缺少Android的javax.xml包)。

19
JAXB解决方案是一个非常有创意的方法!它运行良好,我已经用我的样本进行了测试。然而,对于任何遇到该问题并被允许使用JodaTime的人,我建议使用它,因为它感觉更自然。但你的解决方案不需要额外的库(至少在Java 6中)。 - Ice09
38
这是反向操作:Calendar c = GregorianCalendar.getInstance();c.setTime(aDate);return javax.xml.bind.DatatypeConverter.printDateTime(c); - Alexander Ljungberg
哇,这是一个非常不明显的地方放置如此有用的东西。我已经搜索了几天了。 - gtrak
4
实际上并不简单,因为你需要初始化JAXB数据类型转换器。我最终使用了DatatypeFactory,就像DataTypeConverterImpl内部一样。真是让人头痛。 - gtrak
如果这些都是真的,那么你应该更新维基百科:http://en.wikipedia.org/wiki/ISO_8601#Time_zone_designators,因为它表明你(至少部分地)是错误的。 - straya

320

被Java 7文档所祝福的方式

DateFormat df1 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
String string1 = "2001-07-04T12:08:56.235-0700";
Date result1 = df1.parse(string1);

DateFormat df2 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
String string2 = "2001-07-04T12:08:56.235-07:00";
Date result2 = df2.parse(string2);

你可以在SimpleDateFormat javadoc示例部分找到更多的例子。

更新于02/13/2020:在Java 8中有一种全新的方法来完成这个操作。


7
你的回答帮助我将MongoDB的ISODate转换为本地日期。致意。 - Blue Sky
10
Java为符合ISO 8601标准的格式添加了更多内容。Java推出了全新的日期时间框架,包括对这些格式的内置默认支持。请参见Java 8中的新java.time框架,它受到Joda-Time的启发,取代了麻烦的java.util.Date、.Calendar和SimpleDateFormat类。 - Basil Bourque
2
这难道不意味着您需要提前了解日期格式吗?如果您必须接受 string1string2 但不知道哪个会得到,该怎么办? - Timmmm
21
“Z”需要用引号括起来。 - kervin
11
如果将Z用引号括起来,格式化程序是否仅会寻找字符“Z”,而不是它能代表的所有偏移字符串?如果你的日期字符串正好处于UTC时间,那么似乎只有碰巧使用引号括起来的Z才会起作用。 如果将Z用引号括起来,格式化程序仅会寻找字面上的字符“Z”,而不会将其解释为UTC偏移量。因此,如果你的日期字符串确实在UTC时间下,使用引号括起来的Z才有效。 - spaaarky21
显示剩余8条评论

210

好的,这个问题已经有答案了,但我仍然会提供我的答案。它可能会对某人有所帮助。

我一直在寻找一个适用于Android(API 7)的解决方案。

  • Joda不是一个选择-它很大并且初始化速度很慢。对于特定目的来说,它似乎也是一个过度设计。
  • 涉及javax.xml的答案无法在Android API 7上使用。

最终实现了这个简单的类。它只涵盖了ISO 8601字符串的最常见形式,但在某些情况下应该足够(当你相当确定输入将以格式为准时)。

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

/**
 * Helper class for handling a most common subset of ISO 8601 strings
 * (in the following format: "2008-03-01T13:00:00+01:00"). It supports
 * parsing the "Z" timezone, but many other less-used features are
 * missing.
 */
public final class ISO8601 {
    /** Transform Calendar to ISO 8601 string. */
    public static String fromCalendar(final Calendar calendar) {
        Date date = calendar.getTime();
        String formatted = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
            .format(date);
        return formatted.substring(0, 22) + ":" + formatted.substring(22);
    }

    /** Get current date and time formatted as ISO 8601 string. */
    public static String now() {
        return fromCalendar(GregorianCalendar.getInstance());
    }

    /** Transform ISO 8601 string to Calendar. */
    public static Calendar toCalendar(final String iso8601string)
            throws ParseException {
        Calendar calendar = GregorianCalendar.getInstance();
        String s = iso8601string.replace("Z", "+00:00");
        try {
            s = s.substring(0, 22) + s.substring(23);  // to get rid of the ":"
        } catch (IndexOutOfBoundsException e) {
            throw new ParseException("Invalid length", 0);
        }
        Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(s);
        calendar.setTime(date);
        return calendar;
    }
}

性能注意事项:我每次都实例化新的SimpleDateFormat,以避免在Android 2.1中出现一个bug。如果你和我一样感到惊讶,请参考这个谜题。对于其他Java引擎,您可以将实例缓存到私有静态字段中(使用ThreadLocal,以确保线程安全)。


3
或许这应该成为一个独立问题,有自己的答案? - Thorbear
5
这是我在寻找答案时偶然发现的第一页,所以似乎很合适。对于大多数Java开发者来说,Android并不完全等同于Java。然而,在大多数情况下,两者的工作方式相同,因此许多Android开发人员在寻找相关信息时会搜索“java”。 - wrygiel
我们在聊天室继续讨论吧。 - Marcus Junius Brutus
6
我不得不添加.SSS来表示小数秒,但效果很好,谢谢。为什么你要这样做s = s.substring(0, 22) + s.substring(23); - 我看不出来这样做有什么意义。 - Dori

150

java.time

java.time API(Java 8及更高版本内置)使此过程变得更加容易。

如果您知道输入是以UTC为基础的,例如在末尾使用Z(代表Zulu),则可以使用Instant类进行解析。

java.util.Date date = Date.from( Instant.parse( "2014-12-12T10:39:40Z" ));

如果您的输入可能是另一个偏移量-从UTC值,而不是以Z(Zulu)结尾的UTC,请使用OffsetDateTime类进行解析。
OffsetDateTime odt = OffsetDateTime.parse( "2010-01-01T12:00:00+01:00" );

然后提取一个 Instant,并通过调用 from 方法将其转换为 java.util.Date
Instant instant = odt.toInstant();  // Instant is always in UTC.
java.util.Date date = java.util.Date.from( instant );

15
这个答案有些冗长。根据Java文档,java.util.Date类没有时区概念,因此不需要使用涉及时区的代码,如LocalDateTime、ZoneId和atZone。只需使用以下一行简单的代码即可:java.util.Date date=Date.from(ZonedDateTime.parse("2014-12-12T10:39:40Z").toInstant()); 这行代码将字符串解析为ZonedDateTime对象,然后转换为Instant对象,并通过Date.from方法转换成java.util.Date类型。 - Basil Bourque
7
@BasilBourque 这种写法太复杂了:Date.from(Instant.parse("2014-12-12T10:39:40Z" )); 就够了。 - assylias
7
@assylias 您是正确的,但只有在日期字符串为UTC时才有效,ISO8601允许任何时区... - Adam
3
抱歉,我的错。我没有意识到这个问题比你的例子更普遍。顺便说一下,一个OffsetDateTime就足以解析ISO8601(它不包含时区信息,只有一个偏移量)。 - assylias
2
@assylias 感谢您关于让 Instant 进行解析的评论。虽然对于这个特定的问题来说不够充分,但这是一个值得指出的重要区别。因此,我添加了第二个代码示例。哎呀,刚注意到这不是我的答案;希望 Adam 能够批准。 - Basil Bourque
显示剩余3条评论

79

从Java 8开始,有一种全新的官方支持方式来实现这一点:

    String s = "2020-02-13T18:51:09.840Z";
    TemporalAccessor ta = DateTimeFormatter.ISO_INSTANT.parse(s);
    Instant i = Instant.from(ta);
    Date d = Date.from(i);

2
如果字符串是以带有尾随Z的即时格式表示的,则无需显式指定偏移量。只需使用Instant i = Instant.parse(s);即可。在问题中的字符串中有+01:00,在这种情况下,DateTimeFormatter.ISO_INSTANT不起作用(至少在我的Java 11上不起作用)。 - Ole V.V.
5
您可以使用ISO_OFFSET_DATE_TIME来格式化带有偏移量的日期,例如 +01:00 (https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html#predefined) - Lucas Basquerotto
1
没错,@LucasBasquerotto。虽然没有明确提到格式化程序,但AdamBasil Bourque的答案已经做了类似的事情。 - Ole V.V.
3
无法解析“2020-06-01T14:34:00-05:00”,该字符串是Javascript的toISOString()方法生成的。 - Jose Solorzano

76

简短易懂版

OffsetDateTime.parse ( "2010-01-01T12:00:00+01:00" )

使用java.time Java 8及以上版本中的新java.time包受Joda-Time启发。 OffsetDateTime类表示时间线上有一个offset-from-UTC,但没有时区的瞬间。如果您使用Java 12+,可以像Answer by Arvind Kumar Avinash所述那样使用Instant.parse
OffsetDateTime odt = OffsetDateTime.parse ( "2010-01-01T12:00:00+01:00" );

调用 toString 方法会生成一个标准的 ISO 8601 格式的字符串:

2010-01-01T12:00+01:00

如果想以 UTC 的视角查看相同的值,可以提取一个 Instant 或将偏移量从 +01:00 调整为 00:00
Instant instant = odt.toInstant();  

…或者…

OffsetDateTime odtUtc = odt.withOffsetSameInstant( ZoneOffset.UTC );

如果需要,可以调整时区。时区是一个地区的UTC偏移量值历史记录,具有一组处理异常情况(如夏令时)的规则。因此,尽可能应用时区而不是仅仅使用偏移量。

ZonedDateTime zonedDateTimeMontréal = odt.atZoneSameInstant( ZoneId.of( "America/Montreal" ) );

对于仅包含日期的值,请使用 LocalDate
LocalDate ld = LocalDate.of( 2010 , Month.JANUARY , 1 ) ;

或者:

LocalDate ld = LocalDate.parse( "2010-01-01" ) ;

关于 java.time

java.time 框架内置于 Java 8 及以上版本。这些类替代了老旧的 legacy 日期时间类,如 java.util.DateCalendarSimpleDateFormat

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

Joda-Time项目现在处于维护模式,建议迁移到java.time类。

你可以直接使用 JDBC 4.2 或更高版本的 JDBC driver 与数据库交换 java.time 对象,无需使用字符串或 java.sql.* 类。Hibernate 5 和 JPA 2.2 支持 java.time
在哪里获取 java.time 类?

2
我认为这并不是有帮助的,因为它不能解析ISO 8601日期,只能解析一个非常特定的子集。例如,java.time.OffsetDateTime.parse("2010-01-01")会失败。 - phil294
@phil294 所有的 java.time 类都会根据各自的适用情况解析 ISO 8601 标准格式输入。你的例子 "2010-01-01" 是一个日期,所以将被仅包含日期的类 LocalDate 解析。例如:LocalDate.parse( "2010-01-01" )。你的例子中的类 OffsetDateTime 是用于带有时间、偏移量(UTC 前/后的小时数-分钟数-秒数)的日期。因此,OffsetDateTime 解析具有日期、时间和偏移量的 ISO 8601 格式化输入。例如:OffsetDateTime.parse( "2010-01-01T12:30:00-08:00" )在 IdeOne.com 上实时运行代码 - Basil Bourque

73

无法解析此日期:2015-08-11T13:10:00。我得到了“String index out of range: 19”的错误。查看代码,似乎需要指定毫秒和时区。这些应该是可选的。 - Timmmm
3
引用文件说明,解析格式为: [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh:mm]]。换句话说,毫秒是可选的,但时区是必须的。 - david_p
2
啊,是的,实际上看起来你是对的。不过,我相信ISO8601允许你省略时区,所以它仍然是错误的。不过JodaTime可以解决这个问题:new DateTime("2015-08-11T13:10:00").toDate() - Timmmm
3
那个类现已被弃用,新的是StdDateFormat。除此之外它的功能保持不变。 - JohnEye
ISO8601DateFormat已被弃用,请使用StdDateFormat。 - Dániel Kis

35

针对Java 7版本

你可以参考Oracle文档: http://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html

X - 用于ISO 8601时区

TimeZone tz = TimeZone.getTimeZone("UTC");
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX");
df.setTimeZone(tz);
String nowAsISO = df.format(new Date());

System.out.println(nowAsISO);

DateFormat df1 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX");
//nowAsISO = "2013-05-31T00:00:00Z";
Date finalResult = df1.parse(nowAsISO);

System.out.println(finalResult);

这意味着时区是必需的。根据ISO 8601,它是可选的。秒数等也是如此。因此,这只解析ISO 8601的特定子集。 - Timmmm
3
完美地适用于Java 1.8。 - Thiago Pereira

23

DatatypeConverter 解决方案并不适用于所有虚拟机。以下内容对我有效:

javax.xml.datatype.DatatypeFactory.newInstance().newXMLGregorianCalendar("2011-01-01Z").toGregorianCalendar().getTime()

我发现 Joda 在使用时不能直接套用 (尤其是对于上述带有时区日期的例子,它应该是有效的)


17
我认为我们应该使用:
DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")

对于日期 2010-01-01T12:00:00Z


5
为什么这个答案比其他答案更好,包括得到76个赞成的被采纳答案? - Erick Robertson
3
@ErickRobertson:它是简单易用的,无需转换,灵活性高,大多数人不关心时区。 - TWiStErRob
7
如果你不在意时区,使用时间的意义就不是很大了! - Dori
19
这完全忽略了时区。我之前一直在使用这个,直到意识到这个问题后,我转而使用了JodaTime。 - Joshua Pinter
4
放弃时间区域将最终导致某些错误。 - Bart van Kuik
显示剩余5条评论

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