W3docs

Устаревший класс Date в Java

Устаревший класс java.util.Date — почему его заменил java.time и как выполнять преобразование между ними.

java.util.Date был оригинальным классом даты и времени в Java, доступным начиная с Java 1.0 в 1995 году. Он по-прежнему входит в JDK; в новом коде использовать его не следует, однако вы будете встречать его в библиотеках, базах данных (java.sql.Date наследует его) и любом коде, написанном до примерно 2014 года. Эта глава призвана показать, как перейти от него к java.time.

Коротко: Date — это обёртка вокруг значения типа long, хранящего миллисекунды от эпохи, примерно так же, как Instant хранит пару (секунды, наносекунды). Поэтому естественное преобразование — Date ↔ Instant. Для всего остального (год, месяц, день, календарная арифметика) сначала преобразуйте значение в java.time.

Что на самом деле представляет собой Date

public class Date implements Cloneable, Comparable<Date>, Serializable {
  private long fastTime;        // milliseconds since 1970-01-01T00:00:00Z
}

Вот и всё реальное состояние. Date — это точка во времени, измеряемая в миллисекундах с начала Unix-эпохи в UTC. Несмотря на название, класс не хранит календарную дату — getYear() и аналогичные методы вычисляют значения из миллисекундного значения с учётом часового пояса JVM по умолчанию, что и является источником печально известных проблем этого API.

Date now = new Date();                                       // current moment
Date epoch = new Date(0);                                    // 1970-01-01T00:00:00Z
Date fromMs = new Date(1_700_000_000_000L);

long ms = now.getTime();                                     // milliseconds since epoch

new Date() и Date.getTime() — это два метода, которые выдержали проверку временем. Всё остальное либо устарело, либо является потенциальным источником ошибок.

Устаревшие аксессоры календаря

Эти методы были помечены как устаревшие в Java 1.1 (1997), когда был добавлен класс Calendar:

date.getYear();                                              // year - 1900   (deprecated)
date.getMonth();                                             // 0-11          (deprecated)
date.getDate();                                              // 1-31, day of month (deprecated)
date.getDay();                                               // 0-6, day of week (deprecated)
date.getHours(); date.getMinutes(); date.getSeconds();       // local-zone reads (deprecated)

Эти пометки об устаревании существуют уже 28 лет. Методы по-прежнему работают. Опасные особенности:

  • getYear() возвращает year - 1900. Для 2025 года возвращается 125. Это безусловный претендент на звание «самого странного решения в JDK».
  • getMonth() возвращает 0-11. Январь — 0, декабрь — 11. Ошибки на единицу гарантированы, если написать getMonth() + 1 и один раз забыть об этом.
  • Каждый аксессор читает значение в часовом поясе JVM по умолчанию. Один и тот же Date на двух машинах в разных часовых поясах даст разные результаты getDate().

Не вызывайте эти методы. Как только вы потянетесь к date.getYear(), преобразуйте значение в Instant/ZonedDateTime и используйте современные аксессоры.

Мост Date ↔ Instant

Date legacy = new Date();
Instant inst = legacy.toInstant();                            // since Java 8

Instant other = Instant.parse("2025-11-04T19:30:00Z");
Date back = Date.from(other);                                 // since Java 8

toInstant() и Date.from(...) — это современные методы преобразования, добавленные вместе с java.time. Это единственные вызовы java.util.Date, которые стоит писать в новом коде.

Преобразование теряет данные в одном направлении: Date имеет точность до миллисекунды, Instant — до наносекунды. Цикл Instant → Date → Instant усекает суб-миллисекундные наносекунды:

Instant high = Instant.parse("2025-11-04T19:30:00.123456789Z");
Instant low = Date.from(high).toInstant();
// low = 2025-11-04T19:30:00.123Z — the 456789 nanos are gone

Для серверных меток времени это допустимо; для захвата событий с высоким разрешением оставайтесь в Instant на протяжении всего пути.

java.sql.Date и java.sql.Timestamp

Типы JDBC являются подклассами java.util.Date:

  • java.sql.Date — дата без времени (компонент времени принудительно устанавливается в 00:00:00 в некотором часовом поясе). Вводящее в заблуждение название; внутри это всё та же обёртка над миллисекундами с эпохи.
  • java.sql.Time — время без даты.
  • java.sql.Timestamp — как Date, но с точностью до наносекунды.

Все они имеют методы преобразования, добавленные в JDK 8:

java.sql.Date    sqlDate  = java.sql.Date.valueOf(LocalDate.of(2025, 11, 4));
LocalDate localDate = sqlDate.toLocalDate();

java.sql.Timestamp ts = java.sql.Timestamp.from(Instant.now());
Instant inst = ts.toInstant();

