☕ Java
Calendar
Calendar (and its near-universally-used concrete subclass GregorianCalendar) is the abstract legacy class that served as the primary way to perform calendar-field arithmetic and access on java.util.Date values prior to java.time, introduced to fix Date's deprecated field accessors but ultimately inheriting and compounding many of the same design problems. This entry covers Calendar's field-based mutable design and its confusing zero-indexed month constants, how Calendar.getInstance() depends on locale and time zone in ways that surprise newcomers, how field arithmetic via add()/roll() works and where the two diverge, and why java.time fully superseded rather than merely supplementing Calendar.
Calendar's Field-Based, Mutable Design
Calendar is an abstract class — code almost always interacts with it via Calendar.getInstance(), a factory method that returns a concrete subclass instance (GregorianCalendar in the overwhelming majority of real-world cases, since that's the calendar system used by default almost everywhere) rather than via any public constructor, similar in spirit to java.time's factory-method pattern but otherwise diverging sharply in actual design. Internally, a Calendar represents its date and time as a set of individually mutable integer fields (YEAR, MONTH, DAY_OF_MONTH, HOUR_OF_DAY, MINUTE, SECOND, MILLISECOND, and several others) accessed and modified through get(field) and set(field, value), rather than through type-specific, self-documenting methods the way java.time exposes getYear() or plusDays(n) directly — meaning code working with Calendar fields is, by construction, working with raw integers and integer constants rather than a more strongly-typed, harder-to-misuse API surface.
Like java.util.Date, Calendar is mutable — calling set(field, value) modifies the existing Calendar instance in place rather than returning a new one, carrying forward the exact same shared-mutable-reference hazard already covered for Date, compounded by the fact that Calendar objects are commonly held onto and reused for sequential field manipulations, increasing the surface area for one part of a codebase to unexpectedly mutate state another part is still relying on.
Java
// ── Calendar.getInstance() — factory method, not a public constructor ─
Calendar cal = Calendar.getInstance(); // returns a GregorianCalendar in virtually all real cases
// ── Field-based access — raw integer constants, not self-documenting methods
cal.set(Calendar.YEAR, 2024);
cal.set(Calendar.DAY_OF_MONTH, 15);
int year = cal.get(Calendar.YEAR); // raw int, retrieved via a raw int CONSTANT
int dayOfMonth = cal.get(Calendar.DAY_OF_MONTH);
// ── Mutability — the SAME hazard as Date, carried forward into Calendar
Calendar shared = Calendar.getInstance();
someMethodThatTakesACalendar(shared);
shared.set(Calendar.YEAR, 1999); // if someMethodThatTakesACalendar() kept a reference,
// its view of "shared" just silently changed too — same underlying objectThe Zero-Indexed Month Trap and Locale/Zone Dependence
One of Calendar's most famous and persistently bug-inducing design choices is that Calendar.MONTH is zero-indexed — Calendar.JANUARY is 0, Calendar.FEBRUARY is 1, and so on through Calendar.DECEMBER being 11 — in sharp, easy-to-forget contrast to DAY_OF_MONTH, which is conventionally one-indexed exactly as a human would expect (the first day of the month is 1, not 0). This single inconsistency, between a zero-indexed month field sitting right next to a one-indexed day field on the very same object, has been responsible for an enormous, well-documented volume of off-by-one bugs across Java's history — code that does cal.set(Calendar.MONTH, 6) intending to set July, while actually setting August, is a mistake made constantly by developers reasonably assuming consistent indexing across adjacent fields. java.time's LocalDate and the Month enum deliberately moved to one-indexed months (1 = January) specifically to eliminate this exact class of error going forward.
Separately, Calendar.getInstance() is itself both locale-sensitive and time-zone-sensitive in ways that surprise newcomers expecting a single, neutral "current calendar" — the no-argument overload implicitly uses the JVM's default Locale (affecting things like which day is considered the first day of the week) and default TimeZone (affecting what "now" actually resolves to), both of which are ambient, global JVM state that can differ across environments, differ across machines, or even be changed at runtime by other code — Calendar.getInstance(TimeZone) and Calendar.getInstance(Locale) overloads exist specifically to make that dependency explicit and controllable rather than implicit and ambient, but reaching for them requires already knowing the implicit defaults are a hazard in the first place.
Java
// ── The infamous zero-indexed MONTH field — DAY_OF_MONTH is NOT zero-indexed
Calendar cal = Calendar.getInstance();
cal.set(Calendar.YEAR, 2024);
cal.set(Calendar.MONTH, 6); // ← developer THINKS this means July...
cal.set(Calendar.DAY_OF_MONTH, 15); // ← ...and correctly uses 15 for the 15th (1-indexed here!)
System.out.println(cal.get(Calendar.MONTH)); // 6 — but Calendar.JULY is actually 6... wait:
System.out.println(Calendar.JULY); // 6 — OK this one happens to be right
System.out.println(Calendar.AUGUST); // 7 — but set(MONTH, 7) would be AUGUST, not July!
// The constants exist PRECISELY because raw integers here are so easy to get wrong
// ── java.time's fix — Month is one-indexed, matching DAY_OF_MONTH's convention
LocalDate clearDate = LocalDate.of(2024, Month.JULY, 15); // unambiguous, self-documenting
// ── getInstance() depends on AMBIENT, implicit JVM-wide Locale/TimeZone
Calendar implicitDefaults = Calendar.getInstance(); // uses default Locale + TimeZone
Calendar explicitZone = Calendar.getInstance(TimeZone.getTimeZone("Asia/Tokyo")); // explicit insteadField Arithmetic — add() vs roll(), and Why java.time Fully Superseded Calendar
Calendar provides two distinct, easy-to-confuse methods for field arithmetic: add(field, amount), which behaves the way most developers actually want — incrementing the given field and correctly carrying any overflow into the next larger field, exactly the way plusMonths() does on a java.time type — versus roll(field, amount), which increments only the specified field and explicitly does not propagate overflow into any larger field at all, instead wrapping back around within that field's own valid range while leaving every other field completely untouched. Calling roll(Calendar.MONTH, 1) on a Calendar set to December does not advance the year the way add() would — it wraps the month back around to January while the year field stays exactly where it was, which is a genuinely useful, deliberate behavior for certain "wrap within this field only" use cases, but is also frequently reached for by mistake when add() was actually what was intended, given how similar the two method signatures and names appear at a glance.
Ultimately, java.time was designed as a wholesale replacement for Calendar rather than an incremental fix, specifically because Calendar's underlying problems — its mutability, its locale/zone-ambient getInstance() defaults, its raw-integer field-constant API, and its zero-indexed month inconsistency — were all structural design choices baked into the class's original API shape, not isolated bugs that could be patched without breaking backward compatibility; this is exactly why Calendar (along with Date and SimpleDateFormat) remains un-deprecated and fully present in the JDK to this day for legacy compatibility, while essentially all new code is written against java.time instead, with Calendar relegated to the same boundary-conversion role Date occupies — toInstant() exists on Calendar as well, for exactly the same bridging purpose, immediately handing off to java.time for any actual logic.
Java
// ── add() — overflow CORRECTLY carries into the next larger field ─────
Calendar addCal = Calendar.getInstance();
addCal.set(2024, Calendar.DECEMBER, 15);
addCal.add(Calendar.MONTH, 1);
System.out.println(addCal.get(Calendar.YEAR) + "-" + (addCal.get(Calendar.MONTH) + 1));
// 2025-1 — correctly rolled over into January of the FOLLOWING year
// ── roll() — wraps WITHIN the field only, larger fields stay UNCHANGED ─
Calendar rollCal = Calendar.getInstance();
rollCal.set(2024, Calendar.DECEMBER, 15);
rollCal.roll(Calendar.MONTH, 1);
System.out.println(rollCal.get(Calendar.YEAR) + "-" + (rollCal.get(Calendar.MONTH) + 1));
// 2024-1 — STILL 2024! Month wrapped back to January, but the year did NOT advance
// ── Calendar's java.time equivalent — and the boundary-conversion bridge
LocalDate javaTimeEquivalent = LocalDate.of(2024, 12, 15).plusMonths(1);
System.out.println(javaTimeEquivalent); // 2025-01-15 — unambiguous, matches add()'s behavior
Calendar legacyCal = Calendar.getInstance();
Instant fromCalendar = legacyCal.toInstant(); // same bridging pattern as Date.toInstant()Related Topics in Date Time API
LocalDate
LocalDate is the java.time class (introduced in Java 8 as part of the redesigned date-time API replacing the deprecated java.util.Date/Calendar) representing a date without a time component or a time zone — just a year, month, and day on the ISO-8601 calendar. This entry covers why LocalDate deliberately carries no time-of-day or zone information, how it is created and parsed, how it is manipulated via its immutable, fluent with*/plus*/minus* methods, how two LocalDate instances are compared and the period between them measured, and the common pitfalls of treating it as if it secretly carried a time or zone after all.
LocalTime
LocalTime is the java.time class representing a time-of-day without any date or time zone attached — just an hour, minute, second, and nanosecond on a 24-hour clock. This entry covers what LocalTime intentionally omits and why, how to create and parse LocalTime values with varying precision, how arithmetic on LocalTime wraps around midnight instead of rolling into a different day, and how LocalTime is meant to be combined with LocalDate (via LocalDateTime) rather than used as a date substitute.
LocalDateTime
LocalDateTime is the java.time class combining a LocalDate and a LocalTime into a single date-and-time-of-day value that still, deliberately, carries no time zone or UTC-offset information. This entry covers why LocalDateTime is best understood as a composite of the two simpler types rather than a primitive type in its own right, how it is created from and decomposed back into its LocalDate/LocalTime parts, why the absence of a zone makes it unsuitable for representing one unambiguous, real-world instant, and how it relates to and converts into the zone-aware types (ZonedDateTime, Instant) when an actual moment in time is what's needed.
ZonedDateTime
ZonedDateTime is the java.time class representing a LocalDateTime combined with a ZoneId, fully resolved against the time zone's rules to also carry a specific UTC offset — making it the type capable of representing one genuinely unambiguous real-world instant while still preserving human-readable, zone-local wall-clock fields. This entry covers the relationship between ZoneId and the resolved offset, how daylight saving transitions create gaps and overlaps that ZonedDateTime must resolve, how zone-aware arithmetic differs from plain LocalDateTime arithmetic across those transitions, and when to prefer ZonedDateTime over the simpler offset-only OffsetDateTime or the zone-independent Instant.