如何将 ZonedDateTime 转换为日期?

How to convert ZonedDateTime to Date?

我正在尝试在我的数据库中设置一个与服务器无关的日期时间,我认为这样做的最佳做法是设置一个 UTC 日期时间。我的数据库服务器是 Cassandra,Java 的数据库驱动程序只理解日期类型。

所以假设在我的代码中我正在使用新的 Java 8 ZonedDateTime 现在获取 UTC (ZonedDateTime.now(ZoneOffset.UTC)),我如何将这个 ZonedDateTime 实例转换为 "legacy"日期 class?

您可以使用 Java 8 及更高版本中内置的 java.time 类 来执行此操作。

ZonedDateTime temporal = ...
long epochSecond = temporal.getLong(INSTANT_SECONDS);
int nanoOfSecond = temporal.get(NANO_OF_SECOND);
Date date = new Date(epochSecond * 1000 + nanoOfSecond / 1000000);

如果您只对现在感兴趣,那么只需使用:

Date d = new Date();

您可以将 ZonedDateTime 转换为一个瞬间,您可以直接将其与 Date 一起使用。

Date.from(java.time.ZonedDateTime.now().toInstant());

tl;博士

java.util.Date.from(  // Transfer the moment in UTC, truncating any microseconds or nanoseconds to milliseconds.
    Instant.now() ;   // Capture current moment in UTC, with resolution as fine as nanoseconds.
)

尽管上面的代码没有任何意义。 java.util.DateInstant 都代表 UTC 中的一个时刻,始终以 UTC 表示。上面的代码与以下代码具有相同的效果:

new java.util.Date()  // Capture current moment in UTC.

这里使用 ZonedDateTime 没有任何好处。如果您已经有 ZonedDateTime,请通过提取 Instant.

调整为 UTC
java.util.Date.from(             // Truncates any micros/nanos.
    myZonedDateTime.toInstant()  // Adjust to UTC. Same moment, same point on the timeline, different wall-clock time.
)

其他答案正确

by ssoltanid correctly addresses your specific question, how to convert a new-school java.time object (ZonedDateTime) to an old-school java.util.Date object. Extract the Instant from the ZonedDateTime and pass to java.util.Date.from()

数据丢失

请注意,您将遭受数据丢失,因为Instant自纪元以来跟踪nanoseconds since epoch while java.util.Date tracks milliseconds

您的问题和评论引发了其他问题。

将服务器保持在 UTC

您的服务器通常应将其主机 OS 设置为 UTC 作为最佳做法。在我所知道的 Java 实现中,JVM 选择此主机 OS 设置作为其默认时区。

指定时区

但您永远不应依赖 JVM 当前的默认时区。启动 JVM 时传递的标志可以设置另一个时区,而不是选择主机设置。更糟糕的是:任何应用程序的任何线程中的任何代码都可以随时调用 java.util.TimeZone::setDefault 来更改运行时的默认值!

Cassandra Timestamp 类型

任何 decent database and driver should automatically handle adjusting a passed date-time to UTC for storage. I do not use Cassandra, but it does seem to have some rudimentary support for date-time. The documentation says its Timestamp 类型都是同一纪元(UTC 中 1970 年的第一刻)的毫秒计数。

ISO 8601

此外,Cassandra 在 ISO 8601 standard formats. Fortunately, java.time uses ISO 8601 formats as its defaults for parsing/generating strings. The Instant class’ toString 实现中接受字符串输入会做得很好。

精度:毫秒与纳秒

但首先我们需要将 ZonedDateTime 的纳秒精度降低到毫秒。一种方法是使用毫秒创建一个新的 Instant。幸运的是,java.time 有一些方便的方法来转换毫秒。

示例代码

这是 Java 8 Update 60 中的一些示例代码。

ZonedDateTime zdt = ZonedDateTime.now( ZoneId.of( "America/Montreal" ) );
…
Instant instant = zdt.toInstant();
Instant instantTruncatedToMilliseconds = Instant.ofEpochMilli( instant.toEpochMilli() );
String fodderForCassandra = instantTruncatedToMilliseconds.toString();  // Example: 2015-08-18T06:36:40.321Z

或根据此 Cassandra Java driver doc, you can pass a java.util.Date instance (not to be confused with java.sqlDate)。因此,您可以从上面代码中的 instantTruncatedToMilliseconds 生成 j.u.Date。

java.util.Date dateForCassandra = java.util.Date.from( instantTruncatedToMilliseconds );

如果经常这样做,你可以做一个单行。

java.util.Date dateForCassandra = java.util.Date.from( zdt.toInstant() );

但是创建一个小的实用方法会更整洁。

static public java.util.Date toJavaUtilDateFromZonedDateTime ( ZonedDateTime zdt ) {
    Instant instant = zdt.toInstant();
    // Data-loss, going from nanosecond resolution to milliseconds.
    java.util.Date utilDate = java.util.Date.from( instant ) ;
    return utilDate;
}

