Статьи

Java 8 Дата и время

В настоящее время некоторые приложения по-прежнему используют API-интерфейсы java.util.Date и java.util.Calendar , в том числе библиотеки, чтобы упростить нашу работу с этими типами, например JodaTime. Java 8, однако, представила новые API для обработки даты и времени, которые позволяют нам иметь более точный контроль над нашим представлением даты и времени, предоставляя нам неизменные объекты datetime, более гибкий API и в большинстве случаев повышение производительности без использования дополнительные библиотеки. Давайте посмотрим на основы.

LocalDate / LocalTime / LocalDateTime

Давайте начнем с новых API, которые больше всего связаны с java.util.Date : LocalDate , API- LocalDate даты, который представляет дату без времени; LocalTime , представление времени без даты; и LocalDateTime , который является комбинацией двух предыдущих. Все эти типы представляют локальную дату и / или время для региона, но, подобно java.util.Date , они содержат нулевую информацию о зоне, в которой она представлена, только представление даты и времени в вашем текущем часовой пояс.

Прежде всего, эти API поддерживают простую реализацию:

01
02
03
04
05
06
07
08
09
10
11
12
13
LocalDate date = LocalDate.of(2018,2,13);
// Uses DateTimeformatter.ISO_LOCAL_DATE for which the format is: yyyy-MM-dd
LocalDate date = LocalDate.parse("2018-02-13");
 
LocalTime time = LocalTime.of(6,30);
// Uses DateTimeFormatter.ISO_LOCAL_TIME for which the format is: HH:mm[:ss[.SSSSSSSSS]]
// this means that both seconds and nanoseconds may optionally be present.
LocalTime time = LocalTime.parse("06:30");
 
LocalDateTime dateTime = LocalDateTime.of(2018,2,13,6,30);
// Uses DateTimeFormatter.ISO_LOCAL_DATE_TIME for which the format is the
// combination of the ISO date and time format, joined by 'T': yyyy-MM-dd'T'HH:mm[:ss[.SSSSSSSSS]]
LocalDateTime dateTime = LocalDateTime.parse("2018-02-13T06:30");

Их легко конвертировать:

1
2
3
4
5
6
7
8
9
// LocalDate to LocalDateTime
LocalDateTime dateTime = LocalDate.parse("2018-02-13").atTime(LocalTime.parse("06:30"));
 
// LocalTime to LocalDateTime
LocalDateTime dateTime = LocalTime.parse("06:30").atDate(LocalDate.parse("2018-02-13"));
 
// LocalDateTime to LocalDate/LocalTime
LocalDate date = LocalDateTime.parse("2018-02-13T06:30").toLocalDate();
LocalTime time = LocalDateTime.parse("2018-02-13T06:30").toLocalTime();

Помимо этого, невероятно легко выполнять операции с нашими представлениями даты и времени, используя методы «плюс» и «минус», а также некоторые вспомогательные функции:

01
02
03
04
05
06
07
08
09
10
LocalDate date = LocalDate.parse("2018-02-13").plusDays(5);
LocalDate date = LocalDate.parse("2018-02-13").plus(3, ChronoUnit.MONTHS);
 
LocalTime time = LocalTime.parse("06:30").minusMinutes(30);
LocalTime time = LocalTime.parse("06:30").minus(500, ChronoUnit.MILLIS);
 
LocalDateTime dateTime = LocalDateTime.parse("2018-02-13T06:30").plus(Duration.ofHours(2));
 
// using TemporalAdjusters, which implements a few useful cases:
LocalDate date = LocalDate.parse("2018-02-13").with(TemporalAdjusters.lastDayOfMonth());

Теперь, как бы мы перешли от java.util.Date к LocalDateTime и его вариантам? Что ж, все просто: мы можем преобразовать тип Date в тип Instant, который представляет собой время с эпохи 1 января 1970 года, а затем мы можем создать экземпляр LocalDateTime используя Instant и текущую зону.

1
LocalDateTime dateTime = LocalDateTime.ofInstant(new Date().toInstant(), ZoneId.systemDefault());

