1. 概述

在这个快速教程中,我们将学习如何解析来自Unix时间戳的日期表示。Unix时间是自1970年1月1日以来经过的秒数。然而,时间戳可以精确到纳秒。因此,我们将探讨可用的工具,并创建一个方法,将任何范围的时间戳转换为Java对象。

2. 旧方法(Java 8之前)

在Java 8之前,我们的最简单选择是DateCalendar类。Date类有一个直接接受毫秒级时间戳的构造函数:

public static Date dateFrom(long input) {
    return new Date(input);
}

使用Calendar,我们需要在getInstance()后调用setTimeInMillis()

public static Calendar calendarFrom(long input) {
    Calendar calendar = Calendar.getInstance();
    calendar.setTimeInMillis(input);
    return calendar;
}

换句话说,我们必须知道输入的时间戳是以秒、纳秒还是介于两者之间的其他精度。然后,我们不得不手动将时间戳转换为毫秒。

3. 新方法(Java 8+)

Java 8引入了Instant类。这个类有一些实用方法,可以从秒和毫秒创建实例。其中的一个方法接受一个纳秒调整参数:

Instant.ofEpochSecond(seconds, nanos);

但我们仍然必须提前知道时间戳的精度。例如,如果我们知道时间戳是纳秒级的,就需要进行一些计算:

public static Instant fromNanos(long input) {
    long seconds = input / 1_000_000_000;
    long nanos = input % 1_000_000_000;

    return Instant.ofEpochSecond(seconds, nanos);
}

首先,我们将时间戳除以十亿得到秒数,然后使用余数获取秒后的部分。

4. 使用Instant的通用解决方案

为了避免额外的工作,让我们创建一个方法,可以将任何输入转换为毫秒,大多数类都可以解析。首先,我们要检查时间戳所在的范围,然后执行计算来提取毫秒。此外,我们将使用科学记数法使条件更易于阅读。

还要记住,时间戳是有符号的,所以我们必须检查正负范围(负时间戳表示它们是从1970年开始倒计时的)。

首先,让我们检查输入是否为纳秒:

private static long millis(long timestamp) {
    if (millis >= 1E16 || millis <= -1E16) {
        return timestamp / 1_000_000;
    }

    // next range checks
}

首先,我们检查它是否在1E16范围内,即一个后面跟着16个零。负值代表1970年之前的日期,所以我们也需要检查它们。然后,我们将值除以一百万以得到毫秒。

类似地,微秒在1E14范围内。这次,我们将除以一千:

if (timestamp >= 1E14 || timestamp <= -1E14) {
    return timestamp / 1_000;
}

当我们的值在1E11-3E10范围内时,我们不需要做任何改变。这意味着我们的输入已经是毫秒级精度:

if (timestamp >= 1E11 || timestamp <= -3E10) {
    return timestamp;
}

最后,如果输入不属于这些范围,那么它必须是秒级的,因此我们需要将其转换为毫秒:

return timestamp * 1_000;

4.1. 将输入标准化为Instant

现在,让我们创建一个方法,使用Instant.ofEpochMilli()从任何精度的输入返回Instant

public static Instant fromTimestamp(long input) {
    return Instant.ofEpochMilli(millis(input));
}

请注意,每次我们除以或乘以值时,都会丢失精度。

4.2. 使用LocalDateTime处理本地时间

Instant表示一个时间点,但如果没有时区信息,它不易读,因为它依赖于我们所在的世界位置。因此,让我们创建一个方法生成本地时间表示。我们将使用UTC来避免测试中的不同结果:

public static LocalDateTime localTimeUtc(Instant instant) {
    return LocalDateTime.ofInstant(instant, ZoneOffset.UTC);
}

现在,我们可以看到,当方法期望特定格式时,使用错误精度可能导致完全不同的日期。首先,让我们传递一个已知正确日期的纳秒级时间戳,但将其转换为微秒并使用我们之前创建的fromNanos()方法:

@Test
void givenWrongPrecision_whenInstantFromNanos_thenUnexpectedTime() {
    long microseconds = 1660663532747420283l / 1000;
    Instant instant = fromNanos(microseconds);
    String expectedTime = "2022-08-16T15:25:32";

    LocalDateTime time = localTimeUtc(instant);
    assertThat(!time.toString().startsWith(expectedTime));
    assertEquals("1970-01-20T05:17:43.532747420", time.toString());
}

当我们使用之前子节创建的fromTimestamp()方法时,这个问题就不会发生:

@Test
void givenMicroseconds_whenInstantFromTimestamp_thenLocalTimeMatches() {
    long microseconds = 1660663532747420283l / 1000;

    Instant instant = fromTimestamp(microseconds);
    String expectedTime = "2022-08-16T15:25:32";

    LocalDateTime time = localTimeUtc(instant);
    assertThat(time.toString().startsWith(expectedTime));
}

5. 总结

在这篇文章中,我们学习了如何使用核心Java类转换时间戳。然后,我们看到了它们可能具有不同的精度级别,以及这如何影响我们的结果。最后,我们创建了一个简单的方法来标准化输入并获得一致的结果。

如往常一样,源代码可在GitHub上找到。