☕ Java

Date Time API

The java.time API (JSR-310), introduced in Java 8, is a complete replacement for the legacy java.util.Date and java.util.Calendar classes, built around immutable, thread-safe value types that clearly separate human-readable dates and times from machine instants, durations, and time zone reasoning. The legacy API was widely regarded as one of Java's worst-designed parts: Date was mutable, not thread-safe, used confusing zero-based months and an offset-from-1900 year representation, and Calendar's API was verbose and error-prone, while neither class clearly distinguished a date from a timestamp from a duration. This entry covers the motivation and design principles behind java.time, the core classes (LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant, Duration, Period), formatting and parsing with DateTimeFormatter, time zone handling, and interoperability with the legacy API.

Motivation — Why java.util.Date and Calendar Were Replaced

java.util.Date, present since Java 1.0, suffered from several compounding design flaws: it was mutable (two threads or two pieces of code could change a shared Date instance unexpectedly), its year was represented as an offset from 1900 and its month as zero-based (January = 0), and despite its name it represented a precise instant in time (milliseconds since the epoch) rather than a calendar date, which made it semantically wrong for representing things like "a birthday" that should not carry time-of-day or time-zone information. java.util.Calendar, added later to address some of these issues, instead added enormous API surface complexity and remained mutable, while SimpleDateFormat (used for parsing and formatting) was not thread-safe, a fact that caused production bugs when instances were shared across threads or stored as static fields. JSR-310, designed by Stephen Colebourne (author of the popular third-party Joda-Time library) and adopted into Java 8 as java.time, fixed these problems by design rather than by patching the old classes: every core class is immutable and thread-safe (operations like plusDays() return a new instance rather than mutating the receiver), and the API draws a sharp distinction between human-scale concepts (LocalDate, LocalTime, a date or time without time zone context) and machine-scale concepts (Instant, a single point on the UTC timeline), with Duration and Period providing two distinct ways to represent elapsed time depending on whether it should be measured in exact seconds or in calendar units like months and days.
Java
// ── Legacy API problems ─────────────────────────────────────────────────
Date d = new Date(2024 - 1900, 0, 15);   // confusing: year offset, zero-based month
d.setMonth(d.getMonth() + 1);             // MUTATES the existing instance — unsafe to share

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");  // NOT thread-safe
// Sharing `sdf` across threads can corrupt parse/format results unpredictably

// ── java.time equivalents — immutable, clear semantics ─────────────────
LocalDate ld = LocalDate.of(2024, Month.JANUARY, 15);  // explicit month enum, real year
LocalDate next = ld.plusMonths(1);                      // returns a NEW instance
System.out.println(ld);     // 2024-01-15 — original untouched
System.out.println(next);   // 2024-02-15

DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");  // thread-safe, immutable
String formatted = ld.format(fmt);
LocalDate parsed = LocalDate.parse("2024-01-15", fmt);

Core Types — Local Dates/Times, Instant, Duration, and Period

LocalDate represents a date without time-of-day or time-zone information (a birthday, a holiday). LocalTime represents a time-of-day without a date or zone (opening hours). LocalDateTime combines both but still carries no time-zone information, making it unsuitable for representing a precise, unambiguous moment — "2024-06-19T14:30" means a different actual instant depending on what time zone it's interpreted in. ZonedDateTime adds an explicit ZoneId, making the date-time unambiguous and able to correctly account for daylight saving transitions and historical offset changes for that zone. Instant represents a point on the UTC timeline, measured as seconds (and nanoseconds) since the 1970-01-01T00:00:00Z epoch — it has no concept of calendar fields like "year" or "month" and is the closest equivalent to what Date was meant to represent. Instant is the right type for timestamps, logging, and machine-to-machine communication, while LocalDate/LocalDateTime are right for human-facing concepts and ZonedDateTime is right when both a human-readable date-time and an unambiguous real-world moment are both needed simultaneously (e.g. scheduling a meeting across time zones). Duration measures an exact amount of time in seconds and nanoseconds — useful for "how long did this operation take" or "wait 30 seconds." Period measures an amount of time in calendar units — years, months, and days — useful for "this person is 3 years, 2 months old" where the actual number of seconds varies depending on which months and leap years are involved. Using Duration where Period is semantically correct (or vice versa) is a common source of subtle bugs, since adding "1 month" as a fixed duration of seconds doesn't account for months having different lengths.
Java
// ── LocalDate / LocalTime / LocalDateTime — no zone information ────────
LocalDate today = LocalDate.now();
LocalTime now = LocalTime.now();
LocalDateTime dt = LocalDateTime.of(today, now);
LocalDateTime birthday = LocalDateTime.of(1990, Month.MARCH, 12, 0, 0);

