Java 8 java.time: добавление TemporalUnit в Instant vs LocalDateTime

Я играю с новым пакетом java.time в Java 8. У меня есть устаревшая база данных, которая дает мне java.util.Date, который я конвертирую в Instant.

Я пытаюсь добавить период времени, основанный на другом флаге базы данных. Я мог бы добавить дни, недели, месяцы или годы. Мне не нужно беспокоиться о том, что я добавляю, и я хотел бы иметь возможность добавлять больше опций в будущем.

Моей первой мыслью было Instant.plus(), но это дает мне UnsupportedTemporalTypeException для значений больше одного дня. Очевидно, Instant не поддерживает операции с большими единицами времени. Хорошо, что угодно, LocalDateTime делает.

Итак, это дает мне этот код:

private Date adjustDate(Date myDate, TemporalUnit unit){
    Instant instant = myDate.toInstant();
    LocalDateTime dateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
    dateTime = dateTime.plus(1, unit);
    Instant updatedInstant = dateTime.atZone(ZoneId.systemDefault()).toInstant();
    return new Date(dueInstant.toEpochMilli());
}

Я впервые использую API нового времени, так что, возможно, я что-то здесь упустил. Но мне кажется неуклюжим, что я должен идти:

Date --> Instant --> LocalDateTime --> do stuff--> Instant --> Date.

Даже если бы мне не пришлось использовать часть даты, я все равно подумал бы, что это было немного неудобно. Итак, мой вопрос заключается в следующем: я делаю это совершенно неправильно и как лучше всего это сделать?


Изменить: разверните обсуждение в комментариях.

Я думаю, что теперь у меня есть лучшее представление о том, как LocalDateTime и Instant играют с java.util.Date и java.sql.Timestamp. Спасибо всем.

Теперь более практическое рассмотрение. Допустим, пользователь отправляет мне дату из любой точки мира, в произвольном часовом поясе. Они присылают мне 2014-04-16T13:00:00, который я могу проанализировать в LocalDateTime. Затем я конвертирую это напрямую в java.sql.Timestamp и сохраняю в своей базе данных.

Теперь, ничего не делая, я извлекаю свой java.sql.timestamp из своей базы данных, конвертирую в LocalDateTime с помощью timestamp.toLocalDateTime(). Все хорошо. Затем я возвращаю это значение своему пользователю, используя форматирование ISO_DATE_TIME. Результат 2014-04-16T09:00:00.

Я предполагаю, что это различие связано с некоторым типом неявного преобразования в / из UTC. Я думаю, что мой часовой пояс по умолчанию может применяться к значению (EDT, UTC-4), что объясняет, почему число отключено на 4 часа.

Новый вопрос (ы). Где здесь происходит неявное преобразование местного времени в UTC? Как лучше сохранить часовые пояса. Не следует ли мне напрямую переходить от местного времени в виде строки (2014-04-16T13: 00: 00) к LocalDateTime? Должен ли я ожидать часовой пояс от пользовательского ввода?


person jacobhyphenated    schedule 01.04.2014    source источник
comment
Какую ценность вы здесь представляете? Instant не логически знает о календарной системе - это просто момент времени, поэтому добавление месяца к нему не имеет смысла. Вам также следует тщательно обдумать, действительно ли вы действительно хотите использовать системный часовой пояс - хотите ли вы получать разные результаты для одних и тех же значений в зависимости от того, где вы работаете?   -  person Jon Skeet    schedule 01.04.2014
comment
@JonSkeet Было бы тогда более подходящим произвольно выбрать ZoneId? Всегда GMT или что-то в этом роде? Похоже, это может иметь свой собственный набор проблем. Возможно, проблема в том, что я не знаю, как представить свой java.util.date. У меня есть момент во времени. Если выполняются определенные условия, я хочу изменить эту точку на один месяц (или день, год, что угодно) в будущем.   -  person jacobhyphenated    schedule 01.04.2014
comment
Использование UTC может быть лучшим вариантом, но вам действительно нужно подумать о своих требованиях. Что вообще значит изменить момент времени (без часового пояса или календаря) на один месяц?   -  person Jon Skeet    schedule 01.04.2014
comment
Дело в том, что данный момент может представлять, например, 28 февраля или 1 марта, в зависимости от часового пояса. Таким образом, добавление месяца вернет 28 марта или 1 апреля, в зависимости от часового пояса. Возможно, вам стоит использовать не Date, а LocalDateTime. Это позволит вам добавить месяц независимо от часового пояса. И тогда вы сможете преобразовать LocalDateTime в мгновение в заданном часовом поясе.   -  person JB Nizet    schedule 01.04.2014
comment
@JBNizet Я бы хотел это сделать, но дата берется из базы данных (java.sql.timestamp) и отображается с помощью спящего режима. Я не думаю, что у меня есть возможность изменить это, по крайней мере, в ближайшем будущем. Таким образом, он представляет момент времени как количество миллисекунд с эпохи. Предполагая, что клиент может выбрать часовой пояс позже, пока я поддерживаю миллисекунды с момента эпохи, через месяц может быть 28 февраля или 1 марта в зависимости от клиента, и это будет нормально. Это должно быть определено клиентом позже, а не мной, где бы я ни был, манипулирующим базой данных.   -  person jacobhyphenated    schedule 01.04.2014
comment
Если в вашей базе данных все хранится в формате UTC, вы можете считать, что отметка времени является моментом в часовом поясе UTC, и преобразовать ее в LocalDateTime с помощью UTC. Вы можете инкапсулировать это преобразование в свою сущность или использовать jadira, которая (AFAIK) позволит вам напрямую сопоставить LocalDateTime.   -  person JB Nizet    schedule 01.04.2014
comment
@JBNizet Хорошо, хорошо. Этот подход будет работать, но он, возможно, ничем не отличается от приведенного выше кода, только преобразования перемещаются в класс фреймворка (или объект). Но старую дату Java еще нужно преобразовать в Instant в LocalDateTime, выполнить операции, затем преобразовать обратно в Instant, а затем в Date / Timestamp. То есть подход, который я использовал выше, правильный, даже если его можно немного реорганизовать?   -  person jacobhyphenated    schedule 01.04.2014
comment
Мне кажется правильным, если вы укажете UTC в качестве часового пояса. Я бы использовал Date.from (Instant) вместо миллисекунд. Будущие версии Hibernat, вероятно, будут напрямую поддерживать новые типы времени.   -  person JB Nizet    schedule 01.04.2014
comment
@JBNizet Последний вопрос, гипотетический. Если даты изначально были сохранены как systemDefault () вместо UTC, тогда я должен везде использовать systemDefault (), верно?   -  person jacobhyphenated    schedule 01.04.2014
comment
Я не понимаю, что вы имеете в виду. Date не имеет часового пояса, и базы данных часто используют UTC для внутренних целей.   -  person JB Nizet    schedule 01.04.2014
comment
JDBC требует, чтобы драйверы интерпретировали время и временные метки без часового пояса как находящиеся в местном часовом поясе, поэтому, если ваш часовой пояс не является utc или ваш драйвер не совместим с jdbc, это предположение неверно.   -  person Mark Rotteveel    schedule 02.04.2014
comment
@jacobhyphenated A java.sql.Timestamp имеет toLocalDateTime() < / a> и статический _ 3_. Не должно быть необходимости использовать Instant в качестве посредника.   -  person Mark Rotteveel    schedule 02.04.2014
comment
@MarkRotteveel Спасибо! Я отредактировал свой вопрос, чтобы включить в него косвенно связанный с ним вопрос.   -  person jacobhyphenated    schedule 02.04.2014


