【问题标题】:How to format java.util.Date with DateTimeFormatter portable?如何使用可移植的 DateTimeFormatter 格式化 java.util.Date?
【发布时间】:2017-09-17 13:22:21
【问题描述】:

如何格式化java.util.DateDateTimeFormatter 便携?

我不能用

Date in = readMyDateFrom3rdPartySource();
LocalDateTime ldt = LocalDateTime.ofInstant(in.toInstant(), ZoneId.systemDefault());
ldt.format(dateTimeFormatter);

因为我担心ZoneId.systemDefault() 的使用会带来一些变化。

我需要完全格式化我拥有的那个对象。

更新

注意:时间就是时间。不是空间。时区是非常粗略的经度度量,即空间。我不需要它。只有时间(和日期)。

更新 2

我编写了以下程序,证明Date 不仅包含正确的“即时”:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DataNature2 {

   public static void main(String[] args) throws ParseException {

      SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

      String dateTimeString = "1970-01-01 00:00:01";

      Date date = simpleDateFormat.parse(dateTimeString);

      System.out.println("1 second = " + date.getTime());

   }
}

输出如下:

1 second = -10799000

应该是这样的

1 second = 1000

如果Date 是“即时”。

数字107990003*60*60*1000-1000 - 我当地时间的时区偏移量。

这意味着,Date 类是双重的。它的毫秒部分可能会相对于hh mm ss 部分偏移时区偏移量。

这意味着,如果任何实用程序根据其部件 (hh mm ss) 返回 Date 对象,则它会隐式转换为本地时间。 getTime() 同时表示不同的时间。我的意思是如果这个程序在不同的机器上同时运行,getTime() 会是一样的,而时间部分会不同。

因此,开头的代码示例是正确的:它采用Date 的“即时”部分,并提供系统时区部分,该部分隐含在Date 中使用。 IE。它将双 Date 对象转换为具有相同部分的显式 LocalDateTime 对象。因此,之后的格式是正确的。

更新 3

活动更有趣:

Date date = new Date(70, 0, 1, 0, 0, 1);
assertEquals(1000, date.getTime());

此测试失败。

UDPATE 4

新代码。献给所有信徒。

public class DataNature3 {

   public static class TZ extends java.util.TimeZone {


      private int offsetMillis;

      public TZ(int offsetHours) {
         this.offsetMillis = offsetHours * 60 * 60 * 1000;
      }

      @Override
      public int getOffset(int era, int year, int month, int day, int dayOfWeek, int milliseconds) {
         throw new UnsupportedOperationException();
      }

      @Override
      public void setRawOffset(int offsetMillis) {
         this.offsetMillis = offsetMillis;
      }

      @Override
      public int getRawOffset() {
         return offsetMillis;
      }

      @Override
      public boolean useDaylightTime() {
         return false;
      }

      @Override
      public boolean inDaylightTime(Date date) {
         return false;
      }
   }

   public static void main(String[] args) {

      Date date = new Date(0);

      for(int i=0; i<10; ++i) {

         TimeZone.setDefault(new TZ(i));

         if( i<5 ) {
            System.out.println("I am date, I am an instant, I am immutable, my hours property is " + date.getHours() + ", Amen!");
         }
         else {
            System.out.println("WTF!? My hours property is now " + date.getHours() + " and changing! But I AM AN INSTANT! I AM IMMUTABLE!");
         }

      }

      System.out.println("Oh, please, don't do that, this is deprecated!");

   }
}

输出:

I am date, I am an instant, I am immutable, my hours property is 0, Amen!
I am date, I am an instant, I am immutable, my hours property is 1, Amen!
I am date, I am an instant, I am immutable, my hours property is 2, Amen!
I am date, I am an instant, I am immutable, my hours property is 3, Amen!
I am date, I am an instant, I am immutable, my hours property is 4, Amen!
WTF!? My hours property is now 5 and changing! But I AM AN INSTANT! I AM IMMUTABLE!
WTF!? My hours property is now 6 and changing! But I AM AN INSTANT! I AM IMMUTABLE!
WTF!? My hours property is now 7 and changing! But I AM AN INSTANT! I AM IMMUTABLE!
WTF!? My hours property is now 8 and changing! But I AM AN INSTANT! I AM IMMUTABLE!
WTF!? My hours property is now 9 and changing! But I AM AN INSTANT! I AM IMMUTABLE!
Oh, please, don't do that, this is deprecated!

