Date and Time API Overview – Date and Time

17.1 Date and Time API Overview

The java.time package provides the main support for dealing with dates and times. It contains the main classes that represent date and time values, including those that represent an amount of time.

  • LocalDate: This class represents a date in terms of date fields (year, month, day). A date has no time fields or a time zone. (This class is not to be confused with the java.util.Date legacy class.)
  • LocalTime: This class represents time in a 24-hour day in terms of time fields (hour, minute, second, nanosecond). A time has no date fields or a time zone.
  • LocalDateTime: This class represents the concept of date and time combined, in terms of both date and time fields. A date-time has no time zone.
  • ZonedDateTime: This class represents the concept of a date-time with a time zone— that is, a zoned date-time.
  • Instant: This class represents a measurement of time as a point on a timeline starting from a specific origin (called the epoch). An instant is represented with nanosecond precision and can be a negative value.
  • Period: This class represents an amount or quantity of time in terms of number of days, months, and years, which can be negative. A period is a date-based amount of time. It has no notion of a clock time, a date, or a time zone.
  • Duration: This class represents an amount or quantity of time in terms of number of seconds and nanoseconds, which can be negative. A duration is a time-based amount of time. As with instants, durations have no notion of a clock time, a date, or a time zone.

We will use the term temporal objects to mean objects of classes that represent temporal concepts.

The temporal classes implement immutable and thread-safe temporal objects. The state of an immutable object cannot be changed. Any method that is supposed to modify such an object returns a modified copy of the temporal object. It is a common mistake to ignore the object returned, thinking that the current object has been modified. Thread-safety guarantees that the state of such an object is not compromised by concurrent access.

Table 17.1 summarizes the fields in selected classes from the Date and Time API. The table shows the relative size of the objects of these classes in terms of their fields; for example, a LocalTime has only time fields, whereas a ZonedDateTime has time-, date-, and zone-based fields. The three asterisks *** indicate that this information can be derived by methods provided by the class, even though these fields do not exist in an object of the Duration class.

Table 17.1 Fields in Selected Classes in the Date and Time API

ClassesYearMonthDayHoursMinutesSeconds/NanosZone offsetZone ID
LocalTime
(p. 1027)
   +++  
LocalDate
(p. 1027)
+++     
LocalDateTime
(p. 1027)
++++++  
ZonedDate-Time
(p. 1072)
++++++++
Instant
(p. 1049)
     +  
Period
(p. 1057)
+++     
Duration
(p. 1064)
  *********+  

The information in Table 17.1 is crucial to understanding how the objects of these classes can be used. A common mistake is to access, format, or parse a temporal object that does not have the required temporal fields. For example, a LocalTime object has only time fields, so trying to format it with a formatter for date fields will result in a java.time.DateTimeException. Many methods will also throw an exception if an invalid or an out-of-range argument is passed in the method call. It is important to keep in mind which temporal fields constitute the state of a temporal object.

Table 17.2 provides an overview of the method naming conventions used in the temporal classes LocalTime, LocalDate, LocalDateTime, ZonedDateTime, and Instant. This method naming convention makes it easy to use the API, as it ensures method naming is standardized across all temporal classes. Depending on the method, the suffix XXX in a method name can be a specific field (e.g., designate the Year field in the getYear method name), a specific unit (e.g., designate the unit Days for number of days in the plusDays method name), or a class name (e.g., designate the class type in the toLocalDate method name).

Table 17.2 Selected Method Name Prefixes in the Temporal Classes

Prefix (parameters not shown)Usage
atXXX()Create a new temporal object by combining this temporal object with another temporal object. Not provided by the ZonedDateTime class.
of() ofXXX()Static factory methods for constructing temporal objects from constituent temporal fields.
get() getXXX()Access specific fields in this temporal object.
isXXX()Check specific properties of this temporal object.
minus() minusXXX()Return a copy of this temporal object after subtracting an amount of time.
plus() plusXXX()Return a copy of this temporal object after adding an amount of time.
toXXX()Convert this temporal object to another type.
with() withXXX()Create a copy of this temporal object with one field modified.

Apart from the methods shown in Table 17.2, the selected methods shown in Table 17.3 are common to the temporal classes LocalTime, LocalDate, LocalDateTime, ZonedDateTime, and Instant.

Table 17.3 Selected Common Methods in the Temporal Classes

Method (parameters not shown)Usage
now()Static method that obtains the current time from the system or specified clock in the default or specified time zone.
from()Static method to obtain an instance of this temporal class from another temporal.
until()Calculate the amount of time from this temporal object to another temporal object.
toString()Create a text representation of this temporal object.
equals()Compare two temporal objects for equality.
hashCode()Returns a hash code for this temporal object.
compareTo()Compare two temporal objects. (The class ZonedDateTime does not implement the Comparable<E> interface.)
parse()Static method to obtain a temporal instance from a specified text string (§18.6, p. 1127).
format()Create a text representation of this temporal object using a specified formatter (§18.6, p. 1127). (Instant class does not provide this method.)

