С выходом Java 8 был представлен новый пакет java.time, который полностью заменил устаревшие классы java.util.Date и java.util.Calendar. Он предлагает immutable, потокобезопасные и удобные в использовании типы для работы с датой и временем. В этой статье мы подробно рассмотрим, как с помощью этого API получить данные за последние N дней – будь то фильтрация коллекций в памяти, запрос к базе данных или работа с потоками.
Содержание
- Обзор пакета
java.time - Как получить текущую дату/время
- Выборка за последние дни из коллекции (in‑memory)
- Аналогичная выборка с использованием Stream API
- Выборка за последние дни из реляционной БД (JDBC)
- Частые ошибки и как их избежать
- Лучшие практики
Обзор пакета java.time
Основные классы, которые нам понадобятся:
LocalDate– дата без времени и часового пояса (год‑месяц‑день).LocalDateTime– дата и время без часового пояса.ZonedDateTime– дата‑время с учётом часового пояса.Instant– момент времени в UTC (эпоха Unix).DurationиPeriod– интервалы времени.DateTimeFormatter– форматирование и парсинг строк.
Все эти классы immutable, то есть любые операции возвращают новый объект, оставляя исходный неизменным.
Как получить текущую дату/время
Для большинства задач достаточно текущей даты в нужном часовом поясе:
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.ZoneId;
// Текущая дата (без времени) в системном часовом поясе
LocalDate today = LocalDate.now;
// Текущая дата и время
LocalDateTime now = LocalDateTime.now;
// Текущий момент с учётом конкретного часового пояса (например, Москва)
ZonedDateTime nowInMoscow = ZonedDateTime.now(ZoneId.of("Europe/Moscow"));
Если нужно работать с UTC (например, при хранении в базе как TIMESTAMP WITH TIME ZONE), удобно использовать Instant:
Instant nowUtc = Instant.now; // момент в UTC
Выборка за последние дни из коллекции (in‑memory)
Предположим, у нас есть список событий, каждое из которых содержит поле LocalDateTime timestamp. Нужно оставить только те, которые произошли за последние N дней.
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
class Event {
private String id;
private LocalDateTime timestamp;
// конструкторы, геттеры, сеттеры omitted for brevity
}
public class RecentEventsFilter {
/* Возвращает подсписок событий за последние {@code days} дней (включая сегодня).
*/
public static List<Event> filterLastNDays(List<Event> events, int days) {
// Граница: N дней назад от текущего момента (без учёта времени)
LocalDateTime cutoff = LocalDateTime.now.minus(days, ChronoUnit.DAYS);
List<Event> result = new ArrayList<>;
for (Event e : events) {
if (e.getTimestamp.isAfter(cutoff) || e.getTimestamp.isEqual(cutoff)) {
result.add(e);
}
}
return result;
}
// Пример использования
public static void main(String[] args) {
List<Event> allEvents = /* загрузка данных */;
List<Event> recent = filterLastNDays(allEvents, 7); // последние 7 дней
System.out.println("Найдено событий: " + recent.size);
}
} Обратите внимание, что мы использовали ChronoUnit.DAYS для вычитания дней – это делает код более читаемым и позволяет легко менять единицу измерения (часы, минуты и т.д.).
Аналогичная выборка с использованием Stream API
Java 8 Stream API идеально сочетается с новым Date‑Time API. Ниже пример той же фильтрации, но в функциональном стиле:
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.stream.Collectors;
public class RecentEventsStream {
public static List<Event> filterLastNDaysStream(List<Event> events, int days) {
LocalDateTime cutoff = LocalDateTime.now.minus(days, ChronoUnit.DAYS);
return events.stream
.filter(e -> e.getTimestamp.isAfter(cutoff) ||
e.getTimestamp.isEqual(cutoff))
.collect(Collectors.toList);
}
}
Stream позволяет легко добавить дополнительные этапы обработки (сортировка, преобразование, агрегация) без промежуточных коллекций.
Выборка за последние дни из реляционной БД (JDBC)
При работе с базами данных удобно передавать границу даты как параметрPreparedStatement; Это позволяет СУБД использовать индексы по колонке с датой.
Допустим, таблица events имеет колонку event_time TIMESTAMP WITH TIME ZONE.
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
public class EventDao {
private final Connection connection;
public EventDao(Connection connection) {
this.connection = connection;
}
/*
Возвращает события за последние {@code days} дней.
*/ public List<Event> findRecentEvents(int days) throws Exception {
// Граница в UTC (можно также использовать ZonedDateTime с нужным зоной)
ZonedDateTime cutoffZdt = ZonedDateTime.now(ZoneId.of("UTC"))
.minusDays(days);
Timestamp cutoffTs = Timestamp.from(cutoffZdt.toInstant);
String sql = "SELECT id, event_time, payload FROM events " +
"WHERE event_time >= ? " + // индекс-friendly условие
"ORDER BY event_time DESC";
try (PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setTimestamp(1, cutoffTs);
try (ResultSet rs = ps.executeQuery) {
List<Event> result = new ArrayList<>;
while (rs.next) {
Event e = new Event;
e.setId(rs.getLong("id"));
// Преобразуем TIMESTAMP WITH TIME ZONE в Instant, затем в нужный тип
Instant instant = rs.getTimestamp("event_time").toInstant;
e.setTimestamp(instant.atZone(ZoneId.systemDefault).toLocalDateTime);
e.setPayload(rs.getString("payload"));
result.add(e);
}
return result;
}
}
}
}
Важно:
- Использовать параметризованный запрос (
?) – это защищает от SQL‑инъекций и позволяет кэшировать план выполнения. - Если колонка хранит только дату (
DATE), достаточно сравнивать сjava.sql.Date. - Для часовых поясов лучше хранить время в UTC (
TIMESTAMP WITH TIME ZONEилиTIMESTAMP+ явное указание зоны) и выполнять все вычисления в UTC, а уже потом переводить в нужный зону для отображения.
Частые ошибки и как их избежать
Смешивание java.util.Date и java.time
Несмотря на наличие методов конвертации (toInstant, from), прямое смешивание приводит к путанице с часовыми поясами. Лучше сразу работать с новым API и конвертировать только на границе с legacy‑кодом (например, при работе с старыми библиотеками).
Неправильный учёт часового пояса при вычитании дней
Вычитание дней из LocalDateTime игнорирует часовой пояс. Если ваши данные привязаны к конкретному зоне, используйте ZonedDateTime или OffsetDateTime.
Использование now в многопоточной среде без учёта drift
Метод now возвращает момент вызова. Если между получением границы и выполнением запроса проходит значительное время, может возникнуть «сдвиг». Для критичных случаев вычисляйте границу один раз и передавайте её дальше как параметр.
Ошибочное сравнение с equals вместо isBefore/isAfter
Классы LocalDate* не переопределяют equals для сравнения по смыслу? На самом деле они переопределяют, но для диапазонов удобнее пользоваться специализированными методами (isBefore, isAfter, isEqual).
Лучшие практики
- Храните даты и времена в базе в UTC (
TIMESTAMP WITH TIME ZONEилиTIMESTAMP+ явное указание зоны). - Вычисляйте границу (
cutoff) один раз в начале операции и передавайте её как параметр. - Для фильтрации в памяти предпочитайте Stream API – он лаконичен и легко расширяется.
- Если нужно выполнять сложные расчёты (например, «рабочие дни», «последний пятница месяца»), используйте
TemporalAdjustersиз пакетаjava.time.temporal. - Логируйте значения границы и временные зоны при отладке – это помогает быстро обнаружить ошибки с часовыми поясами.
- При работе с legacy‑API используйте методы конверсии:
Date.from(Instant)иInstant.ofEpochMilli(date.getTime).
Java 8 Date‑Time API предоставляет мощный и безопасный инструментарий для работы с датами и временем. Выборка за последние дни становится простой задачей:
- Получите текущий момент (
LocalDateTime.now,ZonedDateTime.nowилиInstant.now). - Вычтите нужный интервал с помощью
minusиChronoUnit. - Сравните каждое событие с полученной границей (
isAfter,isBefore,isEqual). - При работе с БД передавайте границу как параметр
PreparedStatement– так вы получаете преимущества индексов и защиту от SQL‑инъекций.
Следуя рекомендациям из этой статьи, вы сможете писать надёжный, читаемый и легко поддерживаемый код, который корректно работает в разных часовых поясах и не подвержен типичным ошибкам, связанным со старыми классами java.util.Date и java.util.Calendar.
Если у вас остались вопросы или нужен пример для конкретного сценария (например, работа с OffsetDateTime в REST‑API), не стесняйтесь задавать их в комментариях!