如何使用LocalDateTime解析/格式化日期?(Java 8)

476

Java 8新增了一个名为java.time的API,用于处理日期和时间(JSR 310)。

我有一个字符串类型的日期和时间(例如"2014-04-08 12:30"),如何从给定的字符串中获取一个LocalDateTime实例?

当我完成操作LocalDateTime对象之后:如何将LocalDateTime实例转换回与上述相同的字符串格式的字符串?


16
大多数时候,人们希望使用ZonedDateTime而不是LocalDateTime,尽管名称似乎有些反直觉;Local一词表示一般的“任何”地方,而不是特定的时区。 因此,LocalDateTime对象与时间轴没有关联。 要具有意义并获得时间轴上的指定时刻,必须应用一个时区。 - Basil Bourque
请查看我的答案,了解LocalDateTimeZonedDateTimeOffsetDateTimeInstantLocalDateLocalTime之间的区别,以及如何保持冷静并在第一次尝试时正确地处理它们。 - Ondra Žižka
6
如果名称不会过于冗长,LocalDateTime 可能被命名为 ZonelessOffsetlessDateTime - Ondra Žižka
12个回答

740

解析日期和时间

要从字符串创建一个LocalDateTime对象,您可以使用静态的LocalDateTime.parse()方法。它需要一个字符串和一个DateTimeFormatter作为参数。 DateTimeFormatter用于指定日期/时间模式。

String str = "1986-04-08 12:30";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
LocalDateTime dateTime = LocalDateTime.parse(str, formatter);

格式化日期和时间

要从一个LocalDateTime对象创建格式化字符串,可以使用format()方法。

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
LocalDateTime dateTime = LocalDateTime.of(1986, Month.APRIL, 8, 12, 30);
String formattedDateTime = dateTime.format(formatter); // "1986-04-08 12:30"
请注意,在DateTimeFormatter中预定义了一些常用的日期/时间格式。例如:使用DateTimeFormatter.ISO_DATE_TIME对上述示例中的LocalDateTime实例进行格式化,将生成字符串"1986-04-08T12:30:00"
所有与日期/时间相关的对象(例如LocalDateZonedDateTime)都可以使用parse()format()方法。

85
请注意,DateTimeFormatter是不可变的且线程安全的。因此,建议将其存储在静态常量中(如果可能的话)。 - JodaStephen
25
Õ░ØÞ»òõ¢┐þö¿DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX")ÒÇé - Ray Hulha
1
@Loenix 可能是因为你试图在 LocalDateTime 类上调用 format() 而不是在实例上调用?至少,这就是我所犯的错误:在上面的示例中,我混淆了 DateTimedateTime - glaed
2
不要忘记在 MM 上使用大写字母。 - Wesos de Queso
1
@AJW 我下定决心,将那些从DateLocalDate及其相关类的位重新编写了一遍。 - Dragas
显示剩余3条评论

210
你可以在不提供格式的情况下,使用LocalDate.parse()LocalDateTime.parse()来解析一个ISO 8601 format格式的String
例如,
String strDate = "2015-08-04";
LocalDate aLD = LocalDate.parse(strDate);
System.out.println("Date: " + aLD);

String strDatewithTime = "2015-08-04T10:11:30";
LocalDateTime aLDT = LocalDateTime.parse(strDatewithTime);
System.out.println("Date with Time: " + aLDT);

输出,

Date: 2015-08-04
Date with Time: 2015-08-04T10:11:30

只有在需要处理其他日期格式时,才使用DateTimeFormatter

例如,在以下示例中,dd MMM uuuu表示月份的日期(两位数字),月份名称的三个字母(Jan、Feb、Mar等)和四位数的年份:

DateTimeFormatter dTF = DateTimeFormatter.ofPattern("dd MMM uuuu");
String anotherDate = "04 Aug 2015";
LocalDate lds = LocalDate.parse(anotherDate, dTF);
System.out.println(anotherDate + " parses to " + lds);

输出

