Преобразование между java.time.LocalDateTime и java.util.Date

485

Java 8 имеет совершенно новый API для даты и времени. Одним из наиболее полезных классов в этом API является LocalDateTimeхранение значения даты со временем, не зависящего от часового пояса.

Вероятно, существуют миллионы строк кода, использующих устаревший класс java.util.Dateдля этой цели. Таким образом, при взаимодействии старого и нового кода будет необходимо преобразование между ними. Как, кажется, нет прямых методов для достижения этой цели, как это можно сделать?

Кнут Арне Ведаа
источник

Ответы:

706

Короткий ответ:

Date in = new Date();
LocalDateTime ldt = LocalDateTime.ofInstant(in.toInstant(), ZoneId.systemDefault());
Date out = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());

Объяснение: ( на основе этого вопроса о LocalDate)

Несмотря на свое название, java.util.Dateпредставляет собой момент времени, а не «дату». Фактические данные, хранящиеся в объекте, представляют собой longколичество миллисекунд с 1970-01-01T00: 00Z (полночь в начале 1970 года по Гринвичу / UTC).

Класс, эквивалентный классу java.util.Dateв JSR-310, есть Instant, поэтому существуют удобные методы для обеспечения преобразования туда и обратно:

Date input = new Date();
Instant instant = input.toInstant();
Date output = Date.from(instant);

java.util.DateЭкземпляр не имеет понятия о времени зоны. Это может показаться странным , если вы звоните toString()на java.util.Date, потому что toStringэто по отношению к часовому поясу. Однако этот метод на самом деле использует часовой пояс Java по умолчанию на лету для предоставления строки. Часовой пояс не является частью фактического состояния java.util.Date.

InstantТакже не содержит никакой информации о часовом поясе. Таким образом, для преобразования из Instantдаты в местное время необходимо указать часовой пояс. Это может быть зона по умолчанию ZoneId.systemDefault()- или это может быть часовой пояс, которым управляет ваше приложение, например часовой пояс из пользовательских настроек. LocalDateTimeимеет удобный заводской метод, который принимает как мгновенный, так и часовой пояс:

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

И наоборот, LocalDateTimeчасовой пояс указывается путем вызова atZone(ZoneId)метода. Затем ZonedDateTimeможно преобразовать непосредственно в Instant:

LocalDateTime ldt = ...
ZonedDateTime zdt = ldt.atZone(ZoneId.systemDefault());
Date output = Date.from(zdt.toInstant());

Обратите внимание, что преобразование из LocalDateTimeв ZonedDateTimeможет привести к неожиданному поведению. Это связано с тем, что не каждое местное время существует из-за перехода на летнее время. Осенью / осенью местное время перекрывается, когда одна и та же местная дата-время встречается дважды. Весной есть разрыв, где час исчезает. Смотрите Javadoc atZone(ZoneId)для более детального определения того, что будет делать преобразование.

Подводя итоги, если вы java.util.Dateсовершите круговое путешествие от a до a LocalDateTimeи обратно к a, у java.util.Dateвас может получиться другое мгновение из-за перехода на летнее время.

Дополнительная информация: есть еще одно отличие, которое повлияет на очень старые даты. java.util.Dateиспользует календарь, который изменяется на 15 октября 1582 года с датами до этого, используя юлианский календарь вместо григорианского. Напротив, java.time.*использует систему календаря ISO (эквивалент григорианского) для всех времен. В большинстве случаев используется календарная система ISO, но вы можете увидеть странные эффекты при сравнении дат до 1582 года.

JodaStephen
источник
5
Большое спасибо за четкое объяснение. Тем более, почему java.util.Dateне содержит часовой пояс, но печатайте его во время toString(). Официальная документация события не говорит это ясно, как вы публикуете.
Вишня
Предупреждение: LocalDateTime.ofInstant(date.toInstant()... не ведет себя так, как можно наивно ожидать. Например new Date(1111-1900,11-1,11,0,0,0);, станет 1111-11-17 23:53:28использовать этот подход. Посмотрите на реализацию, java.sql.Timestamp#toLocalDateTime()если вам нужно, чтобы результат был 1111-11-11 00:00:00в предыдущем примере.
собака
2
Я добавил раздел об очень старых датах (до 1582). Впрочем, предложенное вами исправление может быть ошибочным, поскольку 1111-11-11 в java.util.Date - это тот же фактический день в истории, что и 1111-11-18 в java.time из-за разных систем календаря (разница в 6,5 минут) происходит во многих часовых поясах до 1900 года)
JodaStephen
2
Также стоит отметить, что java.sql.Date#toInstantвыбрасывает UnsupportedOperationException. Так что не используйте toInstantв RowMapper на java.sql.ResultSet#getDate.
LazerBass
132

Вот то, что я придумал (и, как и все загадки Date Time, это, вероятно, будет опровергнуто на основании некоторой странной корректировки timezone-leapyear-daylight: D)

Отключение: Date<< - >>LocalDateTime

Данный: Date date = [some date]

(1) LocalDateTime<< Instant<<Date

    Instant instant = Instant.ofEpochMilli(date.getTime());
    LocalDateTime ldt = LocalDateTime.ofInstant(instant, ZoneOffset.UTC);

(2) Date<< Instant<<LocalDateTime

    Instant instant = ldt.toInstant(ZoneOffset.UTC);
    Date date = Date.from(instant);

Пример:

Данный:

Date date = new Date();
System.out.println(date + " long: " + date.getTime());

(1) LocalDateTime<< Instant<< Date:

Создать Instantиз Date:

