☕ 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

Lambda Expressions
Lambda expressions, introduced in Java 8, are anonymous functions — blocks of code that can be stored in variables, passed as arguments, and returned from methods, treating behavior as data. A lambda has three parts: a parameter list, an arrow token (->), and a body. The body is either a single expression (whose value is the implicit return value) or a block of statements wrapped in braces. Lambdas implement functional interfaces — interfaces with exactly one abstract method — allowing any lambda whose signature matches the abstract method's signature to be used wherever that interface is expected. The lambda syntax is syntactic sugar: every lambda is compiled to an invocation of the functional interface's abstract method, with the compiler generating a class (via invokedynamic) that implements the interface and delegates to the lambda body. This entry covers the complete lambda syntax including all shorthand forms, variable capture and the effectively-final constraint, method references as a specialized lambda syntax, the relationship between lambdas and the type system, how lambdas interact with exception handling, the invokedynamic compilation strategy and its performance characteristics, and the complete set of rules governing lambda type inference.
Functional Interfaces
A functional interface is any Java interface that has exactly one abstract method. This single-abstract-method (SAM) contract makes the interface a valid target type for a lambda expression or method reference — the lambda provides the implementation of that one abstract method. The @FunctionalInterface annotation is optional but strongly recommended: it causes the compiler to verify that the interface satisfies the SAM constraint, rejecting it at compile time if there is more than one abstract method. The java.util.function package, introduced in Java 8, provides 43 standard functional interfaces organized around four root types — Function, Consumer, Supplier, Predicate — and their variations for primitives (IntFunction, LongSupplier, DoubleConsumer, etc.), binary operations (BiFunction, BiConsumer, BiPredicate), and unary operators (UnaryOperator, IntUnaryOperator, etc.). This entry covers the design principles behind functional interfaces, the complete @FunctionalInterface contract including default and static methods, the full java.util.function hierarchy and the pattern that governs naming, creating custom functional interfaces with checked exceptions, composing functional interfaces via default methods, and the relationship between functional interfaces and the type system including the rules for lambda assignment and widening.
Predicate
Predicate<T> is a functional interface in java.util.function representing a boolean-valued function of one argument, with the single abstract method boolean test(T t). It is one of the four foundational functional interfaces in the Java standard library and is used throughout the Collections framework, Streams API, and Optional for filtering, condition testing, and validation. Predicate is designed for composition: its default methods and(Predicate), or(Predicate), and negate() allow building complex boolean expressions from simple predicates without boilerplate. The static methods isEqual(Object) and not(Predicate) provide factory methods for common cases. The primitive specializations IntPredicate, LongPredicate, and DoublePredicate avoid boxing overhead for numeric values. BiPredicate<T,U> extends the concept to two-argument boolean functions. This entry covers the complete Predicate API, all composition methods and their short-circuit semantics, the static factory methods, primitive specializations, BiPredicate, using Predicate in stream pipelines and Collections methods, building validation frameworks with Predicate composition, and the performance and readability trade-offs of different composition styles.
Function
Function<T,R> is a functional interface in java.util.function representing a function that accepts one argument of type T and produces a result of type R, with the single abstract method R apply(T t). It is the most general transformation interface in the standard library, used throughout the Streams API for mapping (Stream.map()), in Optional for value transformation (Optional.map(), Optional.flatMap()), and as a building block for more specialized functional interfaces. Function provides two default composition methods — andThen() and compose() — that create new functions by chaining two functions together, enabling functional pipeline construction without intermediate variables. The specializations cover all combinations of generic and primitive inputs and outputs: ToIntFunction, IntFunction, IntToLongFunction, and so on. UnaryOperator<T> extends Function<T,T> for operations that transform a value within the same type. BiFunction<T,U,R> generalizes to two input arguments. This entry covers the complete Function API, the semantics of andThen versus compose, all specializations and when each is appropriate, the functional relationship between Function and other java.util.function types, partial application patterns, and Function as the basis for building data pipelines.