如何使用 DateTimeFormatter 便携式格式化 java.util.Date?

How to format java.util.Date with DateTimeFormatter portable?

如何使用 DateTimeFormatter 便携式格式化 java.util.Date

我不会用

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

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

我需要准确格式化我拥有的那个对象。

更新

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

更新 2

我写了下面的程序,证明 Date 不仅包含正确的 "instant":

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 是 "Instant"。

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

这意味着,Date class 是对偶的。它的毫秒部分可能会根据时区偏移相对于 hh mm ss 部分移动。

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

因此,开头的代码示例是正确的:它采用 Date 的 "instant" 部分,并提供系统时区部分,该部分在 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!

您可以根据自己的需要使用。

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

TL;DR:您关注系统本地时区的使用是对的,但是您应该在流程的早期就已经关注了,当您使用系统本地时区构建 Date排在首位。

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

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

你说你是 "afraid that usage of ZoneId.systemDefault() can introduce some changes" - 但这正是 Date.getDate() 等人使用的。

Date 没有任何类型的 "dual contract" 可以让您将其视为无时区表示。 只是一个瞬间。几乎所有允许您将其构造或解构为组件的方法都明确记录为使用系统默认时区,就像您使用 ZoneId.systemDefault() 一样。 (一个值得注意的例外是 UTC 方法。)

隐式使用系统默认时区 Date 不同 Date 是一种有效的无时区表示,并且很容易证明原因:它很容易丢失数据。考虑 "March 26th 2017, 1:30am" 的无时区日期和时间。您可能希望能够获取它的文本表示、解析它,然后再重新格式化它。如果您在 Europe/London 时区执行此操作,则会遇到问题,如下所示:

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:30am,那将正常工作。

问题是 2017-03-26T01:30 由于夏令时 Europe/London 时区 不存在 - 凌晨 1 点,时钟向前跳到凌晨 2 点。

因此,如果您对这种破碎感到满意,那么当然可以使用 Date 和系统本地时区。否则,请勿为此目的使用 Date

如果您绝对必须以这种错误的方式使用Date,使用已经被弃用了大约 20 年的方法,因为它们具有误导性,但是您'我们能够更改系统时区,然后将其更改为没有 - 并且从未有过 - DST 的时间。 UTC 是这里显而易见的选择。此时,您可以在本地 date/time 和 Date 之间转换而不会丢失数据。 Date还是不好用,Instant一样只是一个瞬间,但至少不会丢数据

或者您可以确保无论何时从本地 date/time 构造 Date,您都使用 UTC 进行转换,当然,而不是系统本地时区...无论是通过 Date.UTC 方法,还是通过使用 UTC 中的 SimpleDateFormat 解析文本,或者它是什么。不幸的是,您没有告诉我们任何关于您的 Date 价值从何而来的信息...

tl;博士

How to format java.util.Date with DateTimeFormatter portable?

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

或者,一个one-liner...(不是我推荐这么复杂的one-liner)

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

详情

是正确的。这是我自己的看法,有一些具体的要点。

避免遗留 date-time classes.

不要使用 java.util.Datejava.util.CalendarSimpleDateFormatjava.sql.Date/Time/Timestamp 和其他可追溯到 [=295 的最早版本的相关 classes =].虽然 well-intentioned 早期尝试复杂地处理 date-time 值,但他们没有达到目标。现在被 java.time classes 取代。

如果您必须 inter-operate 使用旧代码中的遗留 classes 尚未为 java.time 更新,请转换。在旧 classes.

上调用新方法
Instant instant = myJavaUtilDate.toInstant() ; 

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

LocalDateTime != 时刻

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

A LocalDateTime 没有任何时区概念或 offset-from-UTC。没有这样的概念就是它的目的。 LocalDateLocalTime 同上:没有 zone/offset 的概念。将“Local”部分视为“any locality”或“no locality”,而不是任何一个特定的地方。