// ── ZonedDateTime — unambiguous real-world moment ───────────────────────
ZonedDateTime meetingInTokyo = ZonedDateTime.of(2024, 6, 19, 9, 0, 0, 0, ZoneId.of("Asia/Tokyo"));
ZonedDateTime sameMomentInLA = meetingInTokyo.withZoneSameInstant(ZoneId.of("America/Los_Angeles"));
System.out.println(meetingInTokyo);   // 2024-06-19T09:00+09:00[Asia/Tokyo]
System.out.println(sameMomentInLA);   // 2024-06-18T17:00-07:00[America/Los_Angeles]

// ── Instant — point on the UTC timeline, for timestamps/logging ────────
Instant start = Instant.now();
// ... do some work ...
Instant end = Instant.now();
long epochMillis = end.toEpochMilli();

// ── Duration — exact elapsed time, seconds-based ────────────────────────
Duration elapsed = Duration.between(start, end);
System.out.println(elapsed.toMillis() + " ms elapsed");

Duration timeout = Duration.ofSeconds(30);
LocalTime later = LocalTime.now().plus(timeout);

// ── Period — calendar-based elapsed time, varies in length ─────────────
LocalDate birth = LocalDate.of(1990, 3, 12);
Period age = Period.between(birth, LocalDate.now());
System.out.println(age.getYears() + " years, " + age.getMonths() + " months");

// Why the distinction matters:
LocalDateTime jan31 = LocalDateTime.of(2024, 1, 31, 0, 0);
LocalDateTime plusPeriodMonth = jan31.plus(Period.ofMonths(1));     // 2024-02-29 (leap year, clamps to month-end)
// A naive "add 30 days as Duration" would NOT land on the same calendar-meaningful date

Formatting, Parsing, Time Zones, and Legacy Interop

DateTimeFormatter is the immutable, thread-safe replacement for SimpleDateFormat, supporting both predefined formatters (ISO_LOCAL_DATE, ISO_DATE_TIME) and custom patterns via ofPattern(...) using the same general letter-based pattern syntax as the legacy API (yyyy, MM, dd, HH, mm, ss) but with corrected, locale-aware behavior. Because formatters are immutable, a single static final formatter instance can be safely shared and reused across threads, unlike SimpleDateFormat. Time zones are represented by ZoneId (a named region like "Europe/London" or "Asia/Tokyo", which correctly accounts for historical and future daylight saving rule changes via the IANA time zone database) or ZoneOffset (a fixed UTC offset like +09:00, with no daylight saving awareness). ZoneId.systemDefault() retrieves the JVM's configured zone, but production code that needs unambiguous behavior across deployment environments should generally store and reason in UTC (Instant) and only convert to a specific ZoneId at the point of display to a user. For interoperability with legacy code that still uses Date or Calendar (e.g. older libraries, JDBC drivers before 4.2, or APIs that haven't been migrated), java.time exposes explicit conversion methods rather than implicit/automatic conversion, intentionally forcing the conversion to be visible at the call site: Date.from(Instant) and date.toInstant(), and similarly for Calendar via GregorianCalendar.
Java
// ── Formatting and parsing ──────────────────────────────────────────────
DateTimeFormatter custom = DateTimeFormatter.ofPattern("dd MMM yyyy, HH:mm");
LocalDateTime dt = LocalDateTime.of(2024, 6, 19, 14, 30);
System.out.println(dt.format(custom));        // 19 Jun 2024, 14:30

LocalDate iso = LocalDate.parse("2024-06-19"); // uses ISO_LOCAL_DATE by default
DateTimeFormatter localeFmt = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.FRENCH);
System.out.println(dt.format(localeFmt));      // 19 juin 2024

// ── Time zones — ZoneId vs ZoneOffset ───────────────────────────────────
ZoneId tokyo = ZoneId.of("Asia/Tokyo");          // tracks DST rules historically/automatically
ZoneOffset fixed = ZoneOffset.of("+09:00");       // fixed offset, no DST awareness

ZonedDateTime z1 = ZonedDateTime.now(tokyo);
System.out.println(ZoneId.systemDefault());       // JVM's configured default zone

// Best practice: store/transmit as Instant (UTC), convert to zone only for display:
Instant storedTimestamp = Instant.now();
ZonedDateTime displayed = storedTimestamp.atZone(ZoneId.of("America/New_York"));