【问题讨论】:

  • 那么Date 不是你想要的,因为它代表一个瞬间时间,它只有一个当地的年/月/日/小时/分钟/秒当您应用时区时。同一个瞬间在不同的时区有不同的值,没有时区就没有意义。这就像在不指定编码的情况下要求字符串的二进制编码......
  • @Dims:不,它真的不是Date 是一个瞬间——自 Unix 时代以来的毫秒数。这不是因为某个任意时间点 - 它有一个明确指定的起源。你对抗这个问题的时间越长,你遇到问题的时间就越长。 接受。如果您不相信我,您应该阅读java.util.Date 文档,该文档开头是“Date 类表示特定的时间瞬间,精度为毫秒。”
  • 如果你只有一个绝对时间点(相对于 UNIX 时间戳),没有时区就无法格式化它,因为在不同时区会显示为不同的时间。你的价值来自哪里,以什么形式出现?
  • 您的测试并未显示您认为它显示的内容。您在系统默认时区解析它,并在系统默认时区构建 1970-01-01T00:00:01。但是,我在这里完成了。您忽略了文档中的内容以及在日期/时间工作方面具有丰富经验的人所说的内容。当然欢迎你这样做,但你不会取得任何进展。您应该遵循 SLaks 的建议并使用LocalTime,因为这就是您收到的价值所代表的含义。它并不代表一个明确的瞬间。
  • Dims,我相信您问题的真正答案是:远离java.util.Date。它让你感到困惑,你没有理由想要它。改用java.time 中的新类,就像@JonSkeet 提到的那样。

标签: java date localdate


【解决方案1】:

TL;DR: 你关心系统本地时区的使用是对的,但是你应该在这个过程的早期就关心,当你使用系统本地时区构造一个Date in第一名。

如果您只希望格式化字符串具有与Date.getDate()Date.getMonth()Date.getYear() 等返回的相同组件,那么您的原始代码是合适的:

LocalDateTime ldt = LocalDateTime.ofInstant(in.toInstant(), ZoneId.systemDefault());

你说你“害怕 ZoneId.systemDefault() 的使用会带来一些变化”——但这正是 Date.getDate() 等使用的。

Date 没有任何类型的“双重合同”,可让您将其视为无时区表示。它只是一个瞬间。几乎每一个让您将其构造或解构为组件的方法都被清楚地记录为使用系统默认时区,就像您使用 ZoneId.systemDefault() 一样。 (一个值得注意的例外是UTC 方法。)

隐式使用系统默认时区Date 相同,Date 是一个有效的无时区表示,并且很容易证明原因:它很容易丢失数据。考虑“2017 年 3 月 26 日,凌晨 1:30”的无时区日期和时间。您可能希望能够对其进行文本表示,对其进行解析,然后再对其进行重新格式化。如果您在欧洲/伦敦时区执行此操作,则会遇到问题,如下所示:

import java.util.*;
import java.time.*;
import java.time.format.*;

public class Test {

    public static void main(String[] args) {
        TimeZone.setDefault(TimeZone.getTimeZone("Europe/London"));
        Date date = new Date(2017 - 1900, 3 - 1, 26, 1, 30);

        Instant instant = date.toInstant();
        ZoneId zone = ZoneId.systemDefault();
        LocalDateTime ldt = LocalDateTime.ofInstant(instant, zone);
        System.out.println(ldt); // Use ISO-8601 by default
    }
}

输出为 2017-03-26T02:30。这并不是说代码中存在一个错误 - 如果您将其更改为显示上午 9:30,那将正常工作。

问题在于,由于 DST,2017-03-26T01:30 在欧洲/伦敦时区不存在 - 凌晨 1 点,时钟向前跳到了凌晨 2 点。

因此,如果您对这种中断感到满意,那么请使用Date 和系统本地时区。否则,请勿尝试使用 Date 来达到此目的。

如果您绝对必须以这种破旧的方式使用Date,使用已被弃用大约 20 年的方法,因为它们具有误导性,但您可以更改系统时间区域,然后将其更改为没有 - 也从未有过 - DST 的东西。 UTC 是显而易见的选择。此时,您可以在本地日期/时间和Date 之间进行转换,而不会丢失数据。 Date 仍然是一个不好的用法,它就像Instant 一样只是一个瞬间,但至少你不会丢失数据。

或者您可以确保每当您从本地日期/时间构造 Date 时,您当然使用 UTC 进行转换,而不是系统本地时区...无论是通过 @987654340 @ 方法,或者使用 UTC 格式的 SimpleDateFormat 解析文本,或者不管它是什么。不幸的是,您还没有告诉我们您的 Date 值的来源...

