skip to content

What are the main pitfalls of java.util.Calendar, including its month numbering?

level: juniorimportance: should knowfreq 55%

answer

  1. Calendar months 0-based: Jan=0, Dec=11
  2. Day-of-month 1-based — mixed conventions
  3. Mutable: add/set/roll change in place
  4. Magic int field constants
  5. Lenient rollover hides bugs; use LocalDate

basics

~10 s

Calendar months are 0-based, so January is 0 and December is 11, which causes off-by-one bugs. Calendar is also mutable, verbose, and easy to misuse. Prefer java.time's LocalDate, where months are 1-based.

solid answer

~40 s

java.util.Calendar was the intended fix for Date's deprecated field accessors, but it brought its own problems. The most infamous is 0-based months: Calendar.JANUARY is 0 and DECEMBER is 11, so new GregorianCalendar(2026, 5, 20) is actually June, not May. Days of month are 1-based, mixing conventions confusingly. Calendar is mutable, so calling add() or set() changes the object in place, which makes it unsafe to share and easy to corrupt accidentally. The API is verbose and stringly/int-typed: you set fields with magic int constants rather than named methods. It also rolls over silently on invalid values (set month to 13 and it becomes next January). Use java.time instead: LocalDate.of(2026, 6, 20) is unambiguous, months are 1-based or use the Month enum, and the types are immutable.

go deeper

for a junior

Remembers the 0-based month gotcha and that you should prefer LocalDate.

for a middle

Lists the mutability, magic-int API, and lenient-rollover pitfalls and writes correct java.time equivalents.

for a senior

Explains why mutability and leniency hide bugs, the mixed 0/1-based conventions, and migrates Calendar-based arithmetic to immutable java.time calls.

for a principal

Defines codebase policy banning Calendar/Date, handles locale/time-zone-sensitive arithmetic correctly, and reviews edge cases (DST, end-of-month, leap years) in the migration.

## Why Calendar exists When most of `java.util.Date`'s field methods (`getMonth`, `getYear`, …) were deprecated in Java 1.1, `java.util.Calendar` was introduced as the replacement for doing calendar arithmetic — adding days, extracting the month, handling time zones and locales. `GregorianCalendar` is the concrete implementation almost everyone uses. Unfortunately the design has several traps. ## Pitfall 1: 0-based months (the famous one) In `Calendar`, **months are numbered from 0**: January = 0, February = 1, …, December = 11. So: ```java new GregorianCalendar(2026, 5, 20); // NOT May 20 — this is JUNE 20 ``` The constant `Calendar.JANUARY` equals 0, so reading code that uses the named constants is safer, but raw integers (common in data parsing) silently produce a month that is off by one. Confusingly, the **day of month is 1-based** and the **year is the real year** — so within one constructor call you mix a 0-based field with 1-based fields. ## Pitfall 2: mutability A `Calendar` is **mutable** — its state changes in place. Methods like `add(Calendar.DAY_OF_MONTH, 7)`, `set(...)`, and `roll(...)` modify the same object and return `void`. Consequences: - You cannot safely share a `Calendar` across threads (no synchronization). - Passing one to a method risks that method mutating it. - It is awkward to express "give me a new date 7 days later" without cloning first. ## Pitfall 3: verbose, int-keyed, error-prone API You read and write fields with **magic int constants**: `cal.get(Calendar.HOUR_OF_DAY)`, `cal.set(Calendar.MONTH, 6)`. There is no compile-time check that the value range is sensible for the field, and the field constants themselves are easy to confuse (`HOUR` is 12-hour, `HOUR_OF_DAY` is 24-hour; `DAY_OF_WEEK` is 1=Sunday). ## Pitfall 4: lenient (silent) rollover By default a `Calendar` is **lenient**: setting an out-of-range value silently rolls over instead of throwing. `set(Calendar.MONTH, 13)` becomes February of the next year; day 32 of January becomes February 1. This hides bugs. (`setLenient(false)` makes it throw, but few people set it.) ## Pitfall 5: it pairs with the broken Date `Calendar.getTime()` returns a `java.util.Date`, inheriting all of Date's problems, and `SimpleDateFormat` (the legacy formatter) is built around this stack and is itself not thread-safe. ## The modern fix: java.time ```java LocalDate d = LocalDate.of(2026, 6, 20); // unambiguous; month is 1-based LocalDate later = d.plusDays(7); // returns a NEW immutable LocalDate Month m = d.getMonth(); // a type-safe enum, not an int ``` `java.time` types are immutable (thread-safe), use **1-based months** (or the `Month` enum), validate input (an invalid date throws `DateTimeException`), and offer fluent arithmetic that returns new objects. There is essentially no reason to reach for `Calendar` in new code.

  • What does new GregorianCalendar(2026, 11, 25) represent?
    December 25, 2026 — because month 11 is December in Calendar's 0-based numbering, not November.
  • How do you avoid the silent rollover behavior?
    Call calendar.setLenient(false) so out-of-range field values throw an exception, or better, switch to java.time which validates dates and throws DateTimeException.

saying these in an interview costs you the question

  • Saying Calendar months are 1-based
  • Assuming set() with an out-of-range value throws by default (it rolls over unless lenient is off)
  • Treating Calendar as immutable or thread-safe
  • Confusing HOUR (12h) with HOUR_OF_DAY (24h)

context