Subsequent sections in this chapter provide ample examples of how to create, combine, convert, access, and compare temporal objects, including the use of temporal arithmetic and dealing with time zones and daylight savings. For formatting and parsing temporal objects, see §18.6, p. 1127.

Factors Affecting Performance – Streams

Factors Affecting Performance

There are no guarantees that executing a stream in parallel will improve the performance. In this subsection we look at some factors that can affect performance.

Benchmarking

In general, increasing the number of CPU cores and thereby the number of threads that can execute in parallel only scales performance up to a threshold for a given size of data, as some threads might become idle if there is no data left for them to process. The number of CPU cores boosts performance to a certain extent, but it is not the only factor that should be considered when deciding to execute a stream in parallel.

Inherent in the total cost of parallel processing is the start-up cost of setting up the parallel execution. At the onset, if this cost is already comparable to the cost of sequential execution, not much can be gained by resorting to parallel execution.

A combination of the following three factors can be crucial in deciding whether a stream should be executed in parallel:

  • Sufficiently large data size

The size of the stream must be large enough to warrant parallel processing; otherwise, sequential processing is preferable. The start-up cost can be too prohibitive for parallel execution if the stream size is too small.

  • Computation-intensive stream operations

If the stream operations are small computations, then the stream size should be proportionately large as to warrant parallel execution. If the stream operations are computation-intensive, the stream size is less significant, and parallel execution can boost performance.

  • Easily splittable stream

If the cost of splitting the stream into substreams is higher than processing the substreams, employing parallel execution can be futile. Collections like Array-Lists, HashMaps, and simple arrays are efficiently splittable, whereas LinkedLists and IO-based data sources are less efficient in this regard.

Benchmarking—that is, measuring performance—is strongly recommended to decide whether parallel execution will be beneficial. Example 16.14 illustrates a simple scheme where reading the system clock before and after a stream is executed can be used to get a sense of how well a stream performs.

The class StreamBenchmarks in Example 16.14 defines five methods, at (1) through (5), that compute the sum of values from 1 to n. These methods compute the sum in various ways. Each method is executed with four different values of n; that is, the stream size is the number of values for summation. The program prints the benchmarks for each method for the different values of n, which of course can vary, as many factors can influence the results—the most significant one being the number of CPU cores on the computer.

  • The methods seqSumRangeClosed() at (1) and parSumRangeClosed() at (2) perform the computation on a sequential and a parallel stream, respectively, that are created with the closeRange() method.

Click here to view code image

return LongStream.rangeClosed(1L, n).sum();                // Sequential stream

return LongStream.rangeClosed(1L, n).parallel().sum();     // Parallel stream

Benchmarks from Example 16.14:

  Size   Sequential Parallel
   1000   0.05681   0.11031
  10000   0.06698   0.13979
 100000   0.71274   0.52627
1000000   7.02237   4.37249

The terminal operation sum() is not computation-intensive. The parallel stream starts to show better performance when the number of values approaches 100000. The stream size is then significantly large for the parallel stream to show better performance. Note that the range of values defined by the arguments of the rangeClosed() method can be efficiently split into substreams, as its start and end values are provided.

  • The methods seqSumIterate() at (3) and parSumIterate() at (4) return a sequential and a parallel sequential stream, respectively, that is created with the iterate() method.

Click here to view code image

return LongStream.iterate(1L, i -> i + 1).limit(n).sum();            // Sequential

return LongStream.iterate(1L, i -> i + 1).limit(n).parallel().sum(); // Parallel

Benchmarks from Example 16.14:

  Size   Sequential Parallel
   1000   0.08645   0.34696
  10000   0.35687   1.27861
 100000   3.24083  11.38709
1000000  29.92285 117.87909

The method iterate() creates an infinite stream, and the limit() intermediate operation truncates the stream according to the value of n. The performance of both streams degrades fast when the number of values increases. However, the parallel stream performs worse than the sequential stream in all cases. The values generated by the iterate() method are not known before the stream is executed, and the limit() operation is also stateful, making the process of splitting the values into substreams inefficient in the case of the parallel stream.

  • The method iterSumLoop() at (5) uses a for(;;) loop to compute the sum.

Benchmarks from Example 16.14:

  Size   Iterative
   1000   0.00586
  10000   0.02330
 100000   0.22352
1000000   2.49677

