13  Date and Time

In Java, there are many classes and interfaces used to represent date and time. These have evolved significantly, starting from relatively simple representations to much more sophisticated tools for properly handling calendars, timezones, and time.

Keep in mind that handling date and time is inherently complex.

For example, if in a meeting taking place in Turin someone tells you it’s 5:17 PM, you understand it because you are all in Italy and you know what that means locally. But the same statement, if streamed to someone in the U.S., would be misleading — they’re in a different time zone - so it wouldn’t be 5:17 PM for them. Time zone management is critical in date and calendar handling.

Even something as seemingly trivial as computing “the next day” isn’t as simple as adding one to the day digits. Months have different lengths, and leap years add complexity - like February sometimes having 29 days every fourth year -. Going even further, every so often a leap second is added to adjust for the Earth’s rotational slowdown.

Another importan aspect is daylight saving time. When converting between time zones, it’s not enough to know the offsets, you need to know when daylight saving rules apply. Java handles this using built-in zone databases.

If you ignore these details, you risk inaccurate results. That’s why a simplified or naïve approach to calendar management often doesn’t work except for the most basic operations.

Chronologically date and time in Java evolved through several different APIs:

13.1 System timestamps

The System class provides two methods that can be used to retrieve system timestamps:

  • currentTimeMillis() the difference, measured in milliseconds, between the current time and midnight, January 1, 1970 UTC
  • nanoTime() current value of the running JVM’s high-resolution time source, in nanosecond. Does not have a fixed reference point and is often reset based on internal CPU timing. It’s useful for measuring elapsed time, but not for real-world timestamps.

13.1.1 Performance measurement

A typical application of these method is to measure the time performance of several algorithms, e.g.

long t0 = System.nanoTime();
algorithm();
long t1 = System.nanoTime();
IO.println("Elapsed: " + (t1-t0));

13.1.2 Monitoring

Another typical usage is the monitoring of performance of long running operations.

A monitoring listerner interface can be used to receive apdate about the progress:

interface ProgressListener {
    void progress(int current, int total);
}

An example of a long running algorithm (concatenating integer into a string in the most inefficient way):

static void monitoredAlgorithm(ProgressListener l){
    String s = "";
    for(int i=0; i<N; ++i){
1        if(i%1000 == 0) l.progress(i, N);
        s+=i;
    }
    l.progress(N, N);
}
1
every given number of iterations (1000 in this case) the listener method is invoked to keep track of the progress.

A possible example of monitor:

long[] time = {-1,-1};
monitoredAlgorithm( (c,t) -> {
1    double prop = c / (double)t * 100;
2    if(time[0] == -1){
        time[0] = System.currentTimeMillis();
        IO.print(" %.2f%% completed\r".formatted(prop));
    }else{
3        time[1] = System.currentTimeMillis();
4        double elapsed = (time[1] - time[0])/1000.0;
5        double throughput = c / elapsed;
6        double residual = t / throughput - elapsed;
7        IO.print(" %.1f%% completed in %.1f s (%.1f s remaining) throughput: %.1f k per sec\r"
                .formatted(prop, elapsed, residual, throughput/1000));
    }
});
IO.println("\nDone.");
1
a proportion of completed work is computed
2
on first invocation, the initial time is recorded and the proportion printed
3
on following invocations, the current last time is recorded
4
the elapsed time is computed
5
the average throughput is computed
6
an estimated residual time is computed
7
all the above information is printed

13.2 Old APIs

13.2.1 Date

In earlier versions of Java, the Date class in package java.util was introduced. It’s essentially a wrapper around a long timestamp (milliseconds since the epoch).

However, Date had serious limitations, especially for time zone conversions and date arithmetic.

Even its constructors were problematic: they referenced the year 1900 as a base, so creating a Date with the value 115 meant the year 2015. Months were zero-based (e.g., 4 for May), but days were one-based, making new Date(115, 4, 6) a cryptic way to represent May 6, 2015. This constructor is deprecated now, and rightly so—it’s very confusing and error-prone.

1Date d = new Date(115,4,6);
2String s = d.toString();
1
Deprecated
2
“Wed May 06 00:00:00 CEST 2015”

When instantiated with no arguments Date provides the current date. But even that relies on the time zone of the system it is running on.

Note

While older classes like Date are still found in legacy code, they are not recommended. Most of their methods are deprecated, and they’re not well-suited for complex operations. The newer java.time classes are much more robust, consistent, immutable, and suitable for any kind of date/time processing.

