Chapter 15: Date, Time, and Utilities
Programs often need to work with dates, times, mathematical calculations, and general-purpose utilities that make code simpler and more reliable. Java includes a comprehensive standard library to meet these needs, offering everything from powerful time management through the java.time API to mathematical and object utilities in java.util and related packages.
Before Java 8, working with dates and times involved the older Date and Calendar classes, which were cumbersome and prone to errors. The modern java.time API replaced these with a clean, immutable, and type-safe model based on the ISO-8601 standard, making it far easier to work with time zones, durations, and date arithmetic.
java.time API was inspired by the Joda-Time library and is now the official, recommended way to handle date and time in Java. It provides classes like LocalDate, LocalTime, and ZonedDateTime for precise control and clarity.
In addition to working with time, this chapter explores Java’s essential utility classes. These include Objects for safer null handling, Math for common mathematical functions, Random for generating pseudo-random values, and UUID for creating universally unique identifiers. You will also learn how to format and parse date and numeric data in a locale-aware way, and how to use Optional to write cleaner, null-safe code.
Date and Calendar in new projects. They are maintained for backward compatibility but lack the clarity, thread safety, and precision of the modern java.time API.
By the end of this chapter, you will be able to work confidently with date and time, perform complex calculations, generate secure random data, handle optional values safely, and make full use of Java’s versatile utility classes to streamline your programs.
The java.time API
The java.time package, introduced in Java 8, provides a modern and comprehensive framework for handling dates, times, and durations. It replaces the older Date and Calendar classes with a cleaner, more intuitive API that emphasizes immutability, thread safety, and clarity. All classes in this package are designed around the ISO-8601 standard, which is used internationally for date and time representation.
The core design divides temporal data into distinct types, such as date-only, time-only, date-time, and zoned date-time. This makes it easier to express exactly what kind of temporal information a variable holds, avoiding common mistakes with mixed or ambiguous date/time data.
Core date and time classes
The most commonly used classes in the java.time package include:
LocalDate— represents a date without time or time zone (for example,2025-10-27).LocalTime— represents a time without date or time zone (for example,14:35:00).LocalDateTime— combines date and time, but still without a time zone.ZonedDateTime— represents a full date and time including a time zone and offset from UTC.Instant— represents a precise moment on the timeline in UTC, suitable for timestamps.
import java.time.*;
public class TimeExample {
public static void main(String[] args) {
LocalDate date = LocalDate.now();
LocalTime time = LocalTime.now();
LocalDateTime dateTime = LocalDateTime.now();
ZonedDateTime zoned = ZonedDateTime.now();
System.out.println("Date: " + date);
System.out.println("Time: " + time);
System.out.println("DateTime: " + dateTime);
System.out.println("ZonedDateTime: " + zoned);
}
}
Each of these classes provides a rich set of methods for retrieving and manipulating temporal data, such as adding or subtracting days, comparing times, or converting between formats.
Instant class is ideal for timestamps and logging events. It represents a point in time counted in nanoseconds since the Unix epoch (1 January 1970, 00:00 UTC).
Creating and manipulating dates
You can create date and time instances using static factory methods such as of() or by parsing strings. These immutable objects return new instances whenever modified, ensuring thread safety.
import java.time.*;
public class ManipulateDate {
public static void main(String[] args) {
LocalDate today = LocalDate.now();
LocalDate birthday = LocalDate.of(1990, 5, 12);
LocalDate nextWeek = today.plusWeeks(1);
LocalDate nextMonth = today.plusMonths(1);
System.out.println("Today: " + today);
System.out.println("Next week: " + nextWeek);
System.out.println("Next month: " + nextMonth);
System.out.println("Birthday: " + birthday);
}
}
Because LocalDate and related classes are immutable, methods like plusDays() or minusMonths() return new objects rather than modifying existing ones.
Periods and durations
The Period and Duration classes represent amounts of time, with Period measured in years, months, and days, and Duration measured in seconds and nanoseconds. These are useful for performing arithmetic between temporal objects.
import java.time.*;
import java.time.temporal.ChronoUnit;
public class PeriodDurationExample {
public static void main(String[] args) {
LocalDate start = LocalDate.of(2020, 1, 1);
LocalDate end = LocalDate.of(2025, 10, 27);
Period period = Period.between(start, end);
long days = ChronoUnit.DAYS.between(start, end);
System.out.println("Period: " + period);
System.out.println("Total days: " + days);
}
}
Periods and durations allow you to express time differences naturally and apply them directly to other temporal values using methods such as plus() and minus().
Time zones and offsets
Time zones are represented by the ZoneId class, which identifies regions such as "Europe/London" or "America/New_York". A ZonedDateTime combines a LocalDateTime with a zone to produce an exact instant on the global timeline.
import java.time.*;
public class ZoneExample {
public static void main(String[] args) {
ZoneId london = ZoneId.of("Europe/London");
ZoneId tokyo = ZoneId.of("Asia/Tokyo");
ZonedDateTime londonTime = ZonedDateTime.now(london);
ZonedDateTime tokyoTime = ZonedDateTime.now(tokyo);
System.out.println("London: " + londonTime);
System.out.println("Tokyo: " + tokyoTime);
}
}
Time zones are crucial when dealing with international applications or scheduling across regions. The ZoneOffset class represents fixed offsets from UTC, such as +02:00.
TimeZone. Instead, use ZoneId and ZonedDateTime for consistent, thread-safe behavior.
Instant and epoch time
The Instant class provides nanosecond precision for timestamps relative to the Unix epoch. You can use it to record exact event times or measure elapsed durations with high precision.
import java.time.*;
public class InstantExample {
public static void main(String[] args) {
Instant start = Instant.now();
// Simulated operation
for (int i = 0; i < 1000000; i++);
Instant end = Instant.now();
System.out.println("Elapsed: " +
java.time.Duration.between(start, end).toNanos() + " ns");
}
}
The Instant class is the foundation for high-precision timing and logging. It integrates well with Duration and can be converted easily to ZonedDateTime when required for display.
The java.time API provides a robust, immutable, and well-structured system for handling all aspects of date and time in modern Java applications. It is one of the most significant improvements to the standard library since the language’s early days, replacing error-prone legacy classes with a design that is both elegant and practical.
Formatting and parsing dates
Formatting and parsing are essential when converting between Java’s date and time objects and human-readable text. The java.time.format package provides flexible classes for these operations, allowing you to display dates in a chosen pattern or interpret user input into structured date and time values.
The central class for this purpose is DateTimeFormatter. It works with all major temporal types such as LocalDate, LocalTime, LocalDateTime, and ZonedDateTime, and it supports both predefined ISO formats and custom patterns.
Using predefined formatters
The DateTimeFormatter class provides several built-in constants for common date and time representations. These include ISO_LOCAL_DATE, ISO_LOCAL_DATE_TIME, and ISO_ZONED_DATE_TIME, among others.
import java.time.*;
import java.time.format.DateTimeFormatter;
public class PredefinedFormats {
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
System.out.println("ISO local date: " +
now.format(DateTimeFormatter.ISO_LOCAL_DATE));
System.out.println("ISO date-time: " +
now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
System.out.println("ISO zoned date-time: " +
ZonedDateTime.now().format(DateTimeFormatter.ISO_ZONED_DATE_TIME));
}
}
These standard ISO formatters are especially useful when exchanging data between systems or APIs, since they are unambiguous and universally recognized.
java.time, making it ideal for serialization and interoperability in JSON, XML, and REST APIs.
Custom formatting patterns
For user interfaces or reports, you often need to display dates in a more readable or localized form. Custom patterns can be defined using pattern letters similar to those used in SimpleDateFormat, but with improved thread safety and clarity.
import java.time.*;
import java.time.format.DateTimeFormatter;
public class CustomFormat {
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MMMM yyyy, HH:mm:ss");
String formatted = now.format(fmt);
System.out.println("Formatted: " + formatted);
}
}
In this example, dd represents the day of the month, MMMM the full month name, yyyy the year, and HH:mm:ss the 24-hour time. The resulting string is locale-sensitive, displaying month names and other elements according to the default system locale unless specified otherwise.
MM means month, while mm means minutes. Mixing them up will produce incorrect results.
Parsing text into dates
Parsing is the reverse of formatting: converting a string representation into a date or time object. The same DateTimeFormatter instance can be used for both operations, ensuring consistency between display and input.
import java.time.*;
import java.time.format.DateTimeFormatter;
public class ParseExample {
public static void main(String[] args) {
String input = "27 October 2025, 14:45:00";
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MMMM yyyy, HH:mm:ss");
LocalDateTime dateTime = LocalDateTime.parse(input, fmt);
System.out.println("Parsed date-time: " + dateTime);
}
}
If the text does not match the specified pattern, a DateTimeParseException will be thrown, so it is best to handle this gracefully when accepting user input.
Localized date and time formats
Java provides predefined localized formatters for displaying date and time values according to regional conventions. These can be obtained via the ofLocalizedDate(), ofLocalizedTime(), or ofLocalizedDateTime() methods.
import java.time.*;
import java.time.format.*;
public class LocalizedFormat {
public static void main(String[] args) {
LocalDate date = LocalDate.now();
DateTimeFormatter shortFmt = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT);
DateTimeFormatter longFmt = DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG);
System.out.println("Short: " + date.format(shortFmt));
System.out.println("Long: " + date.format(longFmt));
}
}
Localized formatters automatically adapt to the system locale or one you specify explicitly using withLocale(). This is especially useful in international applications.
Formatting and parsing with time zones
When working with time zone–aware types such as ZonedDateTime, you can include zone and offset information in your patterns using symbols like z for short names (e.g. GMT, PST) or Z for numeric offsets (e.g. +0100).
import java.time.*;
import java.time.format.*;
public class ZoneFormatting {
public static void main(String[] args) {
ZonedDateTime zoned = ZonedDateTime.now(ZoneId.of("Europe/London"));
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
String formatted = zoned.format(fmt);
System.out.println("Formatted with zone: " + formatted);
ZonedDateTime parsed = ZonedDateTime.parse(formatted, fmt);
System.out.println("Parsed back: " + parsed);
}
}
This demonstrates that you can fully round-trip date-time strings containing time zone information, preserving the precise moment represented.
The java.time.format system provides a clear, thread-safe, and consistent way to format and parse date and time values, ensuring both readability for users and reliability for programs that depend on accurate temporal data.
Utility classes
Java provides a collection of utility classes that support everyday programming tasks. Among the most widely used are Objects, Math, and Random. These classes are part of java.util and java.lang and offer essential functionality for working safely with objects, performing mathematical operations, and generating random numbers.
The Objects class
The java.util.Objects class provides a set of static methods that simplify null handling, equality checks, and hash code generation. It helps prevent NullPointerException errors and reduces the amount of boilerplate code often required for defensive programming.
import java.util.Objects;
public class ObjectsExample {
public static void main(String[] args) {
String name = null;
// Safe equality check
System.out.println(Objects.equals(name, "Alice"));
// Null-safe default value
String result = Objects.requireNonNullElse(name, "Unknown");
System.out.println("Name: " + result);
// Manual null validation
try {
Objects.requireNonNull(name, "Name cannot be null");
} catch (NullPointerException e) {
System.out.println(e.getMessage());
}
}
}
The Objects class also provides hash() and toString() helpers that are especially useful when implementing equals() and hashCode() methods in your own classes.
Objects.requireNonNull() when writing constructors or methods that should not accept null arguments. This provides clear, immediate feedback when a contract is violated.
The Math class
The java.lang.Math class offers a comprehensive set of static methods for mathematical calculations. It includes trigonometric, logarithmic, and exponential functions, rounding operations, and constants for PI and E.
public class MathExample {
public static void main(String[] args) {
double x = 7.5;
System.out.println("Ceil: " + Math.ceil(x));
System.out.println("Floor: " + Math.floor(x));
System.out.println("Round: " + Math.round(x));
System.out.println("Power: " + Math.pow(2, 8));
System.out.println("Square root: " + Math.sqrt(81));
System.out.println("Sin(PI/2): " + Math.sin(Math.PI / 2));
}
}
The Math class uses double precision floating-point arithmetic for most operations, ensuring accuracy sufficient for most scientific and engineering tasks. For higher precision, the BigDecimal class (from java.math) can be used instead.
BigDecimal.
The Random class
The java.util.Random class provides methods for generating pseudo-random numbers. It can produce integers, floating-point values, booleans, and even random streams for functional-style processing. Each Random instance maintains its own internal seed, so repeated runs can produce different results unless the seed is fixed.
import java.util.Random;
public class RandomExample {
public static void main(String[] args) {
Random rand = new Random();
System.out.println("Random int: " + rand.nextInt());
System.out.println("Random int (0–9): " + rand.nextInt(10));
System.out.println("Random double: " + rand.nextDouble());
System.out.println("Random boolean: " + rand.nextBoolean());
}
}
Using nextInt(bound) limits the output to a specific range (from 0 inclusive to the bound exclusive), which is useful for indexing or simple random selections.
java.security.SecureRandom instead of Random. It provides stronger, unpredictable randomness.
Random streams and functional use
Since Java 8, Random also supports generating streams of random numbers that can be processed using the Stream API. This makes it easy to produce large sets of random values for analysis or simulation.
import java.util.Random;
public class RandomStreamExample {
public static void main(String[] args) {
Random rand = new Random();
rand.ints(5, 1, 100) // generate 5 random ints from 1 to 99
.forEach(System.out::println);
}
}
This concise style is ideal for generating random data sets or testing algorithms. The ints(), doubles(), and longs() methods each produce specialized numeric streams.
Together, the Objects, Math, and Random classes form an essential part of Java’s core toolkit, offering reliability, safety, and versatility for a wide range of programming tasks.
UUIDs and basic data formatting
In many programs, you need to generate unique identifiers or format numbers and text for display. Java provides powerful utilities for both purposes: UUID for globally unique identifiers and the java.text package for flexible data formatting. Together, these make it easy to manage data safely and present it consistently across systems and locales.
Working with UUIDs
A UUID (Universally Unique Identifier) is a 128-bit value used to uniquely identify information across space and time, without needing a central authority. Java’s java.util.UUID class provides built-in methods to generate and parse these identifiers.
import java.util.UUID;
public class UUIDExample {
public static void main(String[] args) {
UUID id1 = UUID.randomUUID(); // random version 4 UUID
UUID id2 = UUID.nameUUIDFromBytes("example".getBytes());
System.out.println("Random UUID: " + id1);
System.out.println("Name-based UUID: " + id2);
}
}
A UUID is typically represented as a string of 32 hexadecimal digits separated by hyphens, such as 550e8400-e29b-41d4-a716-446655440000. The probability of collision is astronomically low, making UUIDs ideal for database keys, session identifiers, or distributed systems.
randomUUID() method generates a version 4 UUID, which uses random values. For reproducible identifiers, use nameUUIDFromBytes() with a stable input.
Basic number formatting
Numbers often need to be displayed in a readable, locale-specific format. The java.text.NumberFormat class handles this automatically, inserting grouping separators and adjusting decimal points according to regional conventions.
import java.text.NumberFormat;
import java.util.Locale;
public class NumberFormatting {
public static void main(String[] args) {
double value = 1234567.89;
NumberFormat us = NumberFormat.getInstance(Locale.US);
NumberFormat fr = NumberFormat.getInstance(Locale.FRANCE);
System.out.println("US: " + us.format(value));
System.out.println("France: " + fr.format(value));
}
}
This produces:
US: 1,234,567.89
France: 1 234 567,89
The same number is rendered differently depending on the locale, ensuring that applications appear natural to users around the world.
NumberFormat class is not thread-safe. Each thread should use its own instance when formatting numbers concurrently.
Currency and percentage formatting
Specialized number formatters exist for currencies and percentages, automatically adding appropriate symbols and scaling values.
import java.text.NumberFormat;
import java.util.Locale;
public class CurrencyAndPercent {
public static void main(String[] args) {
double price = 49.99;
double rate = 0.075;
NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.UK);
NumberFormat percent = NumberFormat.getPercentInstance();
System.out.println("Price: " + currency.format(price));
System.out.println("Rate: " + percent.format(rate));
}
}
This might display:
Price: £49.99
Rate: 7%
By selecting different locales, you can automatically adapt currency symbols and decimal rules to each region, avoiding manual string manipulation.
Decimal precision control
You can control the number of digits displayed before and after the decimal point using setMinimumFractionDigits() and setMaximumFractionDigits() methods. This is useful for aligning numeric data or producing clean reports.
import java.text.NumberFormat;
public class PrecisionExample {
public static void main(String[] args) {
double pi = Math.PI;
NumberFormat fmt = NumberFormat.getNumberInstance();
fmt.setMaximumFractionDigits(3);
fmt.setMinimumFractionDigits(3);
System.out.println("Pi (3 decimal places): " + fmt.format(pi));
}
}
This ensures consistent rounding and alignment, regardless of the system locale or internal representation of the number.
BigDecimal for arithmetic and then format the result with NumberFormat for display.
Formatting with String.format()
Another approach to formatting numbers, strings, and other data is String.format(), which uses C-style format specifiers. This method is fast and concise for simple tasks.
public class StringFormatExample {
public static void main(String[] args) {
String name = "Alice";
int age = 30;
double balance = 1234.567;
String result = String.format("Name: %s, Age: %d, Balance: %.2f", name, age, balance);
System.out.println(result);
}
}
This prints:
Name: Alice, Age: 30, Balance: 1234.57
The format specifiers work as follows: %s inserts a string, %d an integer, and %.2f a floating-point number with two decimal places. This method is locale-neutral by default but can accept a Locale as the first argument for internationalized formatting.
Together, UUID and Java’s data formatting classes provide a reliable foundation for identifying, presenting, and managing data clearly and consistently in applications of any scale.
Using Optional and null safety
Null references have long been a source of bugs and confusion in Java programs. Accidentally dereferencing a null value leads to a NullPointerException, one of the most common runtime errors in Java. To reduce this problem and encourage safer programming practices, Java 8 introduced the Optional class in java.util. It provides a container that may or may not hold a non-null value, explicitly expressing the possibility of absence.
Creating Optional values
An Optional<T> can be created in several ways: with a non-null value, as an empty instance, or conditionally using ofNullable(). This makes code safer and more readable by clearly showing when a value may be missing.
import java.util.Optional;
public class OptionalCreation {
public static void main(String[] args) {
Optional<String> name1 = Optional.of("Alice"); // value present
Optional<String> name2 = Optional.empty(); // explicitly empty
Optional<String> name3 = Optional.ofNullable(null); // may be empty
System.out.println(name1.isPresent()); // true
System.out.println(name2.isEmpty()); // true
System.out.println(name3.orElse("Unknown"));
}
}
By using Optional, you make it explicit when a method might return no result. This eliminates the need for null checks scattered throughout your code.
Optional.of() only when you are certain a value is non-null. Otherwise, prefer ofNullable() to safely handle potential nulls.
Accessing and transforming values
The Optional class provides functional-style methods to access, transform, or supply default values without explicit null checks. This leads to cleaner, more declarative code.
import java.util.Optional;
public class OptionalTransform {
public static void main(String[] args) {
Optional<String> name = Optional.ofNullable("Bob");
// Transform value if present
String upper = name.map(String::toUpperCase).orElse("No name");
System.out.println(upper);
// Provide default if absent
Optional<String> empty = Optional.empty();
System.out.println(empty.orElse("Default value"));
}
}
The map() and flatMap() methods allow you to apply functions directly to the contained value, if present. If not, the operations are simply skipped without throwing exceptions.
Conditional execution with ifPresent()
Instead of manually checking if a value is present, Optional provides ifPresent() and ifPresentOrElse() methods for conditional execution. These methods accept lambda expressions, keeping your logic concise and expressive.
import java.util.Optional;
public class IfPresentExample {
public static void main(String[] args) {
Optional<String> name = Optional.of("Carol");
name.ifPresent(n -> System.out.println("Hello, " + n));
Optional<String> missing = Optional.empty();
missing.ifPresentOrElse(
n -> System.out.println("Found: " + n),
() -> System.out.println("Value not found")
);
}
}
These methods let you perform actions only when data exists, avoiding complex conditional blocks or null comparisons.
get() without checking presence first. If the Optional is empty, it will throw NoSuchElementException. Prefer safe alternatives such as orElse() or orElseThrow().
Optionals in method design
Returning Optional from methods is a good practice when a value may not exist. It encourages the caller to handle the absence explicitly rather than risking a null pointer.
import java.util.Optional;
public class UserLookup {
public static Optional<String> findUserById(int id) {
if (id == 1) return Optional.of("Alice");
return Optional.empty();
}
public static void main(String[] args) {
String user = findUserById(2).orElse("Guest");
System.out.println("User: " + user);
}
}
This approach communicates intent clearly and prevents accidental null misuse. Callers must decide what to do if no result is found, promoting safer API design.
Combining Optional with streams
Optional integrates smoothly with Java streams, allowing functional-style chaining and data transformation. You can convert between Optional and Stream when filtering or aggregating results.
import java.util.*;
import java.util.stream.*;
public class OptionalStream {
public static void main(String[] args) {
List<Optional<String>> list = List.of(
Optional.of("Alpha"),
Optional.empty(),
Optional.of("Beta")
);
List<String> names = list.stream()
.flatMap(Optional::stream)
.collect(Collectors.toList());
System.out.println(names);
}
}
This example collects only present values, ignoring empty ones automatically. It shows how Optional and the Stream API work together for clean, expressive data processing.
Optional is not intended to replace all null uses, but to make optionality explicit in method results and control flow. It improves readability, safety, and intent.
By embracing Optional and careful null handling, you can eliminate many common runtime errors, making your Java programs safer, more predictable, and easier to maintain.