如何将ZonedDateTime转换为Date?

155

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

因此,假设在我的代码中,我使用新的Java 8 ZonedDateTime来获取UTC时间(ZonedDateTime.now(ZoneOffset.UTC)),那么我该如何将这个ZonedDateTime实例转换为“传统”的Date类?


Java中有两个名为“Date”的类,分别是java.util.Datejava.sql.Date - Basil Bourque
9个回答

244

你可以将ZonedDateTime转换为一个瞬时时间,然后直接与Date一起使用。

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

49
不,它将是您的时区系统默认设置下的当前日期。 - user3460409
9
你的问题没有意义——日期没有时区,它只表示某一刻的时间。 - assylias
2
@assylias 实际上,你的说法没有意义。时间存储的格式暗示了一个时区。日期基于UTC,不幸的是,Java做了一些愚蠢的事情,并没有将其视为UTC,而且还将时间视为本地TZ而不是UTC。Data、LocalDateTime和ZonedDateTime存储数据的方式暗示了一个时区。它们的使用方式似乎不存在TZ,这是完全错误的。java.util.Date具有JVM默认值的TZ,隐含地。人们将其视为任何不同的事实(包括它的Java文档!)只是糟糕的人。 - David
10
@David,不是的 - Date是自纪元以来的毫秒数,因此与UTC有关。如果你打印它,将使用默认时区,但Date类不知道用户的时区...例如,请参阅https://docs.oracle.com/javase/tutorial/datetime/iso/legacy.html中的“映射”部分。并且`LocalDateTime`明确不涉及时区 - 这可能会令人感到困惑... - assylias
4
您的回答不必要地涉及了ZonedDateTimejava.time.Instant类是java.util.Date的直接替代,两者都表示UTC中的一个时刻,虽然Instant使用纳秒而不是毫秒的更细分辨率。您的解决方案应该是Date.from( Instant.now() )。或者说,只需使用new Date(),它具有相同的效果,捕获UTC中的当前时刻。 - Basil Bourque
一个点下去了。只需要一段简单的代码就能说明它不起作用。 - Erick Audet

113

简而言之

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.
)

其他答案正确

ssoltanid答案正确地回答了您特定的问题,即如何将新式java.time对象(ZonedDateTime)转换为旧式的java.util.Date对象。从ZonedDateTime中提取Instant并传递给java.util.Date.from()

数据丢失

请注意,您将丢失数据,因为Instant跟踪纳秒纪元以来,而java.util.Date跟踪毫秒自纪元以来。

diagram comparing resolutions of millisecond, microsecond, and nanosecond

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

保持服务器在UTC时区

作为一般最佳实践,您的服务器应将其主机操作系统设置为UTC。在我所知道的Java实现中,JVM会捕捉到这个主机操作系统设置作为其默认时区。

指定时区

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

Cassandra Timestamp类型

任何体面的数据库和驱动程序都应该自动处理传递的日期时间以进行UTC存储的调整。我不使用Cassandra,但它似乎对日期时间有一些基本支持。文档说它的{{link2:Timestamp}}类型是从UTC的1970年第一时刻开始的毫秒计数。

ISO 8601

此外,Cassandra接受ISO 8601标准格式的字符串输入。幸运的是,java.time使用ISO 8601格式作为其解析/生成字符串的默认格式。Instant类的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文档,你可以传递一个java.util.Date实例(不要与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中,请阅读类文档以了解详细信息)。

Table of date-time types in Java, both modern and legacy


关于java.time

java.time框架内置于Java 8及更高版本中。这些类替代了老旧的遗留日期时间类,如java.util.Date, CalendarSimpleDateFormat

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

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

您可以直接与数据库交换java.time对象。使用符合JDBC 4.2或更高版本的JDBC驱动程序。不需要字符串,也不需要java.sql.*类。

如何获取java.time类?

Table of which java.time library to use with which version of Java or Android

ThreeTen-Extra项目扩展了java.time的附加类。该项目是java.time可能未来添加的可靠性测试。您可能会在这里找到一些有用的类,例如Interval, YearWeek, YearQuarter更多


非常棒的讲解,详细清晰。非常感谢! - Gianmarco F.
感谢@Basil Bourque提供的完整答案。 当返回列表或分页并逐个循环遍历该列表项以将其从一个区域(假设为UTC)转换为另一个区域时,性能是否会变差? - java dev
@devloper152,你是在询问使用时区调整时间的java.time性能吗?自己试试吧。我相信你会发现它非常快速。我怀疑你不会发现java.time成为瓶颈。 - Basil Bourque
我很高兴收到你的回复,我已经尝试过了。先生,当在后端或前端(Angular)中将一个区域转换为另一个区域时,您有什么看法? - java dev
@devloper152 我不使用Angular。我完全使用纯Java构建Web应用程序,使用的是Vaadin Flow - Basil Bourque
你是时间转换的专家 :) 干杯 - Arar

