🔒 Розділ 13 · Питання #21

Чому LocalDate, LocalDateTime є незмінними

Класи сучасного API дати та часу пакета java.time. (JSR-310), впроваджені в Java 8 (LocalDate, LocalTime, LocalDateTime, Instant, ZonedDateTime), були створені суворо незмінними...


🟢 Junior Level

30-секундна відповідь

Класи сучасного API дати та часу пакета java.time.* (JSR-310), впроваджені в Java 8 (LocalDate, LocalTime, LocalDateTime, Instant, ZonedDateTime), були створені суворо незмінними (Immutable) для виправлення катастрофічних архітектурних помилок застарілих класів java.util.Date та java.util.Calendar.

Головні причини незмінності:

  1. 100% Потокобезпечність «з коробки»: дату можна безпечно оголошувати як константу static final та розділяти між тисячами потоків без синхронізації та блокувань.
  2. Відсутність прихованих побічних ефектів: методи модифікації (такі як plusDays(), minusHours(), withYear()) не змінюють поточний об’єкт, а повертають абсолютно новий екземпляр.
  3. Відмова від захисного копіювання (Defensive Copying): гетери доменних сутностей і DTO можуть безпечно повертати посилання на дату напряму, не побоюючись мутацій.

Типова помилка розробників-початківців

LocalDate date = LocalDate.of(2025, Month.JANUARY, 1);

// ❌ ПОМИЛКА: Результат методу проігноровано, date залишився колишнім!
date.plusDays(10); 
System.out.println(date); // 2025-01-01 (дата НЕ змінилася!)

// ✅ ПРАВИЛЬНО: Присвоїти повернутий новий об'єкт змінній
LocalDate newDate = date.plusDays(10);
System.out.println(newDate); // 2025-01-11

Чому старий java.util.Date був катастрофою

Клас java.util.Date містив сетери setTime(), setYear(). Якщо метод повертав Date, зовнішній код міг змінити дату прямо всередині вашої JPA-сутності. А форматер SimpleDateFormat містив змінюваний внутрішній календар: паралельний парсинг дат із двох потоків призводив до пошкодження даних та аварійних збоїв із NumberFormatException.


🟡 Middle Level

4 переваги незмінності java.time для архітектури

Перевага Як проявляється в коді Вигода для системи
Потокобезпечність Об’єкти можна кешувати та використовувати в багатопотокових сервісах Нульові затримки, відсутність блокувань
Усунення Defensive Copy public LocalDate getDate() { return this.date; } Економія пам’яті купи, немає викликів new Date(time)
Безпечне форматування DateTimeFormatter потокобезпечний і незмінний Один статичний форматер на весь застосунок
Fluent API Ланцюжки викликів без side-effects date.plusMonths(1).withDayOfMonth(15)

Класи на основі значень (Value-Based Classes, JEP 390)

Усі класи пакета java.time офіційно є Value-Based Classes:

  1. Класи оголошені як final, а їхні поля — як private final.
  2. Конструктори закриті (створення тільки через статичні фабрики of(), now(), parse()).
  3. Метод equals() порівнює виключно значення полів, а не посилальну ідентичність.
  4. Заборона синхронізації за монітором (JEP 390): Спроба написати synchronized (localDate) в Java 16+ генерує попередження компілятора, а в майбутніх релізах (Project Valhalla) викине виняток IdentityException.

Instant vs LocalDateTime

  • Instant: абсолютна точка на фізичній часовій шкалі у всесвіті (зберігає long epochSecond та int nanos відносно 1970-01-01T00:00:00Z UTC). Ідеальний для логів, транзакцій у БД та черг Kafka.
  • LocalDateTime: календарна дата й час без прив’язки до часового поясу («2025-05-15 14:30»). Без ZoneId неможливо однозначно сказати, скільки часу минуло з цього моменту в іншій точці світу.

🔴 Senior Level

Внутрішня будова LocalDate та підготовка до Project Valhalla

Усередині LocalDate спроєктований гранично компактно і зберігає лише 3 примітивних поля:

public final class LocalDate implements ... {
    private final int year;
    private final short month;
    private final short day;
    // ...
}
  1. Підготовка до Project Valhalla (Value Objects):
    • У Project Valhalla клас LocalDate стане легковажним value class.
    • JVM повністю усуне 16-байтний заголовок об’єкта (Object Header). У масивах LocalDate[] елементи зберігатимуться послідовно в пам’яті як пласкі структури з 8 байтів, як примітиви в C/C++!
  2. JIT Escape Analysis та Scalar Replacement:
    • При виконанні ланцюжка викликів today.plusDays(1).withDayOfMonth(10) C2-компілятор за допомогою Escape Analysis доводить, що проміжний об’єкт не покидає метод.
    • Механізм Scalar Replacement розкладає поля дати прямо по регістрах процесора, повністю виключаючи виділення пам’яті в купі (Zero Garbage).

Чисто функціональні трансформації: Інтерфейс TemporalAdjuster