缺少zone/offset意味着一个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(“圣诞岛”)的床上睡着,如他们家中的 wall-clocks 所示。

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[Pacific/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, the kids on the farms in Québec are just rising at 5 AM the day before (Christmas Eve) to milk the cows and tap the maple sap 中布置礼物。

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

zdtMontreal.toString(): 2018-12-24T05:00-05:00[America/Montreal]

因此,在基里巴斯结束后,精灵们将圣诞老人送往西边,经过一连串新的午夜时间,从远东亚洲和新西兰开始,然后是印度,然后是中东,然后是非洲和欧洲,最后是美洲。目前的偏差范围从比 UTC 早 14 小时到晚 12 小时。所以圣诞老人只有 26 个小时多一点的时间来完成这项工作。

纪元

关于您使用 UTC 1970 年第一刻的 epoch reference 进行的实验,您无意中注入了您自己的 JVM 当前默认时区。您输入的字符串 1970-01-01 00:00:01 有误,因为它缺少任何时区指示符或 offset-from-UTC。换句话说,该输入字符串相当于 LocalDateTime object。当将该字符串解析为 Date(具有 UTC)时,Date class 在解释该输入字符串时默默地隐式应用了 JVM 当前的默认时区,试图创造意义,来确定一个特定的时刻。您又一次不恰当地将缺乏任何 zone/offset 概念的 date-time 与具有 zone/offset.

的 date-time 混合在一起

根据 Date.parse 的文档:

If a time zone or time-zone offset has been recognized, then the year, month, day of month, hour, minute, and second are interpreted in UTC and then the time-zone offset is applied. Otherwise, the year, month, day of month, hour, minute, and second are interpreted in the local time zone.

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

这里的关键是你没有指定一个zone/offset,而Date class 填写了缺失的信息。一个 well-intentioned 的功能,但令人困惑和 counter-productive。

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

如果您指的是 UTC,请说 UTC。在下一行中,我们在末尾包含一个 ZZulu 的缩写,表示 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 class 来表示独立于时间线的时间跨度。

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

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

你的下一个实验有同样的故事。您未能指定 zone/offset,因此Date 在解释您的 zone-less 输入时应用了一个。我认为这是个坏主意,但这是记录在案的行为。

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

Dateobject生成的字符串可以看出,它代表的是date-time1970年之后的一秒,在另一个时区而不是UTC。这是我的 JVM 的输出,默认时区为 America/Los_Angeles

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

为清楚起见,我们转换为 Instant。请注意 hour-of-day 是 UTC 中的上午 8 点。在 1970 年的第一天,America/Los_Angeles 地区的人们使用 wall-clock 时间比 UTC 晚八小时 。因此,在北美西海岸的大部分地区,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

这里有两个重要的部分:

  • 您必须学习和理解,时间轴上的一个时刻,一个点,有不同的 wall-clock 时间,被全球不同地方的不同人使用。换句话说,任何给定时刻的 wall-clock 时间在全球各地因时区而异。
  • 遗留 date-time classes 的糟糕设计选择(例如 java.util.Date 不幸地使情况复杂化。 ill-advised 行为给已经令人困惑的 date-time 处理主题带来了混乱而不是清晰。 避免遗留 classes. 仅使用 java.time classes。停止用头撞砖墙,然后你的头痛就会消失。

提示:

  • 学习在 UTC 中思考、工作、调试、记录和交换数据。将 UTC 视为 The One True Time™。避免在您自己的狭隘时区和 UTC 之间转换 back-and-forth。相反,忘记你自己的区域并在工作时专注于 UTC programming/administrating。在您的桌面上保留 UTC 时钟。
  • 仅当业务逻辑或用户在演示中的期望时才应用时区。
  • 始终明确指定您的 desired/expected 时区作为可选参数。即使您打算使用当前默认值,显式调用默认值,以使您的代码 self-documenting 符合您的意图。顺便说一句……同上 Locale:始终明确指定,从不隐式依赖默认值。

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

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

在哪里获取java.time classes?

  • Java SE 8, Java SE 9,及以后
    • Built-in。
    • 标准 Java API 的一部分,带有捆绑实施。
    • Java 9 添加了一些小功能和修复。
  • Java SE 6 and Java SE 7
    • java.time 的大部分功能是 back-ported 到 Java ThreeTen-Backport 中的 6 和 7。
  • Android
    • Android java.time classes.
    • 捆绑实施的更高版本
    • 对于较早的Android,ThreeTenABP project adapts ThreeTen-Backport (mentioned above). See

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.