Чтобы отметить дни на календаре в Java, достаточно пакета java.time: дата хранится в LocalDate, месяц — в YearMonth, день недели — в перечислении DayOfWeek, а отмеченные дни — в обычном Set<LocalDate>. Старые классы Date, Calendar и SimpleDateFormat в новом коде не нужны, но встречаются в legacy-проектах, и у них есть ловушки, которые разберем в конце.
Содержание
- Мини-словарь: какой класс за что отвечает
- Полный пример: календарь месяца с отмеченными днями
- Как отметить дни по правилу, а не списком
- Типовые операции с датами
- С какого дня начинается неделя
- Сегодняшняя дата и часовой пояс
- Legacy: Calendar, Date и SimpleDateFormat
- Если не получилось
- Выводы
- Где применяется / связь с практикой
- FAQ
Все примеры проверены 24.09.2026 на Java 25 LTS (Eclipse Temurin 25.0.4), запуск одной командой java Файл.java.
Мини-словарь: какой класс за что отвечает
Главная путаница новичков — «дата» в Java бывает разной. Для календаря важно различать четыре класса:
| Класс | Что хранит | Пример значения | Когда брать |
|---|---|---|---|
LocalDate |
дату без времени и часового пояса | 2026-09-24 |
ячейки календаря, дни рождения, дедлайны |
YearMonth |
год и месяц | 2026-09 |
«лист» календаря на месяц |
LocalDateTime |
дату и время без пояса | 2026-09-24T10:30 |
локальное расписание |
ZonedDateTime / Instant |
момент на шкале времени (с поясом / в UTC) | 2026-09-24T07:30:00Z |
события, логи, серверное время |
Для задачи «отметить дни» нужен именно LocalDate: у дня в календаре нет часов и минут, а значит, нет и риска, что отметка «съедет» на соседний день из-за пояса.
Полный пример: календарь месяца с отмеченными днями
Программа печатает сентябрь 2026 года, неделя начинается с понедельника, отмеченные дни помечены звездочкой.
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.format.TextStyle;
import java.util.Locale;
import java.util.Set;
public class MarkedCalendar {
static void print(YearMonth month, Set<LocalDate> marked) {
Locale ru = Locale.of("ru");
System.out.println(month.getMonth().getDisplayName(TextStyle.FULL_STANDALONE, ru)
+ " " + month.getYear());
for (DayOfWeek d : DayOfWeek.values()) {
System.out.printf("%4s", d.getDisplayName(TextStyle.SHORT, ru));
}
System.out.println();
LocalDate first = month.atDay(1);
int shift = first.getDayOfWeek().getValue() - 1; // пн = 1 ... вс = 7
System.out.print(" ".repeat(shift));
for (int day = 1; day <= month.lengthOfMonth(); day++) {
LocalDate date = month.atDay(day);
String cell = marked.contains(date) ? "*" + day : String.valueOf(day);
System.out.printf("%4s", cell);
if (date.getDayOfWeek() == DayOfWeek.SUNDAY) {
System.out.println();
}
}
if (month.atEndOfMonth().getDayOfWeek() != DayOfWeek.SUNDAY) {
System.out.println();
}
}
public static void main(String[] args) {
Set<LocalDate> marked = Set.of(
LocalDate.of(2026, 9, 1),
LocalDate.of(2026, 9, 15),
LocalDate.of(2026, 9, 24));
print(YearMonth.of(2026, 9), marked);
}
}
Фактический вывод:
сентябрь 2026
пн вт ср чт пт сб вс
*1 2 3 4 5 6
7 8 9 10 11 12 13
14 *15 16 17 18 19 20
21 22 23 *24 25 26 27
28 29 30
Как это работает
YearMonth.of(2026, 9)— «лист» календаря. Месяцы вjava.timeнумеруются с 1, сентябрь — это 9.month.atDay(1).getDayOfWeek().getValue()возвращает номер дня недели по ISO: понедельник = 1, воскресенье = 7. 1 сентября 2026 — вторник (2), поэтому перед первой датой печатается одна пустая ячейка.lengthOfMonth()сам учитывает длину месяца и високосный февраль — вручную считать 28/29/30/31 не нужно.- Отметка — проверка
marked.contains(date).LocalDateкорректно сравнивается черезequalsиhashCode, поэтому его можно класть вSetиMap. - После воскресенья — перенос строки; последняя проверка не дает лишней пустой строки, если месяц кончается воскресеньем (так выглядит май 2026).
TextStyle.FULL_STANDALONE дает название месяца в именительном падеже («сентябрь»). Обычный TextStyle.FULL для русской локали вернет родительный («сентября») — он нужен в датах вида «24 сентября».
Как отметить дни по правилу, а не списком
Часто нужно отметить не конкретные даты, а «все понедельники» или «все рабочие дни». Для этого есть datesUntil (Java 9+): он дает поток дат, правая граница не включается.
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.YearMonth;
import java.util.List;
import java.util.Set;
public class Rules {
public static void main(String[] args) {
YearMonth ym = YearMonth.of(2026, 9);
LocalDate from = ym.atDay(1);
LocalDate to = ym.atEndOfMonth().plusDays(1); // граница не включается
List<LocalDate> mondays = from.datesUntil(to)
.filter(d -> d.getDayOfWeek() == DayOfWeek.MONDAY)
.toList();
System.out.println(mondays);
Set<DayOfWeek> weekend = Set.of(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY);
long workdays = from.datesUntil(to)
.filter(d -> !weekend.contains(d.getDayOfWeek()))
.count();
System.out.println(workdays);
}
}
[2026-09-07, 2026-09-14, 2026-09-21, 2026-09-28]
22
Список mondays можно сразу превратить в Set и передать в print из первого примера. Число 22 — это будни без учета праздников: производственного календаря в JDK нет, праздники и переносы добавляют отдельным набором дат (из справочника компании или внешнего источника) и вычитают так же через filter.
Типовые операции с датами
Один пример на все частые вопросы: день недели, сдвиг, ближайший понедельник, конец месяца, разница в днях.
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.util.Locale;
public class DateOps {
public static void main(String[] args) {
LocalDate d = LocalDate.of(2026, 9, 24);
Locale ru = Locale.of("ru");
System.out.println(d.getDayOfWeek());
System.out.println(d.plusDays(10));
System.out.println(d.with(TemporalAdjusters.next(DayOfWeek.MONDAY)));
System.out.println(d.with(TemporalAdjusters.lastDayOfMonth()));
System.out.println(d.with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY)));
System.out.println(ChronoUnit.DAYS.between(d, LocalDate.of(2026, 12, 31)));
System.out.println(d.isLeapYear() + " " + LocalDate.of(2028, 1, 1).isLeapYear());
DateTimeFormatter f = DateTimeFormatter.ofPattern("d MMMM yyyy, EEEE", ru);
System.out.println(d.format(f));
System.out.println(LocalDate.parse("24.09.2026", DateTimeFormatter.ofPattern("dd.MM.yyyy")));
System.out.println(LocalDate.of(2026, 1, 31).plusMonths(1));
}
}
THURSDAY
2026-10-04
2026-09-28
2026-09-30
2026-09-07
98
false true
24 сентября 2026, четверг
2026-09-24
2026-02-28
Что стоит запомнить из вывода:
LocalDateнеизменяем:plusDaysиwithвозвращают новую дату, исходнаяdне меняется. Результат нужно присвоить:d = d.plusDays(1).TemporalAdjusters.nextберет строго следующий понедельник; если сегодня понедельник и он тоже подходит — используйтеnextOrSame.ChronoUnit.DAYS.between(a, b)не включает конечную дату: с 24 сентября по 31 декабря — 98 дней разницы.plusMonths(1)от 31 января дает 28 февраля: день обрезается до последнего допустимого, исключения нет.
С какого дня начинается неделя
В примере календаря неделя жестко начинается с понедельника — это стандарт ISO и привычный для России вариант. Если календарь должен подстраиваться под страну пользователя, первый день недели берут из WeekFields:
import java.time.temporal.WeekFields;
import java.util.Locale;
public class WF {
public static void main(String[] a) {
System.out.println(WeekFields.of(Locale.of("ru")).getFirstDayOfWeek());
System.out.println(WeekFields.of(Locale.of("ru", "RU")).getFirstDayOfWeek());
System.out.println(WeekFields.of(Locale.US).getFirstDayOfWeek());
System.out.println(WeekFields.ISO.getFirstDayOfWeek());
}
}
SUNDAY
MONDAY
SUNDAY
MONDAY
Ловушка в первой строке: первый день недели зависит от страны, а не от языка. Локаль ru без страны на Java 25 вернула воскресенье, ru-RU — понедельник. Поэтому для недельной сетки передавайте локаль со страной или явно WeekFields.ISO.
Чтобы сетка из первого примера подстраивалась под страну, в ней меняются три места: заголовок начинается с firstDay, сдвиг первой даты считается от него по модулю 7, а строка переносится после дня firstDay.minus(1), а не после воскресенья:
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.format.TextStyle;
import java.time.temporal.WeekFields;
import java.util.Locale;
public class LocaleCalendar {
static void print(YearMonth month, Locale locale) {
DayOfWeek firstDay = WeekFields.of(locale).getFirstDayOfWeek();
DayOfWeek lastDay = firstDay.minus(1);
for (int i = 0; i < 7; i++) {
System.out.printf("%4s", firstDay.plus(i).getDisplayName(TextStyle.SHORT, locale));
}
System.out.println();
LocalDate first = month.atDay(1);
int shift = (first.getDayOfWeek().getValue() - firstDay.getValue() + 7) % 7;
System.out.print(" ".repeat(shift));
for (int day = 1; day <= month.lengthOfMonth(); day++) {
LocalDate date = month.atDay(day);
System.out.printf("%4d", day);
if (date.getDayOfWeek() == lastDay) {
System.out.println();
}
}
if (month.atEndOfMonth().getDayOfWeek() != lastDay) {
System.out.println();
}
}
public static void main(String[] args) {
print(YearMonth.of(2026, 9), Locale.US);
}
}
Для Locale.US неделя начинается с воскресенья, и 1 сентября 2026 (вторник) встает в третью колонку:
Sun Mon Tue Wed Thu Fri Sat
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
С Locale.of("ru", "RU") тот же код печатает сетку с понедельника, как в первом примере. Отметки добавляются так же: проверкой marked.contains(date) в цикле.
Сегодняшняя дата и часовой пояс
LocalDate.now() берет дату в часовом поясе JVM. На сервере в UTC около полуночи по Москве «сегодня» будет вчерашним днем, и отметка «сегодня» в календаре окажется не в той клетке. Надежнее указать пояс явно: LocalDate.now(ZoneId.of("Europe/Moscow")). Для хранения моментов событий берут Instant или ZonedDateTime, а в LocalDate переводят только для показа.
Legacy: Calendar, Date и SimpleDateFormat
Старый API появился в JDK 1.0-1.1 и до сих пор живет в старом коде и библиотеках. Три главные ловушки — в одном прогоне:
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
public class Legacy {
public static void main(String[] args) {
Calendar c = new GregorianCalendar(2026, 9, 1); // задумано 1 сентября
SimpleDateFormat f = new SimpleDateFormat("dd.MM.yyyy");
System.out.println(f.format(c.getTime()));
Calendar ok = new GregorianCalendar(2026, Calendar.SEPTEMBER, 1);
System.out.println(f.format(ok.getTime()));
System.out.println(ok.get(Calendar.MONTH));
System.out.println(ok.get(Calendar.DAY_OF_WEEK) + " " + Calendar.TUESDAY);
DateTimeFormatter wrong = DateTimeFormatter.ofPattern("dd.MM.YYYY", Locale.of("ru", "RU"));
System.out.println(LocalDate.of(2025, 12, 29).format(wrong));
Date legacy = ok.getTime();
LocalDate modern = legacy.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
System.out.println(modern);
}
}
01.10.2026
01.09.2026
8
3 3
29.12.2026
2026-09-01
Разбор по строкам вывода:
- Месяцы в
Calendarс нуля.new GregorianCalendar(2026, 9, 1)— это 1 октября, а не сентября. Исправление — константыCalendar.SEPTEMBER(их значение 8) или переход наLocalDate.of(2026, 9, 1). - Дни недели в
Calendarнумеруются иначе, чем вjava.time: воскресенье = 1, вторник = 3. ВDayOfWeekвторник — 2. При переносе кода номера не совпадают, сравнивайте с константами. YYYY— это год недели, а не календарный год. 29 декабря 2025 года уже относится к первой неделе 2026-го, и шаблонdd.MM.YYYYпечатает «29.12.2026». Ошибка проявляется только в последние дни декабря, поэтому ее легко пропустить в тестах. Правильно —yyyy(илиuuuu).- Переход на новый API:
Date->Instant->ZonedDateTimeв нужном поясе ->LocalDate, как в последней строке.
Еще две ловушки, которые в этом прогоне не видны. Calendar и Date изменяемые: если отдать объект в другой метод, его могут сдвинуть через add или set. SimpleDateFormat не потокобезопасен: общий экземпляр в многопоточном сервере дает перепутанные даты. DateTimeFormatter неизменяем и безопасен для совместного использования.
| Задача | Legacy | java.time |
|---|---|---|
| дата без времени | Calendar с обнуленными часами |
LocalDate |
| месяц сентябрь | 8 или Calendar.SEPTEMBER |
9 или Month.SEPTEMBER |
| прибавить дни | cal.add(Calendar.DAY_OF_MONTH, 10) меняет объект |
d.plusDays(10) возвращает новую дату |
| форматирование | SimpleDateFormat, не потокобезопасен |
DateTimeFormatter, неизменяем |
| високосный год | new GregorianCalendar().isLeapYear(y) (метод экземпляра) |
LocalDate.isLeapYear(), Year.isLeap(y) |
Если не получилось
- Дата сдвинута на месяц вперед — в коде
Calendarмесяц передан числом как в жизни. Используйте константы илиjava.time. - В конце декабря год «прыгает» на следующий — в шаблоне
YYYYвместоyyyy. - Неделя в сетке начинается с воскресенья —
WeekFieldsполучил локаль без страны (на Java 25 это воскресенье независимо от локали JVM); передайтеru-RUилиWeekFields.ISO. - Отмеченный день не подсвечивается — в наборе лежит
LocalDateTimeилиDate, а сравниваетсяLocalDate: разные типы не равны. Приводите все кLocalDate. - Вместо русских названий «кракозябры» — кодировка консоли не совпадает с выводом JVM (частый случай в Windows). Помогает перевод консоли в UTF-8 или запуск из IDE.
Locale.ofне компилируется — метод появился в Java 19. На старых версиях используйтеnew Locale("ru", "RU").
Выводы
- Для календаря с отметками достаточно
YearMonth(лист месяца),LocalDate(ячейка) иSet<LocalDate>(отметки); длину месяца и високосность API считает сам. - Отмечать дни по правилу удобно через
datesUntilи фильтр поDayOfWeek; праздников в JDK нет, их добавляют отдельным набором. - Первый день недели зависит от страны локали:
ruбез страны дает воскресенье,ru-RUиWeekFields.ISO— понедельник. - Главные ловушки legacy-API: месяцы
Calendarс нуля,YYYYвместоyyyy, изменяемостьCalendarи непотокобезопасныйSimpleDateFormat.
Где применяется / связь с практикой
Календарная логика есть почти в любом прикладном проекте: бронирование, графики смен, дедлайны задач, напоминания, отчеты «за месяц». Ошибка на один день или один месяц в таких местах стоит дорого, поэтому работу с java.time, коллекциями и потоками стоит отработать на задачах, а не только прочитать.
Освойте тему на практике
Базу языка, включая коллекции, Stream API и работу со стандартной библиотекой, системно разбирают на курсе «Java разработчик. Базовый уровень». Чтобы познакомиться с форматом и преподавателями, можно начать с бесплатных открытых уроков.
FAQ
Как вывести календарь в графическом окне, а не в консоли?
Логика та же: YearMonth дает число дней и сдвиг первого дня, а ячейки рисуются компонентом интерфейса (например, сеткой 7 столбцов в JavaFX или Swing). Отмеченные дни подсвечивают по тому же Set<LocalDate>.
Как хранить отмеченные даты в базе данных?
Для даты без времени подходит SQL-тип DATE; современные JDBC-драйверы умеют читать его как LocalDate через rs.getObject(col, LocalDate.class). Поддержку стоит проверить в документации конкретного драйвера.
Можно ли смешивать Date и LocalDate в одном проекте?
Можно, если конвертировать на границе со старым кодом (toInstant() и Date.from(...)) и явно указывать часовой пояс. Внутри новой логики лучше держать только java.time.