【讨论】:

  • @Dims:无论你多么反对,Date 代表一个单一的瞬间。是的,它在多个地方被滥用,但其中唯一的信息是自 Unix 纪元以来的毫秒数。这是一个瞬间,它提供了不推荐使用的方法来在本地时区解释它。这不会使它成为两个瞬间 - 它使用“方便”方法(应该避免)使它成为一个瞬间。滥用它不会改变它所代表的东西。字符串被滥用来存储二进制数据(很糟糕)——这也不会给 它们 一个“双重性质”。
  • @Dims:更让我担心的是,你拒绝接受很多很多人的建议文档本身。如果这种态度在您的编码中无处不在,那么您的生活就会变得比需要的更加艰难。我通常不喜欢诉诸权威,但我日期/时间方面的部分专家 - 例如,我是 .NET 的 Noda Time 库的作者。我没有看到您试图理解为什么您的“证明”无效的论点(它们实际上证明与您声称的相反)。相反,您只需大喊。
  • @Dims:不,它不包含当前时区...仅在转换 into 和 Instant 或 from 时使用瞬间。它不再是对象状态的一部分,就像用于将字符串转换为二进制数据或反之亦然的编码是 String 对象的一部分。不,您的示例根本没有显示 1000 - 那将是 Unix 纪元之后的一秒,这不是您的 Date 实例所代表的......它代表的是 1970-01-01 00:00:01 的瞬间在您当地的时区。它仍然只是一个瞬间,即 1969-12-31T21:00:01Z。
  • @Dims:他们并非闻所未闻,但他们并不能证明你想要他们做什么。虽然内部表示并不重要,但所表示的状态重要的......而 java.util.Date 表示的状态是单一瞬间的。这与它上面的所有方法都是一致的。我将更新我的答案,以解释您的 Date 代表两个瞬间的所有“证明”实际上是如何证明它代表一个瞬间。不过,这将是几个小时后。
  • @Dims:不,您刚刚表明您没有阅读 getHours 的文档,其中明确表示它使用本地时区 - 换句话说,返回值取决于日期的状态系统本地时区设置的状态。你没有改变 Date - 尽管 Date 可变的,通过 setTime 方法......
【解决方案2】:

tl;博士

如何使用可移植的 DateTimeFormatter 格式化 java.util.Date?

Instant instant = myJavaUtilDate.toInstant() ;  // When encountering a `Date`, immediately convert from troublesome legacy class to modern *java.time* class. Then forget all about that `Date` object!
ZoneId z = ZoneId.systemDefault() ;             // Or ZoneId.of( "America/Montreal" ) or ZoneId.of( "Africa/Tunis" ) etc.
ZonedDateTime zdt = instant.atZone( z ) ;
DateTimeFormatter f = DateTimeFormatter.ofLocalizedDateTime( FormatStyle.FULL ).withLocale( Locale.CANADA_FRENCH ) ;
String output = zdt.format( f ) ;

或者,单线……(不是我推荐这么复杂的单线)

myJavaUtilDate.toInstant().atZone( ZoneId.systemDefault() ).format( DateTimeFormatter.ofLocalizedDateTime( FormatStyle.FULL ).withLocale( Locale.CANADA_FRENCH )  ) 

详情

Answer by Jon Skeet 是正确的。这是我自己的看法,有一些具体的观点。

避免使用旧的日期时间类。

不要使用java.util.Datejava.util.CalendarSimpleDateFormatjava.sql.Date/Time/Timestamp 和其他可追溯到 Java 最早版本的相关类。虽然在复杂处理日期时间值方面进行了善意的早期尝试,但它们没有达到标准。现在被 java.time 类所取代。

如果您必须与尚未更新的旧代码中的遗留类进行交互操作java.time,请转换。在旧类上调用新方法。

Instant instant = myJavaUtilDate.toInstant() ; 

您在问题中这样做了,但随后继续思考更多关于 Date 的信息。 忘记java.util.Date。假装它从未存在过。 DateInstant 都代表同一件事:UTC 中的一个时刻,时间轴上的一个点。唯一的区别是概念是现代Instant 具有更精细的纳秒分辨率,而不是Date 中的毫秒。

LocalDateTime != 时刻

然后,您从 Instant 转换为 LocalDateTime。你从时间线上的一个特定点移动到一个模糊的可能时刻。 这在几乎任何实际情况下都没有意义

LocalDateTime 缺少任何时区概念或与 UTC 的偏移量。没有这样的概念正是它的目的。 LocalDateLocalTime 同上:没有区域/偏移的概念。将“本地”部分视为“任何地区”或“地区”,而不是任何一个特定地区。