04 Aug 2015 parses to 2015-08-04

请记住,DateTimeFormatter 对象是双向的;它既可以解析输入,也可以格式化输出。
String strDate = "2015-08-04";
LocalDate aLD = LocalDate.parse(strDate);
DateTimeFormatter dTF = DateTimeFormatter.ofPattern("dd MMM uuuu");
System.out.println(aLD + " formats as " + dTF.format(aLD));

输出

2015-08-04 formats as 04 Aug 2015

(查看完整的日期格式和解析格式的格式模式列表。)
  Symbol  Meaning                     Presentation      Examples
  ------  -------                     ------------      -------
   G       era                         text              AD; Anno Domini; A
   u       year                        year              2004; 04
   y       year-of-era                 year              2004; 04
   D       day-of-year                 number            189
   M/L     month-of-year               number/text       7; 07; Jul; July; J
   d       day-of-month                number            10

   Q/q     quarter-of-year             number/text       3; 03; Q3; 3rd quarter
   Y       week-based-year             year              1996; 96
   w       week-of-week-based-year     number            27
   W       week-of-month               number            4
   E       day-of-week                 text              Tue; Tuesday; T
   e/c     localized day-of-week       number/text       2; 02; Tue; Tuesday; T
   F       week-of-month               number            3

   a       am-pm-of-day                text              PM
   h       clock-hour-of-am-pm (1-12)  number            12
   K       hour-of-am-pm (0-11)        number            0
   k       clock-hour-of-am-pm (1-24)  number            0

   H       hour-of-day (0-23)          number            0
   m       minute-of-hour              number            30
   s       second-of-minute            number            55
   S       fraction-of-second          fraction          978
   A       milli-of-day                number            1234
   n       nano-of-second              number            987654321
   N       nano-of-day                 number            1234000000

   V       time-zone ID                zone-id           America/Los_Angeles; Z; -08:30
   z       time-zone name              zone-name         Pacific Standard Time; PST
   O       localized zone-offset       offset-O          GMT+8; GMT+08:00; UTC-08:00;
   X       zone-offset 'Z' for zero    offset-X          Z; -08; -0830; -08:30; -083015; -08:30:15;
   x       zone-offset                 offset-x          +0000; -08; -0830; -08:30; -083015; -08:30:15;
   Z       zone-offset                 offset-Z          +0000; -0800; -08:00;

   p       pad next                    pad modifier      1

   '       escape for text             delimiter
   ''      single quote                literal           '
   [       optional section start
   ]       optional section end
   #       reserved for future use
   {       reserved for future use
   }       reserved for future use

14
这个回答涉及到一个重要的主题:尽可能使用预定义的格式化程序,比如不要基于“yyyy-MM-dd”创建格式化程序,而是使用DateTimeFormatter.ISO_LOCAL_DATE。这会让你的代码看起来更整洁。此外,请尽量最大化使用ISO8061格式,这将在长期内产生好处。 - Christopher Yang
我想解析一个日期以进行验证,例如 2018-08-09 12:00:08,但是当我解析时,会添加一个我不需要的 T。有没有什么方法可以解决这个问题? - Raghuveer
@Raghuveer,T只是日期和时间之间的ISO-8061分隔符。如果您的格式中有空格,您可以使用模式“yyyy-MM-dd hh:mm:ss”进行解析和格式化。 T将始终显示在默认(ISO-8061)格式中,但您可以使用自己的模式。 - Egor Hans

52

Sufiyan Ghori的micha的答案都很好地解释了关于字符串模式的问题。然而,如果你正在使用ISO 8601,则无需应用DateTimeFormatter,因为LocalDateTime已经为此做好了准备:

将LocalDateTime转换为时区ISO 8601字符串

LocalDateTime ldt = LocalDateTime.now();
ZonedDateTime zdt = ldt.atZone(ZoneOffset.UTC); // You might use a different zone
String iso8601 = zdt.toString();

