Класс LocalDate в Java: методы и примеры
Строка date.plusDays(1); компилируется и выполняется без ошибок, но date после неё остаётся прежней. LocalDate неизменяем: plusDays() возвращает новый объект, и если его не присвоить (date = date.plusDays(1);), результат просто теряется.
LocalDate в Java (java.time.LocalDate) — неизменяемый потокобезопасный класс, который хранит дату без времени и часового пояса: год, месяц и день месяца, например 2024-03-15. Он появился в Java 8 в пакете java.time и заменяет java.util.Date и Calendar там, где нужна только календарная дата: день рождения, дата платежа, срок действия документа.
Как создать LocalDate: of(), now(), parse()
Объект LocalDate создаётся только статическими фабричными методами — конструктора у класса нет. LocalDate.of(год, месяц, день) задаёт конкретную дату, LocalDate.now() возвращает текущую дату, а LocalDate.parse("2024-03-15") разбирает строку в формате ISO-8601 (yyyy-MM-dd).
import java.time.LocalDate;
import java.time.Month;
public class CreateLocalDateExample {
public static void main(String[] args) {
LocalDate newYear2001 = LocalDate.of(2001, 1, 1);
LocalDate newYear2002 = LocalDate.of(2002, Month.JANUARY, 1);
LocalDate fromString = LocalDate.parse("2024-03-15");
LocalDate day32 = LocalDate.ofYearDay(2001, 32); // 32-й день года
LocalDate epoch = LocalDate.ofEpochDay(0); // 0 дней от 1970-01-01
LocalDate today = LocalDate.now();
System.out.println(newYear2001);
System.out.println(newYear2002);
System.out.println(fromString);
System.out.println(day32);
System.out.println(epoch);
System.out.println(today);
}
} 2001-01-01
2002-01-01
2024-03-15
2001-02-01
1970-01-01
2026-10-18 (текущая дата на момент запуска) LocalDate.now() берёт дату в часовом поясе JVM по умолчанию. Если важно, какой сейчас день в конкретном городе, передайте зону: LocalDate.now(ZoneId.of("Asia/Tokyo")) — в Токио новый день наступает на 6 часов раньше, чем в Москве.
Метод LocalDate.of проверяет дату сразу при создании. Несуществующая дата не «перетекает» на следующий месяц, как в старом Calendar, а вызывает исключение:
LocalDate.of(2023, 2, 29); // DateTimeException: Invalid date 'February 29' as '2023' is not a leap year
LocalDate.of(2023, 4, 31); // DateTimeException: Invalid date 'APRIL 31'
LocalDate.of(2023, 13, 1); // DateTimeException: Invalid value for MonthOfYear (valid values 1 - 12): 13
LocalDate.parse("2023-1-5"); // DateTimeParseException: Text '2023-1-5' could not be parsed at index 5 Месяцы считаются с 1
В LocalDate январь — это 1, декабрь — 12. В java.util.Calendar январь был 0, и LocalDate.of(2024, 0, 1) по старой привычке выбросит DateTimeException. Чтобы не путаться, используйте перечисление Month: LocalDate.of(2024, Month.JANUARY, 1). Почему старые классы заменили — в уроке Дата и время в Java 8.
Получение года, месяца и дня
Год, месяц и день из LocalDate получают геттерами: getYear(), getMonthValue() (число 1–12), getMonth() (перечисление Month), getDayOfMonth(), getDayOfYear() и getDayOfWeek() (перечисление DayOfWeek). Длину месяца и года и признак високосного года дают lengthOfMonth(), lengthOfYear() и isLeapYear().
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.Month;
public class LocalDateGetExample {
public static void main(String[] args) {
LocalDate date = LocalDate.of(2024, 3, 15);
int year = date.getYear();
int month = date.getMonthValue();
Month monthAsEnum = date.getMonth(); // как перечисление
int dayYear = date.getDayOfYear();
int dayMonth = date.getDayOfMonth();
DayOfWeek dayWeekEnum = date.getDayOfWeek(); // как перечисление
System.out.println("Год: " + year);
System.out.println("Месяц: " + month);
System.out.println("Название месяца: " + monthAsEnum);
System.out.println("День в году: " + dayYear);
System.out.println("День месяца: " + dayMonth);
System.out.println("День недели: " + dayWeekEnum);
System.out.println("Дней в месяце: " + date.lengthOfMonth());
System.out.println("Дней в году: " + date.lengthOfYear());
System.out.println("Високосный: " + date.isLeapYear());
}
} Год: 2024
Месяц: 3
Название месяца: MARCH
День в году: 75
День месяца: 15
День недели: FRIDAY
Дней в месяце: 31
Дней в году: 366
Високосный: true Тот же результат даёт универсальный метод get(TemporalField) с константами ChronoField. Он удобен, когда поле выбирается динамически:
import java.time.LocalDate;
import java.time.temporal.ChronoField;
public class LocalDateChronoFieldExample {
public static void main(String[] args) {
LocalDate date = LocalDate.of(2024, 3, 15);
System.out.println("Год: " + date.get(ChronoField.YEAR));
System.out.println("Месяц: " + date.get(ChronoField.MONTH_OF_YEAR));
System.out.println("День в году: " + date.get(ChronoField.DAY_OF_YEAR));
System.out.println("День месяца: " + date.get(ChronoField.DAY_OF_MONTH));
System.out.println("День недели: " + date.get(ChronoField.DAY_OF_WEEK)); // понедельник = 1
System.out.println("Дней от 1970-01-01: " + date.getLong(ChronoField.EPOCH_DAY));
}
} Год: 2024
Месяц: 3
День в году: 75
День месяца: 15
День недели: 5
Дней от 1970-01-01: 19797 Обратите внимание на последнюю строку: EPOCH_DAY нужно читать через getLong(). Вызов date.get(ChronoField.EPOCH_DAY) выбросит UnsupportedTemporalTypeException: Invalid field 'EpochDay' for get() method, use getLong() instead, потому что значение поля может не поместиться в int. Поля времени (ChronoField.HOUR_OF_DAY и т.п.) у LocalDate нет совсем — запрос тоже закончится UnsupportedTemporalTypeException.
Сравнение дат
Две даты LocalDate сравнивают методами isBefore(), isAfter() и isEqual(), которые возвращают boolean. Для сортировки используется compareTo(): класс реализует Comparable. Оператор == для дат не подходит — он сравнивает ссылки, а не значения.
import java.time.LocalDate;
import java.time.Month;
public class CompareLocalDateExample {
public static void main(String[] args) {
LocalDate newYear2001 = LocalDate.of(2001, 1, 1);
LocalDate newYear2002 = LocalDate.of(2002, Month.JANUARY, 1);
System.out.println("isAfter: " + newYear2001.isAfter(newYear2002));
System.out.println("isBefore: " + newYear2001.isBefore(newYear2002));
System.out.println("isEqual: " + newYear2001.isEqual(newYear2002));
System.out.println("equals: " + newYear2001.equals(LocalDate.parse("2001-01-01")));
System.out.println("compareTo: " + newYear2001.compareTo(newYear2002));
System.out.println("compareTo: " + newYear2001.compareTo(LocalDate.of(2005, 6, 1)));
}
} isAfter: false
isBefore: true
isEqual: false
equals: true
compareTo: -1
compareTo: -4 Последняя строка показывает неочевидный момент: compareTo() у LocalDate возвращает разницу (сначала по годам, затем по месяцам и дням), а не строго -1, 0 или 1. Поэтому результат сравнивайте с нулём (< 0, > 0), а не с -1. Для LocalDate методы equals() и isEqual() дают одинаковый результат; разница появляется только при сравнении дат из разных календарных систем (например, JapaneseDate) — там isEqual() сравнивает саму точку на временной оси.
Изменение даты методами with()
Методы with() возвращают копию даты, в которой заменено одно поле: withYear(), withMonth(), withDayOfMonth(), withDayOfYear() или универсальный with(ChronoField, значение). Исходный объект не меняется. Для «умных» корректировок — последний день месяца, следующий понедельник — служит with(TemporalAdjuster).
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAdjusters;
public class WithLocalDateExample {
public static void main(String[] args) {
LocalDate newYear2001 = LocalDate.of(2001, 1, 1); // понедельник
LocalDate newYear2003 = newYear2001.with(ChronoField.YEAR, 2003);
LocalDate newYear2004 = newYear2001.withYear(2004);
LocalDate december2001 = newYear2001.withMonth(12);
LocalDate february2001 = newYear2001.withDayOfYear(32);
LocalDate xmas2001 = newYear2001.withMonth(12).withDayOfMonth(25);
LocalDate lastDay = newYear2001.with(TemporalAdjusters.lastDayOfMonth());
LocalDate nextMonday = newYear2001.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
LocalDate firstOfNext = newYear2001.with(TemporalAdjusters.firstDayOfNextMonth());
LocalDate feb = LocalDate.of(2001, 1, 31).withMonth(2);
System.out.println("newYear2003: " + newYear2003);
System.out.println("newYear2004: " + newYear2004);
System.out.println("december2001: " + december2001);
System.out.println("february2001: " + february2001);
System.out.println("xmas2001: " + xmas2001);
System.out.println("lastDay: " + lastDay);
System.out.println("nextMonday: " + nextMonday);
System.out.println("firstOfNext: " + firstOfNext);
System.out.println("feb: " + feb);
}
} newYear2003: 2003-01-01
newYear2004: 2004-01-01
december2001: 2001-12-01
february2001: 2001-02-01
xmas2001: 2001-12-25
lastDay: 2001-01-31
nextMonday: 2001-01-08
firstOfNext: 2001-02-01
feb: 2001-02-28 Два нюанса из вывода. TemporalAdjusters.next(MONDAY) ищет понедельник строго после текущей даты, поэтому от понедельника 1 января получилось 8 января; если подходит и сам текущий день, используйте nextOrSame(). А withMonth(2) от 31 января не падает, а сдвигает день на последний допустимый — 28 февраля. Зато withDayOfMonth(30) для февральской даты выбросит DateTimeException: Invalid date 'FEBRUARY 30': здесь вы явно просите несуществующий день.
Методы plus() и minus()
Чтобы прибавить или вычесть дни, недели, месяцы или годы, используют plusDays(), plusWeeks(), plusMonths(), plusYears(), парные им minus...() и универсальные plus(количество, ChronoUnit) / minus(количество, ChronoUnit). Каждый вызов возвращает новую дату.
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
public class PlusMinusLocalDateExample {
public static void main(String[] args) {
LocalDate newYear2001 = LocalDate.of(2001, 1, 1);
// Добавление
LocalDate newYear2005 = newYear2001.plusYears(4);
LocalDate march2001 = newYear2001.plusMonths(2);
LocalDate jan15Year2001 = newYear2001.plusDays(14);
LocalDate jan22Year2001 = newYear2001.plusWeeks(3);
LocalDate newYear2006 = newYear2001.plus(5, ChronoUnit.YEARS);
// Вычитание
LocalDate newYear2000 = newYear2001.minusYears(1);
LocalDate nov2000 = newYear2001.minusMonths(2);
LocalDate dec30Year2000 = newYear2001.minusDays(2);
LocalDate dec25Year2000 = newYear2001.minusWeeks(1);
LocalDate newYear1999 = newYear2001.minus(2, ChronoUnit.YEARS);
System.out.println("newYear2005: " + newYear2005);
System.out.println("march2001: " + march2001);
System.out.println("jan15Year2001: " + jan15Year2001);
System.out.println("jan22Year2001: " + jan22Year2001);
System.out.println("newYear2006: " + newYear2006);
System.out.println("newYear2000: " + newYear2000);
System.out.println("nov2000: " + nov2000);
System.out.println("dec30Year2000: " + dec30Year2000);
System.out.println("dec25Year2000: " + dec25Year2000);
System.out.println("newYear1999: " + newYear1999);
}
} newYear2005: 2005-01-01
march2001: 2001-03-01
jan15Year2001: 2001-01-15
jan22Year2001: 2001-01-22
newYear2006: 2006-01-01
newYear2000: 2000-01-01
nov2000: 2000-11-01
dec30Year2000: 2000-12-30
dec25Year2000: 2000-12-25
newYear1999: 1999-01-01 Что выведет этот код? Конец месяца — место, где арифметика дат ведёт себя не так, как ожидают:
LocalDate jan31 = LocalDate.of(2001, 1, 31);
System.out.println(jan31.plusMonths(1)); // 2001-02-28
System.out.println(jan31.plusMonths(1).plusMonths(1)); // 2001-03-28
System.out.println(jan31.plusMonths(2)); // 2001-03-31 После первого plusMonths(1) день «обрезается» до 28, и второй вызов уже не знает, что исходно было 31-е. Если считаете, например, ежемесячные даты платежей, всегда прибавляйте N месяцев к исходной дате, а не по одному месяцу к предыдущему результату.
Единицы времени LocalDate не поддерживает: методов plusHours() у класса нет, а date.plus(1, ChronoUnit.HOURS) выбросит UnsupportedTemporalTypeException: Unsupported unit: Hours. Для даты со временем нужен LocalDateTime. Прибавить сразу «1 год 2 месяца 3 дня» можно объектом Period: date.plus(Period.of(1, 2, 3)) — подробно в уроке Класс Period.
Сколько дней между двумя датами
Количество дней между двумя LocalDate возвращает ChronoUnit.DAYS.between(начало, конец) или эквивалентный начало.until(конец, ChronoUnit.DAYS). Результат имеет тип long, конечная дата в подсчёт не входит, а если конец раньше начала, число будет отрицательным.
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
public class DaysBetweenExample {
public static void main(String[] args) {
LocalDate start = LocalDate.of(2001, 1, 1);
LocalDate end = LocalDate.of(2001, 3, 1);
System.out.println(ChronoUnit.DAYS.between(start, end)); // 59
System.out.println(start.until(end, ChronoUnit.DAYS)); // 59
System.out.println(ChronoUnit.MONTHS.between(start, end)); // 2
System.out.println(ChronoUnit.DAYS.between(end, start)); // -59
}
} Если нужна разница в виде «N лет, M месяцев, K дней», используйте Period.between() — см. урок Использование класса Period.
Разбор и форматирование строки
LocalDate.toString() и LocalDate.parse(String) всегда работают с форматом ISO-8601 yyyy-MM-dd. Для любого другого формата, например 15.03.2024, нужен DateTimeFormatter: его передают в parse(текст, formatter) и в format(formatter).
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class LocalDateFormatExample {
public static void main(String[] args) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy");
LocalDate date = LocalDate.parse("15.03.2024", formatter);
System.out.println(date); // 2024-03-15
System.out.println(date.format(formatter)); // 15.03.2024
}
} Символы шаблона, локали и разбор названий месяцев разобраны в уроке Класс DateTimeFormatter.
Конвертация LocalDate в java.util.Date и java.sql.Date
LocalDate в java.util.Date напрямую не превращается: у даты нет времени и зоны, а Date — это момент времени. Поэтому сначала выбирают зону и время суток: Date.from(localDate.atStartOfDay(zone).toInstant()). Обратно: date.toInstant().atZone(zone).toLocalDate(). С java.sql.Date проще — есть готовые java.sql.Date.valueOf(localDate) и sqlDate.toLocalDate().
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
public class LocalDateConvertExample {
public static void main(String[] args) {
ZoneId zone = ZoneId.systemDefault();
LocalDate localDate = LocalDate.of(2024, 3, 15);
// LocalDate -> java.util.Date (полночь в зоне zone)
Date utilDate = Date.from(localDate.atStartOfDay(zone).toInstant());
// java.util.Date -> LocalDate
LocalDate back = utilDate.toInstant().atZone(zone).toLocalDate();
LocalDate back9 = LocalDate.ofInstant(utilDate.toInstant(), zone); // Java 9+
// LocalDate <-> java.sql.Date
java.sql.Date sqlDate = java.sql.Date.valueOf(localDate);
LocalDate fromSql = sqlDate.toLocalDate();
System.out.println(utilDate);
System.out.println(back);
System.out.println(back9);
System.out.println(sqlDate);
System.out.println(fromSql);
}
} Fri Mar 15 00:00:00 MSK 2024 (при зоне по умолчанию Europe/Moscow)
2024-03-15
2024-03-15
2024-03-15
2024-03-15 | Направление | Код | Нюанс |
|---|---|---|
| LocalDate → java.util.Date | Date.from(ld.atStartOfDay(zone).toInstant()) | Время станет 00:00 в выбранной зоне |
| java.util.Date → LocalDate | d.toInstant().atZone(zone).toLocalDate() | В Java 9+ короче: LocalDate.ofInstant(d.toInstant(), zone) |
| LocalDate → java.sql.Date | java.sql.Date.valueOf(ld) | Зона не нужна |
| java.sql.Date → LocalDate | sqlDate.toLocalDate() | sqlDate.toInstant() выбрасывает UnsupportedOperationException |
| Миллисекунды → LocalDate | Instant.ofEpochMilli(ms).atZone(zone).toLocalDate() | Про Instant — в уроке Класс Instant |
| JDBC 4.2+ | rs.getObject("col", LocalDate.class), ps.setObject(1, ld) | Конвертация через java.sql.Date не нужна |
Одна зона в обе стороны
Конвертируйте туда и обратно с одной и той же ZoneId. Если записать полночь 15 марта по Москве (UTC+3), а читать в ZoneOffset.UTC, получится 14 марта, 21:00 — то есть LocalDate 2024-03-14, на день раньше.
Шпаргалка по методам LocalDate
Все методы LocalDate делятся на пять групп: создание, чтение полей, сравнение, получение изменённой копии и конвертация. Методы, которые «меняют» дату, на самом деле возвращают новый объект.
| Метод | Что делает | Граничный случай / ошибка |
|---|---|---|
of(y, m, d) | Создаёт дату | DateTimeException для несуществующей даты |
now(), now(ZoneId) | Текущая дата | Без зоны берётся зона JVM по умолчанию |
parse(text) | Разбирает строку yyyy-MM-dd | DateTimeParseException для «2023-1-5» |
getYear(), getMonthValue(), getDayOfMonth() | Год, месяц (1–12), день | Месяц с 1, а не с 0 |
getDayOfWeek() | День недели (DayOfWeek) | Как число через ChronoField: понедельник = 1 |
lengthOfMonth(), isLeapYear() | Дней в месяце, високосный ли год | Февраль 2024 — 29, февраль 2023 — 28 |
isBefore(), isAfter(), isEqual() | Сравнение | NullPointerException, если аргумент null |
withMonth(), withDayOfMonth(), with(TemporalAdjuster) | Копия с заменённым полем | withMonth(2) от 31-го даёт 28/29-е |
plusDays() … minusYears() | Копия, сдвинутая на N единиц | plus(1, HOURS) → UnsupportedTemporalTypeException |
atStartOfDay(), atTime() | Переход к LocalDateTime | atStartOfDay(zone) возвращает ZonedDateTime |
toEpochDay() | Число дней от 1970-01-01 | Для дат до 1970 года отрицательное |
На чём чаще всего ошибаются
Большинство ошибок с LocalDate связаны с тремя вещами: неизменяемостью объекта, нумерацией месяцев и тем, что у даты нет времени. Ниже — типичные промахи и исправления.
| Ошибка | Что происходит | Как правильно |
|---|---|---|
date.plusDays(1); | Результат теряется, date не меняется | date = date.plusDays(1); |
LocalDate.of(2024, 0, 1) | DateTimeException: месяцы 1–12 | LocalDate.of(2024, Month.JANUARY, 1) |
d1 == d2 | Сравнение ссылок, обычно false | d1.isEqual(d2) или d1.equals(d2) |
d1.compareTo(d2) == -1 | Может вернуть -4 и условие не сработает | d1.compareTo(d2) < 0 или d1.isBefore(d2) |
LocalDate.parse("15.03.2024") | DateTimeParseException | LocalDate.parse(s, DateTimeFormatter.ofPattern("dd.MM.yyyy")) |
Ежемесячная дата через цепочку plusMonths(1) | 31-е «съезжает» на 28-е навсегда | start.plusMonths(n) от исходной даты |
Часто задаваемые вопросы
Чем LocalDate отличается от LocalDateTime и java.util.Date?
LocalDate хранит только дату (2024-03-15), LocalDateTime — дату и время без зоны (2024-03-15T10:30), а java.util.Date — момент времени в миллисекундах от 1970 года и к тому же изменяемый. Для дня рождения или срока оплаты подходит LocalDate, для времени события — LocalDateTime или Instant. Как переводить одно в другое — в разделе Конвертация.
Потокобезопасен ли LocalDate? Можно ли хранить его в static final поле?
Да. LocalDate неизменяем, поэтому один объект можно без синхронизации использовать из любого числа потоков и смело объявлять константой, например static final LocalDate RELEASE = LocalDate.of(2014, 3, 18);. В отличие от SimpleDateFormat, потокобезопасен и DateTimeFormatter.
Как отсортировать список дат LocalDate?
LocalDate реализует Comparable, поэтому достаточно list.sort(Comparator.naturalOrder()) или Collections.sort(list) — от ранней даты к поздней. Обратный порядок: list.sort(Comparator.reverseOrder()). Найти самую позднюю дату: Collections.max(list).
Как протестировать код с LocalDate.now(), если дата каждый день разная?
Передавайте в код объект Clock и вызывайте LocalDate.now(clock). В продакшене это Clock.systemDefaultZone(), а в тесте — фиксированные часы: Clock.fixed(Instant.parse("2024-03-15T10:00:00Z"), ZoneOffset.UTC). Тогда LocalDate.now(clock) всегда вернёт 2024-03-15.
Практика по теме
Закрепите урок на задачах — у каждой есть уровень и оценка времени.
- Практикуем методы LocalDate
- Календарь праздников на текущий год
- Возраст и дни до дня рождения
- Дата выдачи зарплаты в отчёте
Видео объяснение
Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.
Комментарии