13.2.2 Calendar

To improve this, Java introduced – in version 1.1 – an abstract class Calendar and its primary implementation, GregorianCalendar. This class provides standard fields for date components (e.g., YEAR, MONTH, DAY_OF_MONTH, HOUR, …) and lets you get and set these fields. You can also perform operations like adding days, months, or weeks—operations that are calendar-aware.

The main method for interacting with Calendar are:

  • get(field)
  • set(field, value)
  • add(field, delta)

13.3 New Date and Time

Starting with Java 8, a new date/time API was introduced in the java.time package, which provides a comprehensive and consistent set of classes designed according to a few guiding principles:

  • Simplicity
  • Consistency
  • Immutable classes

The classes and interfaces fall into two main categories:

  • Temporal points: such as Instant, LocalDate, LocalTime, LocalDateTime, and ZonedDateTime. These represent moments in time, either in absolute terms or relative to a time zone.

  • Temporal amounts: such as Duration (time-based intervals) and Period (date-based intervals).

classDiagram
    direction LR
    class Temporal {
        + minus()
        + plus()
        + with()
        + get()
    }
    
    Temporal <|-- Instant
    Temporal <|-- LocalDateTime
    Temporal <|-- LocalDate
    Temporal <|-- LocalTime
    link Temporal "#time-points"

    TemporalAmount <|-- Period
    TemporalAmount <|-- Duration
    link TemporalAmount "#intervals"

    TemporalField <|-- ChronoField
    note for ChronoField "enumeration"
    
    TemporalUnit <|-- ChronoUnit
    note for ChronoUnit "enumeration"

    <<interface>> TemporalField
    <<interface>> TemporalUnit

    class Clock
Figure 13.1: Main classes of datetime API

13.3.1 Time points

These types are immutable and do not expose public constructors. Instead, they use factory methods like of() or parse(). This applies uniformly—for instance, LocalDate.of(2025, 5, 6) clearly constructs May 6, 2025, without any weird base-year offsets.

Method Purpose
of() Create instance from a set of specific parameters, with validation
from() Convert from another class with possible loss of information
parse() Parse a string to build an instance
now() Create an instance representing the current time / date. Can accept a ZoneId

Comparison

Method Purpose
isBefore() Checks if this time/date is before the specified time/date
isAfter() Checks if this time/date is after the specified time/date
isEqual() Checks if this time/date is the same as the specified time/date
compareTo() Compares to to other time/date

13.3.2 Changing

Method Purpose
minus() Returns a new date/time built by removing a specific amount of date/time
plus() Returns a new date/time built by adding a specific amount of date/time
with() Returns a new date/time modified as specified by a temporal adjuster

To manipulate dates or times, methods like plus() and minus() are available. These come in two flavors:

  1. You pass a long and a ChronoUnit (e.g., DAYS, MONTHS, YEARS).
  2. You use a TemporalAmount like a Period or Duration.

You can also use TemporalAdjusters to adjust a date to meaningful calendar positions thought method with(), for instance:

  • firstDayOfMonth()
  • firstDayOfNextMonth()
  • firstInMonth(DayOfWeek dayOfWeek)
  • lastDayOfMonth()
  • next(DayOfWeek dayOfWeek)
  • previous(DayOfWeek dayOfWeek)

In addition there are class specific method, e.g., plusDays(long toAdd).

Day of Week and Month are represented by enums:

  • DayOfWeek
  • Month

Can be converted to string using: getDisplayName(style,locale), where style is one of:

  • TextStyle.FULL
  • TextStyle.NARROW
  • TextStyle.SHORT

Examples:

LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plus(1,ChronoUnit.DAYS);
LocalDate inTwoWeeks = today.plusDays(14);
LocalDate lastMonday = oggi.with(TemporalAdjusters.previous(DayOfWeek.MONDAY));

13.3.3 Locale

Class Locale represents a specific geographical, political, or cultural region Used to perform locale-sensitive operations:

  • Date formats
  • DoW, Month names
  • Decimal separators
  • Locale definition

Predefined constants, e.g., Locale.US, Locale.ITALIAN

Constructors

  • Language: 2 or 3 chars code
  • Country: 2 chars or 3 digits

In addition a variant allows optional additional specifications.

13.3.4 ISO-8601

A general recommendation is to use the ISO 8601 date format: YYYY-MM-DD, with four-digit years and two-digit months and days. This standard avoids ambiguity—unlike the European format (DD/MM/YYYY) or the U.S. format (MM/DD/YYYY), which can easily be misinterpreted.

