Устаревший класс 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 epochnew 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 8toInstant() и 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.
Что следует вынести из выполнения программы:
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.