缺少区域/偏移意味着LocalDateTime代表片刻。它不是时间线上的一个点。这是一个关于潜在时刻的模糊概念,范围约为 26-27 小时。除非您将 LocalDateTime 放在特定区域或偏移量的上下文中,否则它没有真正的意义。

使用LocalDateTime 表示“今年的圣诞节从 2018 年 12 月 25 日的第一刻开始”。这样的陈述暗示任何地方,或没有具体地方

LocalDate ld = LocalDate.of(2018, Month.DECEMBER , 25);
LocalTime lt = LocalTime.MIN ;  // 00:00
LocalDateTime xmasStartsAnywhere = LocalDateTime.of( ld , lt ) ;

xmasStartsAnywhere.toString(): 2018-12-25T00:00

ZonedDateTime = 时刻

现在在时区的上下文中添加。从他们家的挂钟上可以看到,第一批从圣诞老人那里收到快递的孩子将在 25 日的第一个小时在Kiritimati(“圣诞岛”)的床上睡着。

ZoneId z = ZoneId.of("Pacific/Kiritimati");
LocalDate ld = LocalDate.of(2018, Month.DECEMBER , 25);
ZonedDateTime zdtKiritimati = ZonedDateTime.of( ld , LocalTime.MIN , z );

zdtKiritimati.toString(): 2018-12-25T00:00+14:00[太平洋/Kiritimati]

顺便说一句,我们可以将那个时区 (ZoneId) 直接分配给我们的LocalDateTime 以获得ZonedDateTime,而不是从头开始。

ZonedDateTime zdtKiritimati = xmasStartsAnywhere.atZone( z ) ;  // Move from the vague idea of the beginning of Christmas to the specific moment Christmas starts for actual people in an actual location.

与此同时,就在圣诞老人在Kiribati 中布置礼物的同时,魁北克农场的孩子们在前一天(平安夜)早上 5 点刚起床挤牛奶和tap the maple sap

ZonedDateTime zdtMontreal = zdtKiribati.withZoneSameInstant( ZoneId.of( "America/Montreal") );

zdtMontreal.toString(): 2018-12-24T05:00-05:00[美国/蒙特利尔]

因此,在基里巴斯结束后,精灵们沿着圣诞老人的路线向西前进,经过一连串新的午夜时间,从远东亚洲和新西兰开始,然后是印度,然后是中东,然后是非洲和欧洲,最终美洲。目前的偏移量范围从 UTC 前 14 小时到 12 小时后。所以圣诞老人只有 26 多小时来完成这项工作。

时代

关于您对 UTC 1970 年第一刻的epoch reference 的实验,您无意中注入了您自己的 JVM 当前的默认时区。您的输入字符串 1970-01-01 00:00:01 有问题,因为它缺少任何时区指示符或与 UTC 的偏移量。换句话说,该输入字符串等效于LocalDateTime 对象。当将该字符串解析为 Date(具有 UTC)时,Date 类在解释该输入字符串时默默地隐式应用您的 JVM 的当前默认时区,拼命尝试创造意义以确定特定时刻。再一次,您不恰当地将缺少任何区域/偏移量概念的日期时间与具有区域/偏移量的日期时间混合在一起。

根据Date.parse 的文档:

如果已识别时区或时区偏移量,则年、月、月中的某天、小时、分钟和秒都以 UTC 解释,然后应用时区偏移量。否则,年、月、月中的某天、小时、分钟和秒将按本地时区进行解释。

最后一句中的“本地”是一个糟糕的词选择。应该写成“通过应用 JVM 当前的默认时区来解释”。

这里的关键是您没有指定区域/偏移量,而Date 类填充了缺失的信息。一个善意的功能,但令人困惑和适得其反。

故事的寓意:如果您想要一个特定的时刻(时间轴上的一个点),始终明确指定您想要/预期的时区

如果您指的是 UTC,请说 UTC。在下一行中,我们在末尾添加了一个Z,是Zulu 的缩写,表示UTC。关于指定 UTC 的这部分是您遗漏的地方。

Instant instant = Instant.parse( "1970-01-01T00:00:01Z" ) ;  // One second after the first moment of 1970 **in UTC**. 

instant.toString(): 1970-01-01T00:00:01Z

顺便说一句,编写该代码的另一种方法是使用为纪元引用 1970-01-01T00:00:00Z 定义的常量,以及用于表示未附加到时间轴的时间跨度的 Duration 类。