5

如果您正在为Android使用ThreeTen backport,并且无法使用较新的Date.from(Instant instant)(需要最低API 26),则可以使用以下方法:

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

或者:

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

请阅读Basil Bourque的回答中的建议。


1
ThreeTen Backport(和 ThreeTenABP)包括一个 DateTimeUtils 类,其中包含转换方法,因此我会使用 Date date = DateTimeUtils.toDate(zdt.toInstant());。它并不是那么底层。 - Ole V.V.

5

对我来说,所接受的答案没有起作用。返回的日期始终是本地日期而不是原始时区的日期。 我住在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

我们从毫秒数中创建日期(之前在这里回答过)。请注意,本地的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);

1
在给你的答案点赞时,我认为你提供的两个选项都有些过于繁琐。我更喜欢这种方式:instant = zdt.toLocalDateTime().toInstant(ZoneOffset.UTC); return Date.from(instant); 在这里,我使用了硬编码的区域偏移量以满足自己的需求,但通常情况下使用它也是很明显的。 - Anton Duzenko

4

以下是一个将当前系统时间转换为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();
    }

2

对于像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 并且通过

Date.from(zonedDateTime.toInstant())

2014年6月30日22:00:00 UTC


1
我使用这个。
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的任何地区运行,则会产生问题。因为计算机不是云上的时区。您应该在代码中正确设置时区,例如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);
    }
}

1
这似乎是7个答案中最复杂的方法之一。它有什么优点吗?我认为没有。真的没有必要经过格式化和解析。 - Ole V.V.
谢谢您的提问。因为Date wrongAns = Date.from(java.time.ZonedDateTime.ofInstant(now, zoneId).toInstant());;这行代码不起作用!!! - beehuang
我在我的时区17:08之前运行了你的代码片段,得到了ans=Mon Jun 18 01:07:56 CEST 2018,这是不正确的,然后得到了wrongAns=Sun Jun 17 17:07:56 CEST 2018,这是正确的。 - Ole V.V.
不,让我诚实地说,我放弃了理解为什么你认为你的错误代码是正确的,反之亦然。我的猜测是可能与忽略Date.toString()打印的时区缩写有关,这对于解释字符串至关重要。但这只是一个猜测。 - Ole V.V.
也许你是对的。但我只想从时区获取日期。(这个问题是ZonedDateTime转Date吗?)。第一个答案是不正确的。因为它只是按照您的区域系统默认设置获取日期。 - beehuang
显示剩余4条评论

0

您可以使用内置于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);

抱歉,这些临时变量和常量是从哪里来的? - Milen Kovachev
@MilenKovachev ZonedDateTime是Temporal的一个实例。 - Peter Lawrey
谢谢,但你应该提到你编辑了你的答案,这样我的评论就不会显得愚蠢了。 - Milen Kovachev
2
@MilenKovachev 你可以删除一条评论,但这不是一个愚蠢的问题。 - Peter Lawrey
1
我无法理解为什么这个答案被踩了。Instant跟踪秒和纳秒。Date跟踪毫秒。从一个转换到另一个是正确的。 - scottb

-2

如果你只是现在感兴趣,那么可以简单地使用:

Date d = new Date();

我对现在感兴趣,但是是UTC现在。 - Milen Kovachev
2
是的,那现在就是UTC时间,日期并不知道更多的信息。 - Jacob Eckel
3
不会,它会显示本地系统的当前时间。日期并不存储任何时区信息,但使用当前系统时间且忽略当前系统时区,因此不会将其转换回协调世界时。 - David
2
@David Date相对于UTC纪元保持时间,因此如果您从美国的系统中获取一个新的Date()对象和从日本计算机中获取另一个对象,则它们将是相同的(查看它们在内部保持的long值)。 - Jacob Eckel
1
@JacobEckel - 是的,但是如果您在美国时区中将9am放入日期中,然后更改为日本时区并创建一个新日期,那么Date中的内部值将不同。一旦您将DST引入混合中,除非您的应用程序始终以UTC运行,否则无法可靠地使用Date,而这可能会因许多原因而发生变化,其中大多数与糟糕的代码有关。 - David
我认为Milen想要UTC+0,也就是GMT或Zulu时间。如果我理解正确,我已经添加了一个答案,可以给出正确的日期对象。new Date()将匹配Java运行时所在的任何时区/本地设置的UTC +?。如果您住在伦敦,new Date()将正常工作,但对于其他时区则不起作用。 - Avec

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