将ISO8601字符串转换回LocalDateTime
String iso8601 = "2016-02-14T18:32:04.150Z";
ZonedDateTime zdt = ZonedDateTime.parse(iso8601);
LocalDateTime ldt = zdt.toLocalDateTime();

43
将带有日期和时间的字符串解析为特定的时间点(Java称之为“Instant”)相当复杂。 Java已经在多个迭代中解决了这个问题。最新的迭代中,java.timejava.time.chrono几乎涵盖了所有需求(除了time dilation :))。
然而,这种复杂性带来了很多困惑。
理解日期解析的关键在于:

为什么Java有这么多解析日期的方式?

  1. 有几种方法可以测量时间。例如,历史上的日本日历是根据各自皇帝或王朝的统治时间范围推导出来的。然后还有Unix时间戳等。 幸运的是,整个(商业)世界都设法使用相同的时间。
  2. 从历史上看,系统因各种原因而被切换。例如,从儒略历格里高利历在1582年进行了切换;因此,那之前的“西方”日期需要以不同的方式处理。
  3. 当然,这种变化并没有一次性发生。因为日历来自某些宗教的总部,欧洲的其他地区信仰其他神灵,例如德国直到1700年才开始转变。

...为什么LocalDateTimeZonedDateTime等如此复杂?

4. 有时区。 时区基本上是地球表面的一条“条纹”[3],其当局遵循相同的规则,即何时具有哪个时间偏移量。这包括夏令时规则。
时区随时间而变化,主要基于谁征服了谁。一个时区的规则会随时间改变
5. 有时间偏移量。这与时区不同,因为时区可能是例如“布拉格”,但它具有夏季时间偏移和冬季时间偏移。
如果您获取带有时区的时间戳,则偏移量可能会有所不同,具体取决于一年中的哪个部分。在闰小时,时间戳可能意味着两个不同的时间,因此如果没有其他信息,则无法可靠地转换。
注:通过时间戳,我指的是“包含日期和/或时间(可选带有时区和/或时间偏移量)的字符串”。
6. 在某些时期,几个时区可能共享相同的时间偏移量。例如,当夏季时间偏移量未生效时,GMT / UTC时区与“伦敦”时区相同。
7. 科学家观察地球的动态变化,根据这一点,在每年末尾添加秒数(因此2040-12-31 24:00:00可能是有效的日期时间)。这需要定期更新系统用于正确转换日期的元数据。例如,在Linux上,您会得到包括这些新数据的Java软件包的定期更新。
8. 更新并不总是保持先前的行为,对于历史和未来的时间戳都是如此。因此,在运行不同版本软件时,比较某些时区更改周围的两个时间戳可能会给出不同的结果。这也适用于在受影响的时区和其他时区之间进行比较。如果这导致您的软件出现错误,请考虑使用一些没有这种复杂规则的时间戳,例如Unix时间戳
9. 因为第7点原因,对于未来的日期,我们无法确切地进行日期转换。因此,例如,当前解析的8524-02-17 12:00:00与未来解析可能相差几秒钟。
10. JDK的API随着当代需求而发展。
  • 早期的Java版本只有java.util.Date,它采用了有点天真的方法,假定只有年、月、日和时间。这很快就不够用了。
  • 此外,数据库的需求也不同,因此在相当早的时候,引入了java.sql.Date,具有自己的限制。
  • 由于两者都无法很好地涵盖不同的日历和时区,因此引入了Calendar API。
  • 这仍然无法涵盖时区的复杂性。然而,上述API的混合使用确实很麻烦。因此,随着Java开发人员开始开发全球Web应用程序,针对大多数用例的库(如JodaTime)迅速流行起来。JodaTime成为事实上的标准约十年。
  • 但是JDK没有与JodaTime集成,因此使用它有点麻烦。因此,在对如何处理此问题进行长时间讨论后,JSR-310创建了,主要基于JodaTime

如何在Java的java.time中处理它

确定将时间戳解析为哪种类型