Чтобы преобразовать обратно в дату, мы можем просто использовать Instant, который представляет тип времени Java 8. Однако следует обратить внимание на то, что хотя LocalDate , LocalTime и LocalDateTime не содержат никакой информации о зоне или смещении, они представляют локальную дату и / или время в определенном регионе и, таким образом, содержат текущее смещение в этом регионе. Таким образом, мы обязаны предоставить смещение для правильного преобразования определенного типа в мгновенный.

1
2
3
4
5
6
7
8
9
// represents Wed Feb 28 23:24:43 CET 2018
Date now = new Date();
 
// represents 2018-02-28T23:24:43.106
LocalDateTime dateTime = LocalDateTime.ofInstant(now.toInstant(), ZoneId.systemDefault());
 
// represent Wed Feb 28 23:24:43 CET 2018
Date date = Date.from(dateTime.toInstant(ZoneOffset.ofHours(1)));
Date date = Date.from(dateTime.toInstant(ZoneId.systemDefault().getRules().getOffset(dateTime)));

Разница во времени — продолжительность и период

Как вы заметили, в одном из приведенных выше примеров мы использовали объект Duration . Duration и Period являются двумя представлениями времени между двумя датами, первая представляет разницу во времени в секундах и наносекундах, вторая — в днях, месяцах и годах.

Когда вы должны использовать это? Period когда вам нужно знать разницу во времени между двумя представлениями LocalDate :

1
Period period = Period.between(LocalDate.parse("2018-01-18"), LocalDate.parse("2018-02-14"));

Duration когда вы ищете разницу между представлением, которое содержит информацию о времени:

1
Duration duration = Duration.between(LocalDateTime.parse("2018-01-18T06:30"), LocalDateTime.parse("2018-02-14T22:58"));

При выводе Period или Duration с помощью toString() будет использоваться специальный формат, основанный на стандарте ISO-8601. Для периода используется шаблон PnYnMnD, где n определяет количество лет, месяцев или дней, представленных в течение периода. Это означает, что P1Y2M3D определяет период 1 год, 2 месяца и 3 дня. , ‘P’ в шаблоне — это указатель периода, который говорит нам, что следующий формат представляет период. Используя шаблон, мы также можем создать период на основе строки, используя метод parse() .

1
2
// represents a period of 27 days
Period period = Period.parse("P27D");

При использовании Durations мы немного отходим от стандарта ISO-8601, поскольку Java 8 не использует те же шаблоны. Шаблон, определенный ISO-8601, имеет вид PnYnMnDTnHnMn.nS. Это в основном паттерн Period , расширенный представлением времени. В шаблоне T — указатель времени, поэтому следующая часть определяет продолжительность, указанную в часах, минутах и ​​секундах.

Java 8 использует два конкретных шаблона для Duration , а именно PnDTnHnMn.nS при синтаксическом анализе строки в Duration и PTnHnMn.nS при вызове метода toString() в экземпляре Duration .

Наконец, что не менее важно, мы также можем извлечь различные части периода или продолжительности, используя соответствующий метод для типа. Однако важно знать, что различные типы datetime также поддерживают это посредством использования типа перечисления ChronoUnit . Давайте посмотрим на некоторые примеры:

1
2
3
4
5
6
7
8
// represents PT664H28M
Duration duration = Duration.between(LocalDateTime.parse("2018-01-18T06:30"), LocalDateTime.parse("2018-02-14T22:58"));
 
// returns 664
long hours = duration.toHours();
 
// returns 664
long hours = LocalDateTime.parse("2018-01-18T06:30").until(LocalDateTime.parse("2018-02-14T22:58"), ChronoUnit.HOURS);

Работа с зонами и смещениями — ZonedDateTime и OffsetDateTime

До сих пор мы показали, как новые API даты сделали несколько вещей немного проще. Что действительно имеет значение, тем не менее, это возможность легко использовать дату и время в контексте часового пояса. Java 8 предоставляет нам ZonedDateTime и OffsetDateTime , первый — LocalDateTime с информацией для конкретной зоны (например, Европа / Париж), второй — LocalDateTime со смещением. Какая разница? OffsetDateTime использует фиксированную разницу во времени между UTC / Гринвичем и указанной датой, тогда как ZonedDateTime указывает зону, в которой представлено время, и будет учитывать летнее время.

