Как получить данные за последние N дней с помощью java.time

Забудьте про старые классы дат! Узнайте, как Java 8 и новый API легко решают задачи за последние N дней. Фильтруйте данные без боли и рутины.

С выходом Java 8 был представлен новый пакет java.time, который полностью заменил устаревшие классы java.util.Date и java.util.Calendar. Он предлагает immutable, потокобезопасные и удобные в использовании типы для работы с датой и временем. В этой статье мы подробно рассмотрим, как с помощью этого API получить данные за последние N дней – будь то фильтрация коллекций в памяти, запрос к базе данных или работа с потоками.


Содержание

  1. Обзор пакета java.time
  2. Как получить текущую дату/время
  3. Выборка за последние дни из коллекции (in‑memory)
  4. Аналогичная выборка с использованием Stream API
  5. Выборка за последние дни из реляционной БД (JDBC)
  6. Частые ошибки и как их избежать
  7. Лучшие практики

Обзор пакета 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), не стесняйтесь задавать их в комментариях!