Современные JDBC-драйверы также принимают типы java.time напрямую через setObject/getObject — в новом коде пропускайте типы java.sql.* и используйте LocalDate/Instant. Эти преобразования существуют для кода, который должен взаимодействовать с драйвером или фреймворком, ещё не прошедшим миграцию.

Сравнение и сортировка

Date реализует Comparable<Date>. Порядок — по миллисекундам с эпохи, как у Instant. Поэтому сортировка List<Date> работает так же, как сортировка List<Instant>.

equals сравнивает базовый long. Два значения Date равны тогда и только тогда, когда у них одинаковое значение в миллисекундах. Хеширование работает корректно (это (int)(time ^ (time >>> 32))), поэтому Date допустимо использовать как ключ HashMap — хотя и здесь Instant является современной альтернативой.

Изменяемость

Главная скрытая ловушка: Date является изменяемым объектом.

Date d = new Date();
d.setTime(0);                                                // mutates d in place

Это означает, что любой метод, принимающий или возвращающий Date, потенциально опасен — вызывающий код может изменить значение после передачи; вызываемый код может изменить значение, которое держит вызывающий. Библиотечный код защитно копирует (new Date(d.getTime())) каждый получаемый Date. Именно от такого шаблонного кода java.time полностью избавился, сделав каждый тип неизменяемым.

В устаревшем коде относитесь к любому полю Date так, как будто оно может измениться в любой момент. Преобразовывайте в Instant на границе API, если вам нужен стабильный снимок.

SimpleDateFormat: другой устаревший компонент

В паре с Date в старом коде часто используется java.text.SimpleDateFormat:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String s = sdf.format(new Date());
Date d = sdf.parse("2025-11-04");

SimpleDateFormat не является потокобезопасным. Совместное использование одного экземпляра несколькими потоками приводит к неверному выводу, исключениям или и тому, и другому — непредсказуемо. Стандартный обходной путь в устаревшем коде — ThreadLocal<SimpleDateFormat>; современная замена — DateTimeFormatter, который является потокобезопасным и пригодным для кэширования.

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

Практический пример: взаимодействие с устаревшим кодом

Программа ниже использует Date так, как он встречается в устаревшем API, и показывает путь преобразования в java.time для каждой операции. Читайте её как «рецепт миграции»: у каждого устаревшего вызова есть однострочный эквивалент через Instant или ZonedDateTime.

java— editable, runs on the server

Что следует вынести из выполнения программы:

  • Date.toString() выводит значение в часовом поясе JVM по умолчанию. Одно и то же значение Date отображается по-разному на UTC-сервере и ноутбуке с America/New_York. Это центральный конструктивный недостаток, который обусловил перепроектирование java.time — значение хранится в UTC, отображение происходит по локальному времени, а API не даёт простого способа понять, какой вид вы видите. Если вас волнует часовой пояс, преобразуйте в ZonedDateTime и явно укажите нужный пояс.
  • now.getYear() вернул 125 для 2025 года. Соглашение year - 1900 было ошибкой Java 1.0, которую так и не исправили; метод был помечен как устаревший в версии 1.1 и по-прежнему существует. Любой вызов getYear() у Date, возвращающий «125» или «85», — это ошибка, которая рано или поздно проявится у пользователя.
  • Instant.parse("...наносекунды") вывел девять знаков точности; то же значение, пройдя через Date.from и обратно, потеряло последние шесть. Для серверных журналов (точности до миллисекунды достаточно) усечение не имеет значения. Если вы захватываете событие с высокоточной временно́й меткой, не пускайте его через Date.
  • Устаревший вариант прибавления 1 дня выглядит как now.getTime() + 24L * 60 * 60 * 1000 — ручная арифметика, в которой легко ошибиться (забыть L и получить переполнение). Современный inst.plus(Duration.ofDays(1)) типобезопасен и читается сам по себе. При миграции замена каждого вычисления time + N * ms на Duration — это наиболее простое улучшение.
  • Демонстрация изменяемости в конце показала, что shared.setTime(0) меняет значение, видимое через оба ссылки. В многопоточном коде это состояние гонки; в однопоточном — всё равно ритуал защитного копирования, который JDK навязал каждой библиотеке. Современный API никогда не требует этого ритуала.

Что дальше

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

Практика

Практика
Старая библиотека возвращает `java.util.Date` из метода `getCreatedAt()`. Вы хотите узнать: 'Приходится ли это на тот же календарный день, что и сегодня в Нью-Йорке?'. Каков правильный путь?
Старая библиотека возвращает `java.util.Date` из метода `getCreatedAt()`. Вы хотите узнать: 'Приходится ли это на тот же календарный день, что и сегодня в Нью-Йорке?'. Каков правильный путь?
Was this page helpful?