Конвертировать в любой из этих типов очень легко:

01
02
03
04
05
06
07
08
09
10
11
12
OffsetDateTime offsetDateTime = LocalDateTime.parse("2018-02-14T06:30").atOffset(ZoneOffset.ofHours(2));
// Uses DateTimeFormatter.ISO_OFFSET_DATE_TIME for which the default format is
// ISO_LOCAL_DATE_TIME followed by the offset ("+HH:mm:ss").
OffsetDateTime offsetDateTime = OffsetDateTime.parse("2018-02-14T06:30+06:00");
 
ZonedDateTime zonedDateTime = LocalDateTime.parse("2018-02-14T06:30").atZone(ZoneId.of("Europe/Paris"));
// Uses DateTimeFormatter.ISO_ZONED_DATE_TIME for which the default format is
// ISO_OFFSET_DATE_TIME followed by the the ZoneId in square brackets.
ZonedDateTime zonedDateTime = ZonedDateTime.parse("2018-02-14T06:30+08:00[Asia/Macau]");
// note that the offset does not matter in this case.
// The following example will also return an offset of +08:00
ZonedDateTime zonedDateTime = ZonedDateTime.parse("2018-02-14T06:30+06:00[Asia/Macau]");

При переключении между ними вы должны иметь в виду, что при преобразовании из ZonedDateTime в OffsetDateTime будет учитываться переход на летнее время, а при преобразовании в другом направлении, от OffsetDateTime до ZonedDateTime , вы не будете иметь информацию о регионе зоны. и не будет никаких правил, применяемых для перехода на летнее время. Это связано с тем, что смещение не определяет никаких правил часового пояса и не связано с конкретным регионом.

01
02
03
04
05
06
07
08
09
10
11
12
ZonedDateTime winter = LocalDateTime.parse("2018-01-14T06:30").atZone(ZoneId.of("Europe/Paris"));
ZonedDateTime summer = LocalDateTime.parse("2018-08-14T06:30").atZone(ZoneId.of("Europe/Paris"));
 
// offset will be +01:00
OffsetDateTime offsetDateTime = winter.toOffsetDateTime();
// offset will be +02:00
OffsetDateTime offsetDateTime = summer.toOffsetDateTime();
 
OffsetDateTime offsetDateTime = zonedDateTime.toOffsetDateTime();
 
OffsetDateTime offsetDateTime = LocalDateTime.parse("2018-02-14T06:30").atOffset(ZoneOffset.ofHours(5));
ZonedDateTime zonedDateTime = offsetDateTime.toZonedDateTime();

А что если мы хотели бы знать, какое время для определенной зоны или смещения находится в нашей собственной временной зоне? Ну, для этого есть несколько удобных функций!

1
2
3
4
5
6
7
// timeInMacau represents 2018-02-14T13:30+08:00[Asia/Macau]
ZonedDateTime timeInMacau = LocalDateTime.parse( "2018-02-14T13:30" ).atZone( ZoneId.of( "Asia/Macau" ) );
// timeInParis represents 2018-02-14T06:30+01:00[Europe/Paris]
ZonedDateTime timeInParis = timeInMacau.withZoneSameInstant( ZoneId.of( "Europe/Paris" ) );
 
OffsetDateTime offsetInMacau = LocalDateTime.parse( "2018-02-14T13:30" ).atOffset( ZoneOffset.ofHours( 8 ) );
OffsetDateTime offsetInParis = offsetInMacau.withOffsetSameInstant( ZoneOffset.ofHours( 1 ) );

Было бы неудобно, если бы нам приходилось все время вручную конвертировать эти типы, чтобы получить тот, который нам нужен. Вот где Spring Framework приходит к нам на помощь. Spring предоставляет нам несколько конверторов даты и времени, которые зарегистрированы в ConversionRegistry и могут быть найдены в классе org.springframework.format.datetime.standard.DateTimeConverters .

При использовании этих преобразователей важно знать, что они не будут преобразовывать время между регионами или смещениями. Например, LocalDateTime будет возвращать LocalDateTime для зоны, в которой он был указан, а не LocalDateTime который он будет представлять в регионе вашего приложения.