Instant instant = Instant.ofEpochMilli(date.getTime());
System.out.println("Instant from Date:\n" + instant);

Создайте Dateиз Instant(не обязательно, но для иллюстрации):

date = Date.from(instant);
System.out.println("Date from Instant:\n" + date + " long: " + date.getTime());

Создать LocalDateTimeизInstant

LocalDateTime ldt = LocalDateTime.ofInstant(instant, ZoneOffset.UTC);
System.out.println("LocalDateTime from Instant:\n" + ldt);

(2) Date<< Instant<<LocalDateTime

Создать Instantиз LocalDateTime:

instant = ldt.toInstant(ZoneOffset.UTC);
System.out.println("Instant from LocalDateTime:\n" + instant);

Создать Dateиз Instant:

date = Date.from(instant);
System.out.println("Date from Instant:\n" + date + " long: " + date.getTime());

Выход:

Fri Nov 01 07:13:04 PDT 2013 long: 1383315184574

Instant from Date:
2013-11-01T14:13:04.574Z

Date from Instant:
Fri Nov 01 07:13:04 PDT 2013 long: 1383315184574

LocalDateTime from Instant:
2013-11-01T14:13:04.574

Instant from LocalDateTime:
2013-11-01T14:13:04.574Z

Date from Instant:
Fri Nov 01 07:13:04 PDT 2013 long: 1383315184574
Saint Hill
источник
2
@scottb ты обращаешься ко мне или человеку, который задал вопрос? Что касается моих мыслей, хорошо, было бы хорошо, если бы преобразование было явно указано в API jdk 8 - они могли бы по крайней мере сделать это. В любом случае, существует множество библиотек, которые будут переработаны, чтобы включить новые функции Java-8 и знать, как это сделать, важно.
Координатор
4
@scottb Почему стороннее дело было бы необычным? Это чертовски часто. Один пример: JDBC 4 и меньше (надеюсь, не 5).
Раман
5
Как общее правило JSR-310, нет необходимости конвертировать между типами с помощью epoch-millis. Существуют лучшие альтернативы с использованием объектов, см. Мой полный ответ ниже. Ответ выше также полностью действителен только при использовании смещения зоны, например UTC - некоторые части ответа не будут работать для полного часового пояса, например America / New_York.
JodaStephen
2
Вместо того, чтобы Instant.ofEpochMilli(date.getTime())делатьdate.toInstant()
коза
1
@Goat toInstant()выглядит красиво, за исключением того, что это не так java.sql.Date, arggggh! Так что, наконец, проще в использовании Instant.ofEpochMilli(date.getTime()).
vadipp
22

Гораздо более удобный способ, если вы уверены, что вам нужен часовой пояс по умолчанию:

Date d = java.sql.Timestamp.valueOf( myLocalDateTime );
Петриченко
источник
25
Конечно, это проще, но мне не нравится смешивать вещи, связанные с jdbc, с простой обработкой дат, по крайней мере, IMHO.
Энрико Джурин
9
Извините, это ужасное решение простой проблемы. Это похоже на определение 5, равного количеству колец в логотипе Олимпийских игр.
Madbreaks
7

Кажется, что следующее работает при преобразовании из нового API LocalDateTime в java.util.date:

Date.from(ZonedDateTime.of({time as LocalDateTime}, ZoneId.systemDefault()).toInstant());

обратное преобразование может быть (надеюсь) достигнуто аналогичным способом ...

Надеюсь, поможет...

головокружительный
источник
5

Все здесь: http://blog.progs.be/542/date-to-java-time

Ответ с «круговым отключением» не является точным: когда вы делаете

LocalDateTime ldt = LocalDateTime.ofInstant(instant, ZoneOffset.UTC);

если ваш системный часовой пояс не UTC / GMT, вы меняете время!

джо-мохо
источник
Только если вы делаете это в течение 1 часа в год, осенью, когда два раза из LocalDateTime перекрываются. Пружинное движение вперед не вызывает проблем. В большинстве случаев будет правильно конвертировать оба направления.
Дэвид
4

Самый быстрый способ для LocalDateTime-> Dateэто:

Date.from(ldt.toInstant(ZoneOffset.UTC))

Лукаш Червинский
источник
3

Я не уверен, если это самый простой или лучший способ, или есть какие-либо подводные камни, но это работает:

static public LocalDateTime toLdt(Date date) {
    GregorianCalendar cal = new GregorianCalendar();
    cal.setTime(date);
    ZonedDateTime zdt = cal.toZonedDateTime();
    return zdt.toLocalDateTime();
}

static public Date fromLdt(LocalDateTime ldt) {
    ZonedDateTime zdt = ZonedDateTime.of(ldt, ZoneId.systemDefault());
    GregorianCalendar cal = GregorianCalendar.from(zdt);
    return cal.getTime();
}
Кнут Арне Ведаа
источник
3
Там определенно ловушка собирается из LocalDateTimeв Date. При переходах на летнее время, a LocalDateTimeможет отсутствовать или встречаться дважды. Вы должны решить, что вы хотите, чтобы произошло в каждом случае.
Джон Скит
1
Кстати, GregorianCalendarпринадлежит старый неуклюжий API, который новый java.timeAPI стремится заменить
Вадим
3

Если вы работаете на Android и используете threetenbp, вы можете использовать DateTimeUtilsвместо этого.

например:

Date date = DateTimeUtils.toDate(localDateTime.atZone(ZoneId.systemDefault()).toInstant());

вы не можете использовать, Date.fromтак как он поддерживается только на API 26+

humazed
источник