Календарь в Java: как вывести месяц и отметить нужные дни

Календарь в Java: как вывести месяц и отметить нужные дни Полезное

Чтобы отметить дни на календаре в Java, достаточно пакета java.time: дата хранится в LocalDate, месяц — в YearMonth, день недели — в перечислении DayOfWeek, а отмеченные дни — в обычном Set<LocalDate>. Старые классы Date, Calendar и SimpleDateFormat в новом коде не нужны, но встречаются в legacy-проектах, и у них есть ловушки, которые разберем в конце.

Все примеры проверены 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

Как это работает

  1. YearMonth.of(2026, 9) — «лист» календаря. Месяцы в java.time нумеруются с 1, сентябрь — это 9.
  2. month.atDay(1).getDayOfWeek().getValue() возвращает номер дня недели по ISO: понедельник = 1, воскресенье = 7. 1 сентября 2026 — вторник (2), поэтому перед первой датой печатается одна пустая ячейка.
  3. lengthOfMonth() сам учитывает длину месяца и високосный февраль — вручную считать 28/29/30/31 не нужно.
  4. Отметка — проверка marked.contains(date). LocalDate корректно сравнивается через equals и hashCode, поэтому его можно класть в Set и Map.
  5. После воскресенья — перенос строки; последняя проверка не дает лишней пустой строки, если месяц кончается воскресеньем (так выглядит май 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

Разбор по строкам вывода:

  1. Месяцы в Calendar с нуля. new GregorianCalendar(2026, 9, 1) — это 1 октября, а не сентября. Исправление — константы Calendar.SEPTEMBER (их значение 8) или переход на LocalDate.of(2026, 9, 1).
  2. Дни недели в Calendar нумеруются иначе, чем в java.time: воскресенье = 1, вторник = 3. В DayOfWeek вторник — 2. При переносе кода номера не совпадают, сравнивайте с константами.
  3. YYYY — это год недели, а не календарный год. 29 декабря 2025 года уже относится к первой неделе 2026-го, и шаблон dd.MM.YYYY печатает «29.12.2026». Ошибка проявляется только в последние дни декабря, поэтому ее легко пропустить в тестах. Правильно — yyyy (или uuuu).
  4. Переход на новый 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.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)