1
2
3
ZonedDateTime zonedDateTime = LocalDateTime.parse("2018-01-14T06:30").atZone(ZoneId.of("Asia/Macau"));
// will represent 2018-01-14T06:30, regardless of the region your application has specified
LocalDateTime localDateTime = conversionService.convert(zonedDateTime, LocalDateTime.class);

И последнее, но не менее важное: вы можете обратиться к ZoneId.getAvailableZoneIds() чтобы найти все доступные часовые пояса, или использовать карту ZoneId.SHORT_IDS , которая содержит сокращенную версию для нескольких часовых поясов, таких как EST, CST и другие.

Форматирование — Использование DateTimeFormatter

Конечно, разные регионы мира используют разные форматы для указания времени. Одно приложение может использовать MM-dd-yyyy, а другое — dd / MM / yyyy. Некоторые приложения хотят устранить все недоразумения и представляют свои даты с помощью yyyy-MM-dd. При использовании java.util.Date мы быстро перейдем к использованию нескольких форматеров. Однако класс DateTimeFormatter предоставляет нам дополнительные шаблоны, поэтому мы можем использовать один форматер для нескольких форматов! Давайте посмотрим, используя несколько примеров.

1
2
3
4
5
6
// Let’s say we want to convert all of patterns mentioned above.
// 09-23-2018, 23/09/2018 and 2018-09-23 should all convert to the same LocalDate.
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("[yyyy-MM-dd][dd/MM/yyyy][MM-dd-yyyy]");
LocalDate.parse("09-23-2018", formatter);
LocalDate.parse("23/09/2018", formatter);
LocalDate.parse("2018-09-23", formatter);

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

1
2
3
4
5
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
    .appendOptional( DateTimeFormatter.ofPattern( "yyyy-MM-dd" ) )
    .optionalStart().appendPattern( "dd/MM/yyyy" ).optionalEnd()
    .optionalStart().appendPattern( "MM-dd-yyyy" ).optionalEnd()
    .toFormatter();

Это основы для включения нескольких шаблонов, но что если наши шаблоны отличаются незначительно? Давайте посмотрим на гггг-ММ-дд и гггг-МММ-дд.

01
02
03
04
05
06
07
08
09
10
// 2018-09-23 and 2018-Sep-23 should convert to the same LocalDate.
// Using the ofPattern example we’ve used above will work:
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("[yyyy-MM-dd][yyyy-MMM-dd]" );
LocalDate.parse( "2018-09-23", formatter );
LocalDate.parse( "2018-Sep-23", formatter );
 
// Using the ofPattern example where we reuse the common part of the pattern
DateTimeFormatter formatter = DateTimeFormatter.ofPattern( "yyyy-[MM-dd][MMM-dd]" );
LocalDate.parse( "2018-09-23", formatter );
LocalDate.parse( "2018-Sep-23", formatter );

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

1
2
3
4
5
LocalDate date = LocalDate.parse("2018-09-23");
// will result in 2018-09-232018-Sep-23
date.format(DateTimeFormatter.ofPattern("[yyyy-MM-dd][yyyy-MMM-dd]" ));
// will result in 2018-09-23Sep-23
date.format(DateTimeFormatter.ofPattern( "yyyy-[MM-dd][MMM-dd]" ));

Поскольку мы находимся в 21 веке, очевидно, что мы должны учитывать глобализацию, и мы хотим предложить локализованные даты для наших пользователей. Чтобы гарантировать, что ваш DateTimeFormatter возвращает конкретную локаль, вы можете просто сделать следующее:

1
2
3
4
DateTimeFormatter formatter = DateTimeFormatter.ofPattern( "EEEE, MMM dd, yyyy" ).withLocale(Locale.UK);
 
 
DateTimeFormatter formatter = new DateTimeFormatterBuilder().appendPattern("yyyy-MMM-dd" ).toFormatter(Locale.UK);

Чтобы узнать, какие локали доступны, вы можете использовать Locale.getAvailableLocales() .

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