Instant instant = Instant.EPOCH.plus( Duration.ofSeconds( 1 ) ) ;

instant.toString(): 1970-01-01T00:00:01Z

您的下一个实验也有同样的故事。您未能指定区域/偏移量,因此Date 在解释您的无区域输入时应用了一个。我认为这是一个坏主意,但这是记录在案的行为。

Date date = new Date(70, 0, 1, 0, 0, 1);
assertEquals(1000, date.getTime());  // fails

您可以从Date 对象生成的字符串中看到,它表示从 1970 年开始在另一个时区而不是 UTC 后一秒的日期时间。这是我的 JVM 的输出,默认时区为 America/Los_Angeles

date.toString(): Thu Jan 01 00:00:01 PST 1970

为了清楚起见,让我们转换为Instant。注意一天中的时间是 UTC 中的上午 8 点。在 1970 年的第一天,America/Los_Angeles 区域中的人们使用挂钟时间比 UTC 晚 8 小时。所以午夜后一秒,00:00:01,在北美西海岸的大部分地区,同时是 UTC 时间的上午 8 点。这里根本没有“有趣”的事情发生。

Instant instant = date.toInstant() ; // 00:00:01 in `America/Los_Angeles` = 8 AM UTC (specifically, 08:00:01 UTC). 

instant.toString(): 1970-01-01T08:00:01Z

这里有两个重要的部分:

  • 您必须学习并理解时间轴上的一个点具有不同的挂钟时间,全球不同地方的不同人使用不同的挂钟时间。换句话说,任何给定时刻的挂钟时间在全球范围内因时区而异。
  • 不幸的是,java.util.Date 等遗留日期时间类的糟糕设计选择使情况复杂化。不明智的行为给已经令人困惑的日期时间处理主题带来了混乱而不是清晰。 避免使用遗留类。 仅使用 java.time 类。停止用头撞砖墙,然后你的头痛就会消失。

提示:

  • 学习在 UTC 中思考、工作、调试、记录和交换数据。将 UTC 视为 The One True Time™。避免在您自己的地方时区和 UTC 之间来回转换。相反,在工作编程/管理时忘记您自己的区域并专注于 UTC。在您的桌面上保留 UTC 时钟。
  • 仅在业务逻辑需要或用户在演示中的期望时应用时区。
  • 始终将所需/预期的时区明确指定为可选参数。即使您打算使用当前的默认值,也要显式调用默认值,以使您的代码自我记录您的意图。顺便说一句……Locale 同上:始终明确指定,从不隐式依赖默认值。

关于java.time

java.time 框架内置于 Java 8 及更高版本中。这些类取代了麻烦的旧 legacy 日期时间类,例如 java.util.DateCalendarSimpleDateFormat

Joda-Time 项目现在位于maintenance mode,建议迁移到java.time 类。

要了解更多信息,请参阅Oracle Tutorial。并在 Stack Overflow 上搜索许多示例和解释。规格为JSR 310

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

从哪里获得 java.time 类?

ThreeTen-Extra 项目通过附加类扩展了 java.time。该项目是未来可能添加到 java.time 的试验场。您可以在这里找到一些有用的类,例如IntervalYearWeekYearQuartermore

【讨论】:

    【解决方案3】:

    您可以根据需要使用。

       java.util.Date
       DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
       Date date = new Date();
       System.out.println(dateFormat.format(date));
    
       java.util.Calendar
       DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
       Calendar cal = Calendar.getInstance();
       System.out.println(dateFormat.format(cal.getTime()));
    
       java.time.LocalDateTime
       DateTimeFormatter dateTimeFormat = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
       LocalDateTime localDateTime = LocalDateTime.now();
       System.out.println(dateTimeFormat.format(localDateTime));
    

    【讨论】:

    • 我需要混合DateTimeFormatterDate
    • @Dims 不,完全避免Date 并坚持使用 java.time 类。遗留的Date/Calendar 和相关类被 java.time 类取代,原因有很多(设计不佳、概念错误、黑客攻击、善意但麻烦的反特性)。您可以使用Instant instant = myJavaUtilDate.toInstant() 进行转换。
    • 很多第 3 方正在使用日期,例如Apache httpcomponents/cookies。不幸的是,我们必须处理它,直到它完全被淘汰。因此,请报告将日期转换为 LocalDateTime 的最快且错误最少的方法。还有一个 2 美分的预测:joda time 也将在几年后结束。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-09-19
    • 2018-01-26
    • 2023-03-24
    • 2011-09-24
    相关资源
    最近更新 更多