// ── Legacy interop — explicit conversions only ──────────────────────────
Date legacyDate = Date.from(Instant.now());        // java.time -> legacy
Instant fromLegacy = legacyDate.toInstant();        // legacy -> java.time

Calendar cal = GregorianCalendar.from(ZonedDateTime.now());
ZonedDateTime fromCal = ((GregorianCalendar) cal).toZonedDateTime();

Related Topics in Java 8 Features

Stream API
The Stream API, introduced in Java 8, provides a functional, declarative model for processing sequences of elements. A Stream<T> is not a data structure — it carries no storage. It is a pipeline specification: a source that provides elements, zero or more intermediate operations that transform or filter the stream, and exactly one terminal operation that consumes the stream and produces a result or side effect. Streams are lazy: intermediate operations do not execute until the terminal operation is invoked, and execution is fused — elements flow through the entire pipeline one at a time (or in batches for parallel streams), avoiding intermediate collection. Streams are single-use: once a terminal operation has been invoked, the stream is consumed and cannot be reused. Java provides both reference streams (Stream<T>) and primitive streams (IntStream, LongStream, DoubleStream) that avoid boxing overhead. The Stream API covers sources (collections, arrays, files, generators), intermediate operations (filter, map, flatMap, sorted, distinct, limit, skip, peek, mapToInt, mapToObj), terminal operations (forEach, collect, reduce, count, findFirst, findAny, anyMatch, allMatch, noneMatch, toList, min, max), and collectors (toList, toSet, toMap, groupingBy, partitioningBy, joining, counting, summarizing). This entry covers the full lifecycle, every operation class, the short-circuit evaluation, the Spliterator model, collector design, and performance guidance.
Intermediate Operations
Intermediate operations are Stream API methods that transform one stream into another stream, enabling pipeline construction through method chaining. Every intermediate operation is lazy — it does not process any elements when called; it only builds a description of the computation to be performed. The actual processing occurs only when a terminal operation triggers stream traversal, at which point all intermediate operations execute in a single pass over the data, interleaved element by element rather than stage by stage. This laziness and fusion model is the defining architectural feature of the Streams API, distinguishing it from eager collection-transformation approaches. Intermediate operations fall into categories: filtering (filter, distinct), transformation (map and its primitive variants, flatMap), ordering (sorted), size-limiting (limit, skip), peeking (peek), and the Java 9 additions for prefix-based selection (takeWhile, dropWhile). This entry covers the laziness model and why it matters, every intermediate operation with its exact semantics and performance characteristics, stateless versus stateful intermediate operations, short-circuiting operations and how they interact with infinite streams, the map/flatMap distinction, and operation ordering and its performance implications.
Terminal Operations
Terminal operations are Stream API methods that trigger the actual traversal and processing of a stream pipeline, producing a non-stream result — a value, a collection, a side effect, or nothing (void). A stream can have exactly one terminal operation; once invoked, the stream is consumed and cannot be reused (calling any further operation on it throws IllegalStateException). Terminal operations fall into categories: reduction (reduce, collect), matching (anyMatch, allMatch, noneMatch), finding (findFirst, findAny), counting (count), iteration with side effects (forEach, forEachOrdered), and array/collection materialization (toArray, toList). Some terminal operations are short-circuiting (anyMatch, findFirst) and can terminate before examining the entire stream; others (forEach, count in most cases, collect) must examine every element. This entry covers every terminal operation in depth, the reduce() method and its three overloads with the combiner function's role in parallel execution, the Collectors framework as the primary mechanism for complex terminal reduction, short-circuiting semantics and their interaction with infinite streams, the single-use constraint and its rationale, and the choice between equivalent terminal operations for performance and clarity.
Optional
Optional<T>, introduced in Java 8, is a container object that may or may not hold a non-null value, designed to make the possible absence of a value explicit in a method's type signature rather than implicit through a nullable return type. Optional.of(value) creates a present Optional and throws NullPointerException if value is null; Optional.empty() creates an absent Optional; Optional.ofNullable(value) creates either a present or empty Optional depending on whether value is null. The design intent, stated explicitly in the Javadoc, is as a return type for methods where the absence of a result is a valid and expected outcome, communicating that absence through the type system instead of through null, with the goal of reducing NullPointerException at the API boundary. Optional is explicitly not intended for use as a field type, a method parameter type, or inside collections, and the JDK team has stated that misuse in these contexts works against its design intent. This entry covers the complete Optional API including creation, value extraction, conditional execution, transformation and chaining, the primitive specializations OptionalInt/OptionalLong/OptionalDouble, and the established conventions for where Optional should and should not be used.