当您使用时间戳字符串时,需要知道它包含的信息。 这是关键点。 如果您没有正确理解,就会出现加密异常,例如“无法创建Instant”、“Zone offset missing”、“unknown zone id”等。

它是否包含日期和时间?

  1. 它是否有时间偏移量? 时间偏移量是+hh:mm部分。有时,+00:00可能会被替换为'Zulu time',UTC作为协调世界时,或GMT作为格林威治平均时间。这些也设置了时区。 对于这些时间戳,您可以使用OffsetDateTime

  2. 它是否有时区? 对于这些时间戳,您可以使用ZonedDateTime。 时区可以通过以下方式指定

    • 名称(“Prague”,“Pacific Standard Time”,“PST”)或
    • “区域ID”(“America/Los_Angeles”,“Europe/London”),由java.time.ZoneId表示。

    时区列表由"TZ database"编制,由ICAAN支持。

    根据ZoneId的javadoc,时区id也可以以某种方式指定为Z和偏移量。我不确定这如何映射到实际区域。

    如果只有TZ的时间戳落在时间偏移量更改的闰秒中,则存在歧义,并且解释取决于ResolverStyle,请参见下文。

  3. 如果两者都没有,则假定或忽略缺失的上下文。消费者必须决定。因此,需要将其解析为LocalDateTime并通过添加缺失的信息将其转换为OffsetDateTime

    • 您可以假定它是UTC时间。添加0小时的UTC偏移量。
    • 您可以假定它是发生转换的地方的时间。通过添加系统的时区进行转换。
    • 您可以忽略并直接使用它。这在比较或减去两个时间(请参阅Duration)或当您不知道它并且并不真正重要时(例如,当地公交车时间表)非常有用。

部分时间信息

  • 根据时间戳所包含的内容,您可以从中获取LocalDateLocalTimeOffsetTimeMonthDayYearYearMonth

如果您拥有完整的信息,则可以获得一个java.time.Instant。这也在内部用于在OffsetDateTimeZonedDateTime之间进行转换。

弄清如何解析它

有一个广泛的文档DateTimeFormatter,它既可以解析时间戳字符串,也可以格式化为字符串。

预创建的DateTimeFormatter应该涵盖几乎所有标准的时间戳格式。例如,ISO_INSTANT可以解析2011-12-03T10:15:30.123457Z

如果您有一些特殊格式,那么您可以创建自己的DateTimeFormatter(也是解析器)。

private static final DateTimeFormatter TIMESTAMP_PARSER = new DateTimeFormatterBuilder()
   .parseCaseInsensitive()
   .append(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SX"))
   .toFormatter();

我建议查看{{DateTimeFormatter}}的源代码,并受其启发,使用{{DateTimeFormatterBuilder}}构建一个日期格式化器。在那里,还要查看{{ResolverStyle}},它控制解析器在格式和模糊信息方面是宽松、智能还是严格。
现在,常见的错误是进入{{TemporalAccessor}}的复杂性。这来自于开发人员习惯于使用{{SimpleDateFormatter.parse(String)}}。没错,{{DateTimeFormatter.parse("...")}}会给你一个{{TemporalAccessor}}。
// No need for this!
TemporalAccessor ta = TIMESTAMP_PARSER.parse("2011-... etc");

但是,有了前面部分的知识,您可以方便地解析成所需的类型:

OffsetDateTime myTimestamp = OffsetDateTime.parse("2011-12-03T10:15:30.123457Z", TIMESTAMP_PARSER);

你实际上不需要使用 DateTimeFormatter。你想要解析的类型有 parse(String) 方法。

OffsetDateTime myTimestamp = OffsetDateTime.parse("2011-12-03T10:15:30.123457Z");

关于TemporalAccessor,如果您对字符串中的信息有模糊的了解,并且想在运行时决定使用它。
我希望我能为您的理解提供一些光芒 :)
注意:Java 6和7有ThreeTen-Backport的后移版本。对于Android,它有ThreeTenABP
[3] 不仅它们不是条纹,而且还存在一些奇怪的极端情况。例如,一些邻近太平洋岛屿具有+14:00和-11:00的时区。这意味着,在一个岛上,是5月1日下午3点,在另一个岛上,离得不远,仍然是4月30日中午12点(如果我数对了:))