Незмінність дат дозволяє реалізовувати складні бізнес-правила розрахунку у функціональному стилі:

// Розрахунок наступного робочого дня без побічних ефектів:
LocalDate nextWorkingDay = date.with(temporal -> {
    DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
    int daysToAdd = switch (dow) {
        case FRIDAY -> 3;
        case SATURDAY -> 2;
        default -> 1;
    };
    return temporal.plus(daysToAdd, ChronoUnit.DAYS);
});

4 Tricky Questions

1. Чому в Java 16+ компілятор видає попередження при спробі синхронізації за об’єктом LocalDate (synchronized (date))? Що таке JEP 390?

Відповідь: JEP 390 (Warnings for Value-Based Classes) готує платформу Java до переходу на Project Valhalla:

  • Класи LocalDate, Instant, Integer визнані класами значень (Value-Based Classes). Їхня суть полягає в даних, а не в об’єктній ідентичності (object identity).
  • У Project Valhalla такі класи втратять заголовок об’єкта (Object Header), у якому зберігається бітове поле монітора блокування (Mark Word). Відповідно, примітив блокування monitorenter/monitorexit фізично перестане існувати для таких типів.
  • Починаючи з Java 16, використання синхронізації за Value-Based класами вважається грубою помилкою і превентивно підсвічується компілятором.

2. Як DateTimeFormatter досягає потокобезпечності без блокувань та ThreadLocal, на відміну від старого SimpleDateFormat?

Відповідь: SimpleDateFormat був мутабельним: він містив внутрішнє поле calendar, у яке записував проміжні стани під час парсингу. Одночасний виклик із двох потоків призводив до перезапису стану календаря прямо під час читання. DateTimeFormatter спроєктований повністю незмінним (Immutable):

  • Він зберігає лише незмінні правила розбору та шаблони.
  • При виклику методу parse() або format() усі тимчасові змінні, контексти розбору та акумулятори полів створюються локально на стеку поточного потоку (Parsed context).
  • Оскільки спільного мутабельного стану немає, форматер на 100% потокобезпечний і може бути безпечно оголошений як public static final.

3. Чому в розподілених мікросервісах та БД для фіксації часу подій категорично заборонено використовувати LocalDateTime?

Відповідь: LocalDateTime являє собою «людську дату на настінному годиннику» і не містить інформації про часовий пояс та зміщення (ZoneOffset).

  • Час 2025-01-01 12:00:00 у Токіо (UTC+9), Лондоні (UTC+0) та Сан-Франциско (UTC-8) настає з різницею в десятки годин.
  • Якщо сервіс в одному регіоні запише LocalDateTime в БД, а сервіс в іншому регіоні прочитає його, виникне викривлення бізнес-логіки та збій сортування подій.
  • Для часових міток подій необхідно використовувати Instant (фізичний timestamp) або OffsetDateTime (timestamp + точне зміщення).

4. Чи створює виклик date.plusDays(0) новий об’єкт у купі?

Відповідь: Ні! Реалізація методу plusDays(long daysToAdd) в OpenJDK містить швидку перевірку:

if (daysToAdd == 0) {
    return this;
}

Якщо кількість днів, що додаються, дорівнює нулю, метод миттєво повертає поточне посилання this без виділення пам’яті. Це можливо виключно завдяки незмінності: оскільки об’єкт незмінний у часі, повернення самого себе є абсолютно безпечним.


🎯 Шпаргалка для інтерв’ю

Головне про LocalDate та LocalDateTime

  • JSR-310 (Java 8): сучасне API дати та часу, яке повністю замінило Date та Calendar.
  • Незмінність: усі методи трансформації (plus, minus, with) повертають новий об’єкт.
  • Thread Safety: 100% потокобезпечні, не вимагають synchronized.
  • Value-Based Classes: заборонена синхронізація за монітором (synchronized(date)), готуються стати Value Objects у Project Valhalla.
  • Форматування: DateTimeFormatter незмінний і потокобезпечний (на відміну від SimpleDateFormat).

Старий Date vs Новий LocalDate / Instant

| Критерій | java.util.Date | LocalDate / Instant | | :— | :— | :— | | Мутабельність | Мутабельний (setTime) | Строго незмінний | | Потокобезпечність | ❌ Непотокобезпечний | 🟢 100% потокобезпечний | | Defensive Copying | Потрібне завжди | ❌ Не потрібне | | Таймзона | Неявно використовує системну | Явний поділ: Instant (UTC), ZonedDateTime (зона) |

Червоні прапорці на співбесіді (Чого говорити не можна)

  • ❌ «Виклик date.plusDays(5) змінює дату в змінній date» — метод повертає нову дату, стара не змінюється.
  • ❌ «SimpleDateFormat можна оголосити як public static final у контролері» — призведе до пошкодження даних у багатопотоковості; використовувати DateTimeFormatter.
  • ❌ «У JPA гетерах для LocalDate потрібно робити defensive copy» — LocalDate незмінний, копіювання не потрібне.

Пов’язані питання