Using a for(;;) loop to calculate the sum performs best for all values of n compared to the streams, showing that significant overhead is involved in using streams for summing a sequence of numerical values.

In Example 16.14, the methods measurePerf() at (6) and xqtFunctions() at (13) create the benchmarks for functions passed as parameters. In the measurePerf() method, the system clock is read at (8) and the function parameter func is applied at (9). The system clock is read again at (10) after the function application at (9) has completed. The execution time calculated at (10) reflects the time for executing the function. Applying the function func evaluates the lambda expression or the method reference implementing the LongFunction interface. In Example 16.14, the function parameter func is implemented by method references that call methods, at (1) through (5), in the StreamBenchmarks class whose execution time we want to measure.

Click here to view code image

public static <R> double measurePerf(LongFunction<R> func, long n) { // (6)
  // …
  double start = System.nanoTime();                                // (8)
  result = func.apply(n);                                          // (9)
  double duration = (System.nanoTime() – start)/1_000_000;         // (10) ms.
  // …
}

Example 16.14 Benchmarking

Click here to view code image

import java.util.function.LongFunction;
import java.util.stream.LongStream;
/*
 * Benchmark the execution time to sum numbers from 1 to n values
 * using streams.
 */
public final class StreamBenchmarks {
  public static long seqSumRangeClosed(long n) {                       // (1)
    return LongStream.rangeClosed(1L, n).sum();
  }
  public static long paraSumRangeClosed(long n) {                      // (2)
    return LongStream.rangeClosed(1L, n).parallel().sum();
  }
  public static long seqSumIterate(long n) {                           // (3)
    return LongStream.iterate(1L, i -> i + 1).limit(n).sum();
  }
  public static long paraSumIterate(long n) {                          // (5)
    return LongStream.iterate(1L, i -> i + 1).limit(n).parallel().sum();
  }
  public static long iterSumLoop(long n) {                             // (5)
    long result = 0;
    for (long i = 1L; i <= n; i++) {
      result += i;
    }
    return result;
  }
  /*
   * Applies the function parameter func, passing n as parameter.
   * Returns the average time (ms.) to execute the function 100 times.
   */
  public static <R> double measurePerf(LongFunction<R> func, long n) { // (6)
    int numOfExecutions = 100;
    double totTime = 0.0;
    R result = null;
    for (int i = 0; i < numOfExecutions; i++) {                        // (7)
      double start = System.nanoTime();                                // (8)
      result = func.apply(n);                                          // (9)
      double duration = (System.nanoTime() – start)/1_000_000;         // (10)
      totTime += duration;                                             // (11)
    }
    double avgTime = totTime/numOfExecutions;                          // (12)
    return avgTime;
  }
  /*
   * Executes the functions in the varargs parameter funcs
   * for different stream sizes.
   */
  public static <R> void xqtFunctions(LongFunction<R>… funcs) {      // (13)
    long[] sizes = {1_000L, 10_000L, 100_000L, 1_000_000L};            // (14)
    // For each stream size …
    for (int i = 0; i < sizes.length; ++i) {                           // (15)
      System.out.printf(“%7d”, sizes[i]);
      // … execute the functions passed in the varargs parameter funcs.
      for (int j = 0; j < funcs.length; ++j) {                         // (16)
        System.out.printf(“%10.5f”, measurePerf(funcs[j], sizes[i]));
      }
      System.out.println();
    }
  }
  public static void main(String[] args) {                             // (17)
    System.out.println(“Streams created with the rangeClosed() method:”);// (18)
    System.out.println(”  Size   Sequential Parallel”);
    xqtFunctions(StreamBenchmarks::seqSumRangeClosed,
                 StreamBenchmarks::paraSumRangeClosed);
    System.out.println(“Streams created with the iterate() method:”);  // (19)
    System.out.println(”  Size   Sequential Parallel”);
    xqtFunctions(StreamBenchmarks::seqSumIterate,
                 StreamBenchmarks::paraSumIterate);
    System.out.println(“Iterative solution with an explicit loop:”);   // (20)
    System.out.println(”  Size   Iterative”);
    xqtFunctions(StreamBenchmarks::iterSumLoop);
  }
}

Possible output from the program:

Click here to view code image

Streams created with the rangeClosed() method:
  Size   Sequential Parallel
   1000   0.05681   0.11031
  10000   0.06698   0.13979
 100000   0.71274   0.52627
1000000   7.02237   4.37249
Streams created with the iterate() method:
  Size   Sequential Parallel
   1000   0.08645   0.34696
  10000   0.35687   1.27861
 100000   3.24083  11.38709
1000000  29.92285 117.87909
Iterative solution with an explicit loop:
  Size   Iterative
   1000   0.00586
  10000   0.02330
 100000   0.22352
1000000   2.49677