Java 以微秒或纳秒精度解析日期

Java date parsing with microsecond or nanosecond accuracy

根据 SimpleDateFormat class documentationJava 在其日期模式中不支持超过毫秒的时间粒度。

所以,像

这样的日期字符串

通过模式解析时

实际上将 . 符号后的整数解释为(将近 10 亿!)毫秒而不是纳秒,从而得到日期

即提前 11 天。令人惊讶的是,使用较少数量的 S 符号仍然会导致解析所有 9 位数字(而不是最左边的 3 位 .SSS)。

有两种方法可以正确处理这个问题:

是否可以通过向标准 SimpleDateFormat 实现提供模式而不进行任何其他代码修改或字符串操作来获得正确的解决方案?

tl;博士

LocalDateTime.parse(                 // With resolution of nanoseconds, represent the idea of a date and time somewhere, unspecified. Does *not* represent a moment, is *not* a point on the timeline. To determine an actual moment, place this date+time into context of a time zone (apply a `ZoneId` to get a `ZonedDateTime`). 
    "2015-05-09 00:10:23.999750900"  // A `String` nearly in standard ISO 8601 format.
    .replace( " " , "T" )            // Replace SPACE in middle with `T` to comply with ISO 8601 standard format.
)                                    // Returns a `LocalDateTime` object.

没有

不,您不能使用 SimpleDateFormat 来处理 nanoseconds

但是你的前提是……

Java does not support time granularity above milliseconds in its date patterns

…从 Java 8, 9, 10 and later with java.time classes built-in. And not really true of Java 6 and Java 7 either, as most of the java.time functionality is back-ported 开始不再正确。

java.time

SimpleDateFormat 和相关的 java.util.Date/.Calendar class 现在已被新的 java.time package found in Java 8 (Tutorial 淘汰。

新的 java.time classes 支持 nanosecond 分辨率。该支持包括解析和生成九位小数秒。例如,当您使用 java.time.format DateTimeFormatter API 时,S 模式字母表示 "fraction of the second" 而不是 "milliseconds",它可以应对具有纳秒值。

Instant

例如,Instant class represents a moment in UTC. Its toString method generates a String object using the standard ISO 8601格式。末尾的 Z 表示 UTC,发音为“Zulu”。

instant.toString()  // Generate a `String` representing this moment, using standard ISO 8601 format.

2013-08-20T12:34:56.123456789Z

请注意,在 Java 8 中捕获当前时刻仅限于毫秒分辨率。 java.time classes可以保持纳秒级的值,但只能确定当前时间的毫秒级。此限制是由于 Clock 的实施。在 Java 9 及更高版本中,新的 Clock 实现可以更精细的分辨率抓取当前时刻,具体取决于您的主机硬件和操作系统的限制,根据我的经验通常 microseconds

Instant instant = Instant.now() ;  // Capture the current moment. May be in milliseconds or microseconds rather than the maximum resolution of nanoseconds.

LocalDateTime

您的示例输入字符串 2015-05-09 00:10:23.999750900 缺少时区指示符或与 UTC 的偏移量。这意味着它不是代表一个时刻,不是时间轴上的一个点。相反,它表示 潜在的 时刻,时间跨度约为 26-27 小时,即全球时区范围。

将这样的输入分解为 LocalDateTime 对象。首先,将中间的SPACE替换为一个T以符合ISO 8601格式,在parsing/generating字符串时默认使用。因此无需指定格式模式。

LocalDateTime ldt = 
        LocalDateTime.parse( 
            "2015-05-09 00:10:23.999750900".replace( " " , "T" )  // Replace SPACE in middle with `T` to comply with ISO 8601 standard format.
        ) 
;

java.sql.Timestamp

java.sql.Timestamp class 也处理纳秒级分辨率,但以一种笨拙的方式。通常最好在 java.time classes 内完成工作。从 JDBC 4.2 及更高版本开始,无需再次使用 Timestamp

myPreparedStatement.setObject( … , instant ) ;

并检索。

Instant instant = myResultSet.getObject( … , Instant.class ) ;

OffsetDateTime

Instant 规范未强制支持 JDBC,但 OffsetDateTime 是。因此,如果上述代码在您的 JDBC 驱动程序中失败,请使用以下代码。

OffsetDateTime odt = instant.atOffset( ZoneOffset.UTC ) ; 
myPreparedStatement.setObject( … , odt ) ;

和检索。

Instant instant = myResultSet.getObject( … , OffsetDateTime.class ).toInstant() ;

如果使用较旧的 pre-4.2 JDBC 驱动程序,您可以使用 toInstant and from 方法在 java.sql.Timestamp 和 java.time 之间来回切换。这些新的转换方法已添加到 旧的 遗留 classes。


关于java.time

java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

要了解更多信息,请参阅 Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310

您可以直接与数据库交换 java.time 对象。使用 JDBC driver compliant with JDBC 4.2 或更高版本。不需要字符串,不需要 java.sql.* classes.

从哪里获得java.time classes?

ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.

我发现像这样涵盖日期时间格式的多种变体非常棒:

final DateTimeFormatterBuilder dtfb = new DateTimeFormatterBuilder();
dtfb.appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSSSSS"))
.appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSSSS"))
.appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSSS"))
.appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS"))
.appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSS"))
.appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSS"))
    .appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"))
    .appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SS"))
    .appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.S"))
    .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
    .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
    .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0);