6

需要注意的另一件事是,LocalDateTime.parse不能与只包含日期格式字符(如uuuuMMdd)的自定义格式化程序一起使用。在这种情况下,应改用LocalDate.parse。例如:

String s = "20210223";
        
// ok
LocalDate.parse(s, DateTimeFormatter.ofPattern("uuuuMMdd"));
        
// java.time.format.DateTimeParseException
LocalDateTime.parse(s, DateTimeFormatter.ofPattern("uuuuMMdd")); 

这就是让我困扰的问题...即使hh:mm:ss都为零,你仍然会得到解析异常,并且只能解析LocalDate部分...非常烦人。 - JonathanDavidArndt

4

获取当前UTC时间并以所需格式显示

// Current the UTC time
OffsetDateTime utc = OffsetDateTime.now(ZoneOffset.UTC);

// Get LocalDateTime
LocalDateTime localDateTime = utc.toLocalDateTime();
System.out.println("*************" + localDateTime);

// Formatted UTC time
DateTimeFormatter dTF = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
System.out.println(" formats as " + dTF.format(localDateTime));

// Get the UTC time for the current date
Date now = new Date();
LocalDateTime utcDateTimeForCurrentDateTime = Instant.ofEpochMilli(now.getTime()).atZone(ZoneId.of("UTC")).toLocalDateTime();
DateTimeFormatter dTF2 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
System.out.println(" formats as " + dTF2.format(utcDateTimeForCurrentDateTime));

3

所有答案都不错。Java 8及以上版本有以下模式用于解析和格式化时区:VzOXxZ

以下是它们的解析规则,根据文档中的规定:

   Symbol  Meaning                     Presentation      Examples
   ------  -------                     ------------      -------
   V       time-zone ID                zone-id           America/Los_Angeles; Z; -08:30
   z       time-zone name              zone-name         Pacific Standard Time; PST
   O       localized zone-offset       offset-O          GMT+8; GMT+08:00; UTC-08:00;
   X       zone-offset 'Z' for zero    offset-X          Z; -08; -0830; -08:30; -083015; -08:30:15;
   x       zone-offset                 offset-x          +0000; -08; -0830; -08:30; -083015; -08:30:15;
   Z       zone-offset                 offset-Z          +0000; -0800; -08:00;

但是关于格式化呢?

以下是针对日期(假设使用ZonedDateTime)的样例,展示不同格式化模式下的行为:

// The helper function:
static void printInPattern(ZonedDateTime dt, String pattern) {
    System.out.println(pattern + ": " + dt.format(DateTimeFormatter.ofPattern(pattern)));
}

// The date:
String strDate = "2020-11-03 16:40:44 America/Los_Angeles";
DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss zzzz");
ZonedDateTime dt = ZonedDateTime.parse(strDate, format);
// 2020-11-03T16:40:44-08:00[America/Los_Angeles]

// Rules:
// printInPattern(dt, "V");     // exception!
printInPattern(dt, "VV");       // America/Los_Angeles
// printInPattern(dt, "VVV");   // exception!
// printInPattern(dt, "VVVV");  // exception!
printInPattern(dt, "z");        // PST
printInPattern(dt, "zz");       // PST
printInPattern(dt, "zzz");      // PST
printInPattern(dt, "zzzz");     // Pacific Standard Time
printInPattern(dt, "O");        // GMT-8
// printInPattern(dt, "OO");    // exception!
// printInPattern(dt, "OO0");   // exception!
printInPattern(dt, "OOOO");     // GMT-08:00
printInPattern(dt, "X");        // -08
printInPattern(dt, "XX");       // -0800
printInPattern(dt, "XXX");      // -08:00
printInPattern(dt, "XXXX");     // -0800
printInPattern(dt, "XXXXX");    // -08:00
printInPattern(dt, "x");        // -08
printInPattern(dt, "xx");       // -0800
printInPattern(dt, "xxx");      // -08:00
printInPattern(dt, "xxxx");     // -0800
printInPattern(dt, "xxxxx");    // -08:00
printInPattern(dt, "Z");        // -0800
printInPattern(dt, "ZZ");       // -0800
printInPattern(dt, "ZZZ");      // -0800
printInPattern(dt, "ZZZZ");     // GMT-08:00
printInPattern(dt, "ZZZZZ");    // -08:00