1
2
3
4
5
// The issue: this will throw an exception.
LocalDate date = LocalDate.parse("2018-02-15T13:45");
// We provide a DateTimeFormatter that can parse the given date representation.
// The result will be a LocalDate holding 2018-02-15.
LocalDate date = LocalDate.parse("2018-02-15T13:45", DateTimeFormatter.ISO_LOCAL_DATE_TIME);

Давайте создадим средство форматирования, которое может обрабатывать шаблоны даты, времени и даты-времени ISO.

1
2
3
4
5
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
    .appendOptional( DateTimeFormatter.ISO_LOCAL_DATE )
    .optionalStart().appendLiteral( "T" ).optionalEnd()
    .appendOptional( DateTimeFormatter.ISO_LOCAL_TIME )
    .toFormatter();

Теперь мы можем отлично выполнить все следующее:

1
2
3
4
5
6
7
// results in 2018-03-16
LocalDate date = LocalDate.parse( "2018-03-16T06:30", formatter );
LocalDate date = LocalDate.parse( "2018-03-16", formatter );
// results in 06:30
LocalTime time = LocalTime.parse( "2018-03-16T06:30", formatter );
LocalTime time = LocalTime.parse( "06:30", formatter );
LocalDateTime localDateTime = LocalDateTime.parse( "2018-03-16T06:30", formatter );

Теперь, где появляется следующий номер? Что если вы попытаетесь разобрать шаблон даты для LocalDateTime ? Что, если вы ожидаете LocalTime и вам дается представление даты или наоборот?

1
2
3
// will throw an exception
LocalDateTime localDateTime = LocalDateTime.parse("2018-03-16", formatter);
LocalDate localDate = LocalDate.parse("06:30", formatter);

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

Если мы начнем с LocalDateTime и вам просто понадобится LocalDate или LocalTime , вы получите соответствующую часть LocalDateTime . Чтобы создать LocalDateTime , нам понадобятся значения по умолчанию для даты и времени, когда он LocalDateTime . Допустим, что если вы не предоставите информацию о дате, мы вернем сегодняшнюю дату, а если вы не предоставите время, мы предположим, что вы имели в виду начало дня.

Поскольку мы возвращаем LocalDateTime , он не будет анализироваться в LocalDate или LocalTime , поэтому давайте используем ConversionService чтобы получить правильный тип.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
TemporalQuery<TemporalAccessor> myCustomQuery = new MyCustomTemporalQuery();
// results in 2018-03-16
LocalDateTime localDateTime = conversionService.convert( formatter.parse( "2018-03-16", myCustomQuery ), LocalDateTime.class );
// results in 00:00
LocalTime localTime = conversionService.convert( formatter.parse( "2018-03-16", myCustomQuery ), LocalTime.class );
 
class MyCustomTemporalQuery implements TemporalQuery<TemporalAccessor>
{
    @Override
    public TemporalAccessor queryFrom( TemporalAccessor temporal ) {
        LocalDate date = temporal.isSupported( ChronoField.EPOCH_DAY )
            ? LocalDate.ofEpochDay( temporal.getLong( ChronoField.EPOCH_DAY ) ) : LocalDate.now();
        LocalTime time = temporal.isSupported( ChronoField.NANO_OF_DAY )
            ? LocalTime.ofNanoOfDay( temporal.getLong( ChronoField.NANO_OF_DAY ) ) : LocalTime.MIN;
        return LocalDateTime.of( date, time );
    }
}

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

Чтобы узнать, как составлять допустимые временные шаблоны, ознакомьтесь с документацией DateTimeFormatter .

Вывод

Большинству новых функций требуется некоторое время, чтобы понять и привыкнуть к ним, и API даты / времени Java 8 ничем не отличается. Новые API предоставляют нам лучший доступ к необходимому правильному формату, а также более стандартизированный и читаемый способ работы с операциями даты и времени. Используя эти советы и приемы, мы можем в значительной степени охватить все наши варианты использования.

Опубликовано на Java Code Geeks с разрешения Стивена Гентенса, партнера нашей программы JCG. Смотреть оригинальную статью здесь: Java 8 Дата и время

Мнения, высказанные участниками Java Code Geeks, являются их собственными.