Ответы (1)


Я продолжу и опубликую ответ, основанный на моем окончательном решении и своего рода резюме очень длинной цепочки комментариев.

Для начала, вся цепочка конверсии:

Date --> Instant --> LocalDateTime --> Do stuff --> Instant --> Date

Это необходимо для сохранения информации о часовом поясе и выполнения операций с объектом типа Date, который знает календарь и весь контекст в нем. В противном случае мы рискуем неявно преобразовать данные в местный часовой пояс, и если мы попытаемся перевести его в удобочитаемый формат даты, время может измениться из-за этого.

Например, метод toLocalDateTime() в классе java.sql.Timestamp неявно преобразуется в часовой пояс по умолчанию. Это было нежелательно для моих целей, но не обязательно плохое поведение. Однако важно знать об этом. Это проблема с преобразованием непосредственно из устаревшего объекта даты Java в объект LocalDateTime. Поскольку унаследованные объекты обычно считаются UTC, при преобразовании используется смещение местного часового пояса.

Теперь предположим, что наша программа принимает ввод 2014-04-16T13:00:00 и сохраняет в базе данных как java.sql.Timestamp.

//Parse string into local date. LocalDateTime has no timezone component
LocalDateTime time = LocalDateTime.parse("2014-04-16T13:00:00");

//Convert to Instant with no time zone offset
Instant instant = time.atZone(ZoneOffset.ofHours(0)).toInstant();

//Easy conversion from Instant to the java.sql.Timestamp object
Timestamp timestamp = Timestamp.from(instant);

Теперь берем метку времени и добавляем к ней некоторое количество дней:

Timestamp timestamp = ...

//Convert to LocalDateTime. Use no offset for timezone
LocalDateTime time = LocalDateTime.ofInstant(timestamp.toInstant(), ZoneOffset.ofHours(0));

//Add time. In this case, add one day.
time = time.plus(1, ChronoUnit.DAYS);

//Convert back to instant, again, no time zone offset.
Instant output = time.atZone(ZoneOffset.ofHours(0)).toInstant();

Timestamp savedTimestamp = Timestamp.from(output);

Теперь нам просто нужно вывести в виде удобочитаемой строки в формате ISO_LOCAL_DATE_TIME.

Timestamp timestamp = ....
LocalDateTime time = LocalDateTime.ofInstant(timestamp.toInstant(), ZoneOffset.ofHours(0));
String formatted = DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(time);
person jacobhyphenated    schedule 21.04.2014
comment
ZoneOffset.ofHours (0) лучше выразить как ZoneOffset.UTC. Вам также может быть проще работать с ZonedDateTime, поскольку этот класс поддерживает все плюсовые / минусовые свойства LocalDateTime, но сохраняет информацию о часовом поясе. - person JodaStephen; 27.05.2014
comment
Для конкретного случая использования дней вам не нужно (Местное / Зональное) Дата и время, поскольку продолжительность дней может быть добавлена ​​непосредственно к моменту: Timestamp.from(timestamp.toInstant().plus(Duration.ofDays(1))) - person Christopher Currie; 10.08.2016
comment
Добавление Duration.ofDays в Instant не учитывает переход на летнее время и т.п., поэтому не рекомендуется. Вместо этого преобразуйте в ZonedDateTime. - person Christoffer Hammarström; 03.04.2017
comment
Что рекомендуется, зависит от того, какое поведение вы хотите. Вы хотите добавить единицы ровно на 24 часа? Тогда используйте Instant.plus. Конечно, если вы хотите рассчитать то же время, что и ваш ввод, но через n дней, вам понадобится часовой пояс, поэтому не следует использовать Instant в первую очередь? ZoneOffset.UTC и / или ZoneOffset.ofHours (n) также не будут обращать внимания на изменения времени в вашей текущей зоне, потому что UTC и нумерованные смещения никогда не изменяют время. - person mjaggard; 15.01.2019