在正偏移的情况下,+ 符号字符用于所有位置(现在的位置使用 -),并且永远不被省略。
这适用于新的java.time类型。如果您要为java.util.Datejava.util.Calendar使用这些类型,则并非所有类型都能正常工作,因为这些类型已经损坏(因此被标记为过时,请不要使用它们)。

2

让我们来看两个问题,示例字符串"2014-04-08 12:30"

如何从给定的字符串中获取LocalDateTime实例?

import java.time.format.DateTimeFormatter
import java.time.LocalDateTime

final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")

// Parsing or conversion
final LocalDateTime dt = LocalDateTime.parse("2014-04-08 12:30", formatter)

dt 应该允许您进行所有日期时间相关的操作。

那么我如何将 LocalDateTime 实例转换回具有相同格式的字符串?

final String date = dt.format(formatter) 

1
通用方法如下所示。它适用于以下情况:
  • yyyy-MM-dd HH:mm:ss.SSS

  • yyyy-MM-dd HH:mm:ss.S

  • yyyy-MM-dd HH:mm:ss

  • yyyy-MM-dd HH:mm

  • yyyy-MM-dd HH

  • yyyy-MM-dd

    public static final String DATE_FORMAT_YYYY_MM_DD_HH_MM_SS_SSS = "yyyy-MM-dd HH:mm:ss.SSS";
    
    public LocalDateTime stringToLocalDateTime(String s){
        return LocalDateTime.parse(s, DateTimeFormatter.ofPattern(DATE_FORMAT_YYYY_MM_DD_HH_MM_SS_SSS.substring(0, s.length())));
    }
    

0

这个问题已经有很多好的答案了。这个答案展示了如何使用预定义的DateTimeFormatter来构建一个可以解析给定日期时间字符串的DateTimeFormatter

然而,使用这个DateTimeFormatter格式化得到的LocalDateTime将返回一个带有HH:mm:ss格式的时间字符串。为了将时间字符串限制为HH:mm格式,我们仍然需要像其他答案一样使用模式uuuu-MM-dd HH:mm

演示

class Main {
    public static void main(String[] args) {
        DateTimeFormatter dtf = new DateTimeFormatterBuilder()
                .append(DateTimeFormatter.ISO_LOCAL_DATE)
                .appendLiteral(' ')
                .append(DateTimeFormatter.ISO_LOCAL_TIME)
                .toFormatter(Locale.ENGLISH);

        String strDateTime = "2014-04-08 12:30";
        LocalDateTime ldt = LocalDateTime.parse(strDateTime, dtf);
        System.out.println(ldt);

        // However, formatting the obtained LocalDateTime using this DateTimeFormatter
        // will return a string with time in HH:mm:ss format. To restrict the time
        // string to HH:mm format, we still have to use the pattern, uuuu-MM-dd HH:mm as
        // other answers have done.
        String strDateTimeFormatted = ldt.format(dtf);
        System.out.println(strDateTimeFormatted);

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm", Locale.ENGLISH);
        strDateTimeFormatted = ldt.format(formatter);
        System.out.println(strDateTimeFormatted);
    }
}

输出:

2014-04-08T12:30
2014-04-08 12:30:00
2014-04-08 12:30

在线演示

注意:在这里,您可以使用y代替u,但是我更喜欢使用u而不是y

Trail: Date Time了解有关现代日期时间API的更多信息。


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