The full ISO 8601 format also supports date and time, separated by a 'T' (e.g., 2025-06-06T15:30:00+02:00). The time zone is included as either 'Z' for UTC or a +/- offset.

ISO8601 datetime 2025 - 12 - 01 T 14 : 30 : 45 Z Hour Hour datetime:h->Hour Minute Minute datetime:m->Minute Second Second datetime:s->Second Year Year Year->datetime:Y Month Month Month->datetime:M Day Day Day->datetime:D Zone Timezone Z(UTC) or +-00 Zone->datetime:tz

ISO 8601 Date and time format

13.3.5 Intervals

For time intervals, use Duration and Period. You can create these with factory methods or with between(start, end), which calculates the elapsed time between two points.

Intervals can be created with the folowing factory methods:

Method Purpose
of() Creates interval from specified amount of TemporalUnits
ofXxxx() Creates interval from specified amount of units (Xxxx is : Days, Hours, etc.)
between() Creates interval between two temporal points

Example: measuring elasped time for

Instant start = Instant.now();
// here some long running operation
Instant end = Instant.now();
Duration elapsed = Duration.between(start, end);
IO.println(elapsed);

This prints a result like PT0.003S (3 milliseconds), using ISO 8601 duration format, where PT stands for Period of Time.

13.4 Testing

Testing code that is time dependent can be difficult. If in code that depend on time (e.g., booking an exam session) now() is hardcoded, it makes testing difficult.

For this, Java provides the Clock class, which you can pass to time-based methods. A Clock can be fixed to a specific instant or offset from the actual system time, making it ideal for testing. A clock object can be used as argument of now().

Clocks can be created with the following factory methods:

  • fixed(instant, zone) returns a clock that always returns the same instant, it is mainly used to testing purposes.
  • offset(base, offset) returns a clock yields instants from the specified clock with the specified duration added, used to simulate past or future events w.r.t. a the given base clock.
  • systemDefaultZone() obtains a clock aligned with the system clock in the system time zone, be careful since it implies a dependency to the default time-zone into your application.
  • systemUTC() obtains a clock aligned with the system clock in the UTC time zone.

As a simple example of method that has dependency on the current time is the computation of a total amount due to return an initial capital plus a monthly rate, given the initial date.

Given an initial sum (\(amount\)) and a monthly interest rate (\(0 < rate < 1\)), the total due (\(due\)) can be computed using the compount interest rate after a given number of months (\(months\)):

\[ due = amount \cdot (1+rate)^{months} \]

An example of code used to compute the amount due today is:

static double totalDue(double amount, LocalDate begin, double monthlyRate){
  LocalDate today = LocalDate.now();

  Period interval = Period.between(begin, today);      
  int months = interval.getMonths();
  double compoundRate = Math.pow(1.0+monthlyRate, months);
  return amount*compoundRate;
}

Of course this method will return a different value based on the day of execution of the test, thus making testing more difficult.

A testable version of the previous date-based computation should include a Clock in the computation that enable faking the date of execution:

static Clock clock = Clock.system();
static double totalDue(double amount, LocalDate begin, double monthlyRate){
  LocalDate today = LocalDate.now(clock);
  Period interval = Period.between(begin, today);      
  int months = interval.getMonths();
  double compoundRate = Math.pow(1.0+monthlyRate, months);
  return amount*compoundRate;
}

With the testable version it is possible to write the following test

@Test
public static void testTotalDue(){
1  LocalDate begin = LocalDate.of(2025,4,10);
  double r = 0.01;
  int amount = 1000;

2  LocalDate in4 = begin.plusMonths(4));
3  clock = Clock.fixed(in4.atStartOfDay(zone).toInstant(),
                      ZoneId.systemDefault());
  
4  double t = totalDue(amount, begin, r);

  assertEquals(amount*Math.pow(1+r, 4), t, 1);
}
1
the values of the arguments to test the method
2
compute the day four mounths after the begin date
3
create a fixed clock with the latter date
4
call the method regularly

Wrap-up

  • Old Date class does not handle time zones correctly. If all you need is a simple date container, storing year/month/day in a custom class may be fine.

  • If you plan to calculate time intervals or work with time zones, using the java.time API is strongly advised.

  • New classes provide a consistent structure for both time and date measures:

    • They are immutable
    • Operations can be performed using existing methods
  • Testing time and date based operations can be complex, the use of Clock is advised to make them testable