请注意所有这些代码与问题中的区别。问题的代码试图将 ZonedDateTime 实例的时区调整为 UTC。但这不是必需的。概念上:

ZonedDateTime = Instant + ZoneId

我们只是提取 Instant 部分,它已经是 UTC 格式(基本上是 UTC 格式,请阅读 class 文档以了解详细信息)。



关于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.

这是一个将当前系统时间转换为 UTC 的示例。它涉及将 ZonedDateTime 格式化为字符串,然后使用 java.text DateFormat 将字符串对象解析为日期对象。

    ZonedDateTime zdt = ZonedDateTime.now(ZoneOffset.UTC);
    final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd HH:mm:ss");
    final DateFormat FORMATTER_YYYYMMDD_HH_MM_SS = new SimpleDateFormat("yyyyMMdd HH:mm:ss");
    String dateStr = zdt.format(DATETIME_FORMATTER);

    Date utcDate = null;
    try {
        utcDate = FORMATTER_YYYYMMDD_HH_MM_SS.parse(dateStr);
    }catch (ParseException ex){
        ex.printStackTrace();
    }

如果您将 ThreeTen backport 用于 Android 并且不能使用较新的 Date.from(Instant instant)(至少需要 API 26),您可以使用:

ZonedDateTime zdt = ZonedDateTime.now();
Date date = new Date(zdt.toInstant().toEpochMilli());

或:

Date date = DateTimeUtils.toDate(zdt.toInstant());

另请阅读

中的建议

我用这个。

public class TimeTools {

    public static Date getTaipeiNowDate() {
        Instant now = Instant.now();
        ZoneId zoneId = ZoneId.of("Asia/Taipei");
        ZonedDateTime dateAndTimeInTai = ZonedDateTime.ofInstant(now, zoneId);
        try {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(dateAndTimeInTai.toString().substring(0, 19).replace("T", " "));
        } catch (ParseException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return null;
    }
}

因为 Date.from(java.time.ZonedDateTime.ofInstant(now, zoneId).toInstant()); 这是行不通的! 如果你运行你的应用程序在你的电脑上,那不是问题。但是如果你 运行 在 AWS 或者 Docker 或 GCP 的任何区域,它就会产生问题。因为计算机不是您在 Cloud 上的时区。您应该在代码中设置正确的时区。例如,Asia/Taipei。然后它将在 AWS 或 Docker 或 GCP 中更正。

public class App {
    public static void main(String[] args) {
        Instant now = Instant.now();
        ZoneId zoneId = ZoneId.of("Australia/Sydney");
        ZonedDateTime dateAndTimeInLA = ZonedDateTime.ofInstant(now, zoneId);
        try {
            Date ans = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(dateAndTimeInLA.toString().substring(0, 19).replace("T", " "));
            System.out.println("ans="+ans);
        } catch (ParseException e) {
        }
        Date wrongAns = Date.from(java.time.ZonedDateTime.ofInstant(now, zoneId).toInstant());
        System.out.println("wrongAns="+wrongAns);
    }
}

对于像 beehuang 评论的 docker 应用程序,你应该设置你的时区。

或者您可以使用 withZoneSameLocal。例如:

2014-07-01T00:00+02:00[GMT+02:00]转换为

Date.from(zonedDateTime.withZoneSameLocal(ZoneId.systemDefault()).toInstant())

2014 年 7 月 1 日星期二 00:00:00 CEST 以及

Date.from(zonedDateTime.toInstant())

6 月 30 日星期一 22:00:00 UTC 2014

已接受的答案对我不起作用。返回的日期始终是本地日期,而不是原始时区的日期。我住在 UTC+2。

//This did not work for me
Date.from(java.time.ZonedDateTime.now().toInstant()); 

我想出了两种从 ZonedDateTime 获取正确日期的替代方法。

假设你有这个夏威夷的 ZonedDateTime

LocalDateTime ldt = LocalDateTime.now();
ZonedDateTime zdt = ldt.atZone(ZoneId.of("US/Hawaii"); // UTC-10

或最初要求的 UTC

Instant zulu = Instant.now(); // GMT, UTC+0
ZonedDateTime zdt = zulu.atZone(ZoneId.of("UTC"));

选项 1

我们可以使用java.sql.Timestamp。这很简单,但它也可能会影响您的编程完整性

Date date1 = Timestamp.valueOf(zdt.toLocalDateTime());

备选方案 2

我们从毫秒创建日期(之前回答 here)。请注意,本地 ZoneOffset 是必须的。

ZoneOffset localOffset = ZoneOffset.systemDefault().getRules().getOffset(LocalDateTime.now());
long zonedMillis = 1000L * zdt.toLocalDateTime().toEpochSecond(localOffset) + zdt.toLocalDateTime().getNano() / 1000000L;
Date date2 = new Date(zonedMillis);