Parallel Streams – Streams

16.9 Parallel Streams

The Stream API makes it possible to execute a sequential stream in parallel without rewriting the code. The primary reason for using parallel streams is to improve performance, but at the same time ensuring that the results obtained are the same, or at least compatible, regardless of the mode of execution. Although the API goes a long way to achieve its aim, it is important to understand the pitfalls to avoid when executing stream pipelines in parallel.

Building Parallel Streams

The execution mode of an existing stream can be set to parallel by calling the parallel() method on the stream (p. 933). The parallelStream() method of the Collection interface can be used to create a parallel stream with a collection as the data source (p. 897). No other code is necessary for parallel execution, as the data partitioning and thread management for a parallel stream are handled by the API and the JVM. As with any stream, the stream is not executed until a terminal operation is invoked on it.

The isParallel() method of the stream interfaces can be used to determine whether the execution mode of a stream is parallel (p. 933).

Parallel Stream Execution

The Stream API allows a stream to be executed either sequentially or in parallel— meaning that all stream operations can execute either sequentially or in parallel. A sequential stream is executed in a single thread running on one CPU core. The elements in the stream are processed sequentially in a single pass by the stream operations that are executed in the same thread (p. 891).

A parallel stream is executed by different threads, running on multiple CPU cores in a computer. The stream elements are split into substreams that are processed by multiple instances of the stream pipeline being executed in multiple threads. The partial results from processing of each substream are merged (or combined) into a final result (p. 891).

Parallel streams utilize the Fork/Join Framework (§23.3, p. 1447) under the hood for executing parallel tasks. This framework provides support for the thread management necessary to execute the substreams in parallel. The number of threads employed during parallel stream execution is dependent on the CPU cores in the computer.

Figure 16.12, p. 963, illustrates parallel functional reduction using the three-argument reduce(identity, accumulator, combiner) terminal operation (p. 962).

Figure 16.14, p. 967, illustrates parallel mutable reduction using the three-argument collect(supplier, accumulator, combiner) terminal operation (p. 966).

Counting – Streams

Counting

The collector created by the Collectors.counting() method performs a functional reduction to count the input elements.

Click here to view code image

static <T> Collector<T,?,Long> counting()

The collector returned counts the number of input elements of type T. If there are no elements, the result is Long.valueOf(0L). Note that the result is of type Long.

The wildcard ? represents any type, and in the method declaration, it is the type parameter for the mutable type that is accumulated by the reduction operation.

In the stream pipeline at (1), the collector Collectors.counting() is used in a standalone capacity to count the number of jazz music CDs.

Click here to view code image

Long numOfJazzCds1 = CD.cdList.stream().filter(CD::isJazz)
    .collect(Collectors.counting());                  // (1) Standalone collector
System.out.println(numOfJazzCds1);                    // 3

In the stream pipeline at (2), the collector Collectors.counting() is used as a downstream collector in a grouping operation that groups the CDs by musical genre and uses the downstream collector to count the number of CDs in each group.

Click here to view code image

Map<Genre, Long> grpByGenre = CD.cdList.stream()
    .collect(Collectors.groupingBy(
                 CD::genre,
                 Collectors.counting()));             // (2) Downstream collector
System.out.println(grpByGenre);                       // {POP=2, JAZZ=3}
System.out.println(grpByGenre.get(Genre.JAZZ));       // 3

The collector Collectors.counting() performs effectively the same functional reduction as the Stream.count() terminal operation (p. 953) at (3).

Click here to view code image

long numOfJazzCds2 = CD.cdList.stream().filter(CD::isJazz)
    .count();                                         // (3) Stream.count()
System.out.println(numOfJazzCds2);                    // 3

Finding Min/Max

The collectors created by the Collectors.maxBy() and Collectors.minBy() methods perform a functional reduction to find the maximum and minimum elements in the input elements, respectively. As there might not be any input elements, an Optional<T> is returned as the result.

Click here to view code image

static <T> Collector<T,?,Optional<T>> maxBy(Comparator<? super T> cmp)
static <T> Collector<T,?,Optional<T>> minBy(Comparator<? super T> cmp)

Return a collector that produces an Optional<T> with the maximum or minimum element of type T according to the specified Comparator, respectively.

The natural order comparator for CDs defined at (1) is used in the stream pipelines below to find the maximum CD. The collector Collectors.maxBy() is used as a standalone collector at (2), using the natural order comparator to find the maximum CD. The Optional<CD> result can be queried for the value.

Click here to view code image

Comparator<CD> natCmp = Comparator.naturalOrder(); // (1)
Optional<CD> maxCD = CD.cdList.stream()
    .collect(Collectors.maxBy(natCmp));            // (2) Standalone collector
System.out.println(“Max CD: “
    + maxCD.map(CD::title).orElse(“No CD.”));      // Max CD: Java Jive

In the pipeline below, the CDs are grouped by musical genre, and the CDs in each group are reduced to the maximum CD by the downstream collector Collectors.maxBy() at (3). Again, the downstream collector uses the natural order comparator, and the Optional<CD> result in each group can be queried.

Click here to view code image

// Group CDs by musical genre, and max CD in each group.
Map<Genre, Optional<CD>> grpByGenre = CD.cdList.stream()
    .collect(Collectors.groupingBy(
        CD::genre,
        Collectors.maxBy(natCmp)));       // (3) Downstream collector
System.out.println(grpByGenre);
//{JAZZ=Optional[<Jaav, “Java Jam”, 6, 2017, JAZZ>],
// POP=Optional[<Jaav, “Java Jive”, 8, 2017, POP>]}

System.out.println(“Title of max Jazz CD: “
    + grpByGenre.get(Genre.JAZZ)
                .map(CD::title)
                .orElse(“No CD.”));       // Title of max Jazz CD: Java Jam

The collectors created by the Collectors.maxBy() and Collectors.minBy() methods are effectively equivalent to the max() and min() terminal operations provided by the stream interfaces (p. 954), respectively. In the pipeline below, the max() terminal operation reduces the stream of CDs to the maximum CD at (4) using the natural order comparator, and the Optional<CD> result can be queried.

Click here to view code image

Optional<CD> maxCD1 = CD.cdList.stream()
    .max(natCmp);                         // (4) max() on Stream<CD>.
System.out.println(“Title of max CD: “
    + maxCD1.map(CD::title)
            .orElse(“No CD.”));           // Title of max CD: Java Jive

The LocalTime Class – Date and Time

The LocalTime Class

The declaration statements below show examples of creating instances of the LocalTime class to represent time on a 24-hour clock in terms of hours, minutes, seconds, and nanoseconds.

Click here to view code image

LocalTime time1 = LocalTime.of(8, 15, 35, 900);   // 08:15:35.000000900
LocalTime time2 = LocalTime.of(16, 45);           // 16:45
// LocalTime time3 = LocalTime.of(25, 13, 30);    // DateTimeException

The ranges of values for time fields hour (0–23), minute (0–59), second (0–59), and nanosecond (0–999,999,999) are defined by the ISO standard. The toString() method of the class will format the time fields according to the ISO standard as follows:

HH:mm:ss.SSSSSSSSS

Omitting the seconds (ss) and fractions of seconds (SSSSSSSSS) in the call to the of() method implies that their value is zero. (More on formatting in §18.6, p. 1134.) In the second declaration statement above, the seconds and the nanoseconds are not specified in the method call, resulting in their values being set to zero. In the third statement, the value of the hour field (25) is out of range, and if the statement is uncommented, it will result in a DateTimeException.

The LocalDate Class

Creating instances of the LocalDate class is analogous to creating instances of the LocalTime class. The of() method of the LocalDate class is passed values for date fields: the year, month of the year, and day of the month.

Click here to view code image

LocalDate date1 = LocalDate.of(1969, 7, 20);            // 1969-07-20
LocalDate date2 = LocalDate.of(-3113, Month.AUGUST, 11);// -3113-08-11
//  LocalDate date3 = LocalDate.of(2021, 13, 11);       // DateTimeException
//  LocalDate date4 = LocalDate.of(2021, 2, 29);        // DateTimeException

The ranges of the values for date fields year, month, and day are (–999,999,999 to +999,999,999), (1–12), and (1–31), respectively. The month can also be specified using the enum constants of the java.time.Month class, as in the second declaration statement above. A DateTimeException is thrown if the value of any parameter is out of range, or if the day is invalid for the specified month of the year. In the third declaration, the value of the month field 13 is out of range. In the last declaration, the month of February cannot have 29 days, since the year 2021 is not a leap year.

The toString() method of the LocalDate class will format the date fields according to the ISO standard (§18.6, p. 1134):

uuuu-MM-dd

The year is represented as a proleptic year in the ISO standard, which can be negative. A year in CE (Current Era, or AD) has the same value as a proleptic year; for example, 2021 CE is the same as the proleptic year 2021. However, for a year in BCE (Before Current Era, or BC), the proleptic year 0 corresponds to 1 BCE, the proleptic year –1 corresponds to 2 BCE, and so on. In the second declaration in the preceding set of examples, the date -3113-08-11 corresponds to 11 August 3114 BCE.

Side Effects – Streams

Side Effects

Efficient execution of parallel streams that produces the desired results requires the stream operations (and their behavioral parameters) to avoid certain side effects.

  • Non-interfering behaviors

The behavioral parameters of stream operations should be non-interfering (p. 909)—both for sequential and parallel streams. Unless the stream data source is concurrent, the stream operations should not modify it during the execution of the stream. See building streams from collections (p. 897).

  • Stateless behaviors

The behavioral parameters of stream operations should be stateless (p. 909)— both for sequential and parallel streams. A behavioral parameter implemented as a lambda expression should not depend on any state that might change during the execution of the stream pipeline. The results from a stateful behavioral parameter can be nondeterministic or even incorrect. For a stateless behavioral parameter, the results are always the same.

Shared state that is accessed by the behavior parameters of stream operations in a pipeline is not a good idea. Executing the pipeline in parallel can lead to race conditions in accessing the global state, and using synchronization code to provide thread-safety may defeat the purpose of parallelization. Using the three-argument reduce() or collect() method can be a better solution to encapsulate shared state.

The intermediate operations distinct(), skip(), limit(), and sorted() are stateful (p. 915, p. 915, p. 917, p. 929). See also Table 16.3, p. 938. They can carry extra performance overhead when executed in a parallel stream, as such an operation can entail multiple passes over the data and may require significant data buffering.

Ordering

An ordered stream (p. 891) processed by operations that preserve the encounter order will produce the same results, regardless of whether it is executed sequentially or in parallel. However, repeated execution of an unordered stream— sequential or parallel—can produce different results.

Preserving the encounter order of elements in an ordered parallel stream can incur a performance penalty. The performance of an ordered parallel stream can be improved if the ordering constraint is removed by calling the unordered() intermediate operation on the stream (p. 932).

The three stateful intermediate operations distinct(), skip(), and limit() can improve performance in a parallel stream that is unordered, as compared to one that is ordered (p. 915, p. 915, p. 917). The distinct() operation need only buffer any occurrence of a duplicate value in the case of an unordered parallel stream, rather than the first occurrence. The skip() operation can skip any n elements in the case of an unordered parallel stream, not necessarily the first n elements. The limit() operation can truncate the stream after any n elements in the case of an unordered parallel stream, and not necessarily after the first n elements.

The terminal operation findAny() is intentionally nondeterministic, and can return any element in the stream (p. 952). It is specially suited for parallel streams.

The forEach() terminal operation ignores the encounter order, but the forEachOrdered() terminal operation preserves the order (p. 948). The sorted() stateful intermediate operation, on the other hand, enforces a specific encounter order, regardless of whether it executed in a parallel pipeline (p. 929).

Autoboxing and Unboxing of Numeric Values

As the Stream API allows both object and numeric streams, and provides support for conversion between them (p. 934), choosing a numeric stream when possible can offset the overhead of autoboxing and unboxing in object streams.

As we have seen, in order to take full advantage of parallel execution, composition of a stream pipeline must follow certain rules to facilitate parallelization. In summary, the benefits of using parallel streams are best achieved when:

  • The stream data source is of a sufficient size and the stream is easily splittable into substreams.
  • The stream operations have no adverse side effects and are computation-intensive enough to warrant parallelization.

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

Summing – Streams

Summing

The summing collectors perform a functional reduction to produce the sum of the numeric results from applying a numeric-valued function to the input elements.

Click here to view code image

static <T> Collector<T,?,
NumType> summingNum
Type(
       To
NumType
Function<? super T> mapper)

Returns a collector that produces the sum of a numtype-valued function applied to the input elements. If there are no input elements, the result is zero. The result is of NumType.

NumType is Int (but it is Integer when used as a type name), Long, or Double, and the corresponding numtype is int, long, or double.

The collector returned by the Collectors.summingInt() method is used at (1) as a standalone collector to find the total number of tracks on the CDs. The mapper function CD::noOfTracks passed as an argument extracts the number of tracks from each CD on which the functional reduction is performed.

Click here to view code image

Integer sumTracks = CD.cdList.stream()
    .collect(Collectors.summingInt(CD::noOfTracks));   // (1) Standalone collector
System.out.println(sumTracks);                         // 42

In the pipeline below, the CDs are grouped by musical genre, and the number of tracks on CDs in each group summed by the downstream collector is returned by the Collectors.summingInt() method at (2).

Click here to view code image

Map<Genre, Integer> grpByGenre = CD.cdList.stream()
    .collect(Collectors.groupingBy(
         CD::genre,
         Collectors.summingInt(CD::noOfTracks)));    // (2) Downstream collector
System.out.println(grpByGenre);                      // {POP=18, JAZZ=24}
System.out.println(grpByGenre.get(Genre.JAZZ));      // 24

The collector Collectors.summingInt() performs effectively the same functional reduction at (3) as the IntStream.sum() terminal operation (p. 973).

Click here to view code image

int sumTracks2 = CD.cdList.stream()                  // (3) Stream<CD>
    .mapToInt(CD::noOfTracks)                        // IntStream
    .sum();
System.out.println(sumTracks2);                      // 42

Averaging

The averaging collectors perform a functional reduction to produce the average of the numeric results from applying a numeric-valued function to the input elements.

Click here to view code image

static <T> Collector<T,?,Double> averaging
NumType
(
       To
NumType
Function<? super T> mapper)

Returns a collector that produces the arithmetic mean of a numtype-valued function applied to the input elements. If there are no input elements, the result is zero. The result is of type Double.

NumType is Int, Long, or Double, and the corresponding numtype is int, long, or double.

The collector returned by the Collectors.averagingInt() method is used at (1) as a standalone collector to find the average number of tracks on the CDs. The mapper function CD::noOfTracks passed as an argument extracts the number of tracks from each CD on which the functional reduction is performed.

Click here to view code image

Double avgNoOfTracks1 = CD.cdList.stream()
    .collect(Collectors
        .averagingInt(CD::noOfTracks));             // (1) Standalone collector
System.out.println(avgNoOfTracks1);                 // 8.4

In the pipeline below, the CDs are grouped by musical genre, and the downstream collector Collectors.averagingInt() at (2) calculates the average number of tracks on the CDs in each group.

Click here to view code image

Map<Genre, Double> grpByGenre = CD.cdList.stream()
    .collect(Collectors.groupingBy(
       CD::genre,
       Collectors.averagingInt(CD::noOfTracks)      // (2) Downstream collector
       ));
System.out.println(grpByGenre);                     // {POP=9.0, JAZZ=8.0}
System.out.println(grpByGenre.get(Genre.JAZZ));     // 8.0

The collector created by the Collectors.averagingInt() method performs effectively the same functional reduction as the IntStream.average() terminal operation (p. 974) at (3).

Click here to view code image OptionalDouble avgNoOfTracks2 = CD.cdList.stream()  // Stream<CD>
    .mapToInt(CD::noOfTracks)                       // IntStream
    .average();                                     // (3)
System.out.println(avgNoOfTracks2.orElse(0.0));     // 8.4

Comparing Dates and Times – Date and Time

Comparing Dates and Times

It is also possible to check whether a temporal object represents a point in time before or after another temporal object of the same type. In addition, the LocalDate and LocalDateTime classes provide an isEqual() method that determines whether a temporal object is equal to another temporal object of the same type. In contrast, the equals() method allows equality comparison with an arbitrary object.

Click here to view code image

LocalDate d1 = LocalDate.of(1948, 2, 28);                  // 1948-02-28
LocalDate d2 = LocalDate.of(1949, 3, 1);                   // 1949-03-01
boolean result1 = d1.isBefore(d2);                         // true
boolean result2 = d2.isAfter(d1);                          // true
boolean result3 = d1.isAfter(d1);                          // false
boolean result4 = d1.isEqual(d2);                          // false
boolean result5 = d1.isEqual(d1);                          // true
boolean result6 = d1.isLeapYear();                         // true

The temporal classes implement the Comparable<E> interface, providing the compareTo() method so that temporal objects can be compared in a meaningful way. The temporal classes also override the equals() and the hashCode() methods of the Object class. These methods make it possible to both search for and sort temporal objects.

Click here to view code image

// LocalTime
boolean isBefore(LocalTime other)
boolean isAfter(LocalTime other)

Determine whether this LocalTime represents a point on the timeline before or after the other time, respectively.

Click here to view code image

// LocalDate
boolean isBefore(ChronoLocalDate other)
boolean isAfter(ChronoLocalDate other)
boolean isEqual(ChronoLocalDate other)
boolean isLeapYear()

The first two methods determine whether this LocalDate represents a point on the timeline before or after the other date, respectively. The LocalDate class implements the ChronoLocalDate interface.

The third method determines whether this date is equal to the specified date.

The last method checks for a leap year according to the ISO proleptic calendar system rules.

Click here to view code image

// LocalDateTime
boolean isBefore(ChronoLocalDateTime<?> other)
boolean isAfter(ChronoLocalDateTime<?> other)
boolean isEqual(ChronoLocalDateTime<?> other)

The first two methods determine whether this LocalDateTime represents a point on the timeline before or after the specified date-time, respectively. The Local-DateTime class implements the ChronoLocalDateTime<LocalDateTime> interface.

The third method determines whether this date-time object represents the same point on the timeline as the other date-time.

Click here to view code image

int compareTo(LocalTime other)                // LocalTime
int compareTo(ChronoLocalDate other)          // LocalDate
int compareTo(ChronoLocalDateTime<?> other)   // LocalDateTime

Compare this temporal object to another temporal object. The three temporal classes implement the Comparable<E> functional interface. The compareTo() method returns 0 if the two temporal objects are equal, a negative value if this temporal object is less than the other temporal object, and a positive value if this temporal object is greater than the other temporal object.

Temporal Fields – Date and Time

Temporal Fields

The java.time.temporal.TemporalField interface represents a specific field of a temporal object. The java.time.temporal.ChronoField enum type implements this interface, defining the fields by constant names so that a specific field can be conveniently accessed. Selected constants from the ChronoField enum type include SECOND_OF_MINUTE, MINUTE_OF_DAY, DAY_OF_MONTH, MONTH_OF_YEAR, and YEAR.

The output from Example 17.4 shows a table with all the temporal fields defined by the ChronoField enum type.

Analogous to a ChronoUnit enum constant, a ChronoField enum constant can be queried by the following selected methods:

TemporalUnit getBaseUnit()

Gets the unit that the field is measured in. For example, ChronoField.DAY_OF_ MONTH.getBaseUnit() returns ChronoUnit.DAYS.

boolean isDateBased()
boolean isTimeBased()

Check whether this field represents a date or a time field, respectively. For example, ChronoField.HOUR_OF_DAY.isDateBased() is false, but ChronoField.SECOND_ OF_MINUTE.isTimeBased() is true.

Click here to view code image

boolean isSupportedBy(TemporalAccessor temporal)

Checks whether this field is supported by the specified temporal object. For example, ChronoField.YEAR.isSupportedBy(LocalTime.MIDNIGHT) is false.

static ChronoField[] values()

Returns an array containing the field constants of this enum type, in the order they are declared. This method is called at (7) in Example 17.4.

The temporal classes provide the method isSupported(field) to determine whether a temporal field is valid for a temporal object. In Example 17.4, this method is used at (8), (9), and (10) to determine whether each temporal field defined by the ChronoField enum type is a valid field for the different temporal classes.

The following methods of the temporal classes all accept a temporal field that designates a specific field of the temporal object:

Click here to view code image

LocalDate date = LocalDate.of(2021, 8, 13);
int monthValue = date.get(ChronoField.MONTH_OF_YEAR);
System.out.print(“Date ” + date + ” has month of the year: ” + monthValue);
// Date 2021-08-13 has month of the year: 8

Click here to view code image

LocalDateTime dateTime = LocalDateTime.of(2021, 8, 13, 20, 20);
System.out.print(“Date-time ” + dateTime);
dateTime = dateTime.with(ChronoField.DAY_OF_MONTH, 11)
                   .with(ChronoField.MONTH_OF_YEAR, 1)
                   .with(ChronoField.YEAR, 2022);
System.out.println(” changed to: ” + dateTime);
// Date-time 2021-08-13T20:20 changed to: 2022-01-11T20:20

In Example 17.4, the code at (1) and at (6) prints tables that show which ChronoUnit and ChronoField constants are valid in which temporal-based object. A LocalTime instance supports time-based units and fields, and a LocalDate instance supports date-based units and fields. A LocalDateTime or a ZonedDateTime supports both time-based and date-based units and fields. Using an invalid enum constant for a temporal object will invariably result in an UnsupportedTemporalTypeException being thrown.

Example 17.4 Valid Temporal Units and Temporal Fields

Click here to view code image

import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
public class ValidTemporalUnitsAndFields {
  public static void main(String[] args) {
    // Temporals:
    LocalTime time = LocalTime.now();
    LocalDate date = LocalDate.now();
    LocalDateTime dateTime = LocalDateTime.now();
    ZonedDateTime zonedDateTime = ZonedDateTime.now();
    Instant instant = Instant.now();
    // Print supported units:                                            // (1)
    System.out.printf(“%29s %s %s %s %s %s%n”,
        “ChronoUnit”, “LocalTime”, “LocalDate”, “LocalDateTime”,
        ” ZDT “, “Instant”);
    ChronoUnit[] units =  ChronoUnit.values();                           // (2)
    for (ChronoUnit unit : units) {
      System.out.printf(“%28S: %7b %9b %10b %9b %7b%n”,
          unit.name(), time.isSupported(unit), date.isSupported(unit),   // (3)
          dateTime.isSupported(unit), zonedDateTime.isSupported(unit),   // (4)
          instant.isSupported(unit));                                    // (5)
      }
    System.out.println();
    // Print supported fields:                                           // (6)
    System.out.printf(“%29s %s %s %s %s %s%n”,
        “ChronoField”, “LocalTime”, “LocalDate”, “LocalDateTime”,
        ” ZDT “, “Instant”);
    ChronoField[] fields =  ChronoField.values();                        // (7)
    for (ChronoField field : fields) {
      System.out.printf(“%28S: %7b %9b %10b %9b %7b%n”,
          field.name(), time.isSupported(field), date.isSupported(field),// (8)
          dateTime.isSupported(field), zonedDateTime.isSupported(field), // (9)
          instant.isSupported(field));                                   // (10)
    }
    System.out.println();
  }
}

Output from the program (ZDT stands for ZonedDateTime in the output):

Click here to view code image

                   ChronoUnit LocalTime LocalDate LocalDateTime  ZDT  Instant
                       NANOS:    true     false       true      true    true
                      MICROS:    true     false       true      true    true
                      MILLIS:    true     false       true      true    true
                     SECONDS:    true     false       true      true    true
                     MINUTES:    true     false       true      true    true
                       HOURS:    true     false       true      true    true
                   HALF_DAYS:    true     false       true      true    true
                        DAYS:   false      true       true      true    true
                       WEEKS:   false      true       true      true   false
                      MONTHS:   false      true       true      true   false
                       YEARS:   false      true       true      true   false
                     DECADES:   false      true       true      true   false
                   CENTURIES:   false      true       true      true   false
                   MILLENNIA:   false      true       true      true   false
                        ERAS:   false      true       true      true   false
                     FOREVER:   false     false      false     false   false
                  ChronoField LocalTime LocalDate LocalDateTime  ZDT  Instant
              NANO_OF_SECOND:    true     false       true      true    true
                 NANO_OF_DAY:    true     false       true      true   false
             MICRO_OF_SECOND:    true     false       true      true    true
                MICRO_OF_DAY:    true     false       true      true   false
             MILLI_OF_SECOND:    true     false       true      true    true
                MILLI_OF_DAY:    true     false       true      true   false
            SECOND_OF_MINUTE:    true     false       true      true   false
               SECOND_OF_DAY:    true     false       true      true   false
              MINUTE_OF_HOUR:    true     false       true      true   false
               MINUTE_OF_DAY:    true     false       true      true   false
                HOUR_OF_AMPM:    true     false       true      true   false
          CLOCK_HOUR_OF_AMPM:    true     false       true      true   false
                 HOUR_OF_DAY:    true     false       true      true   false
           CLOCK_HOUR_OF_DAY:    true     false       true      true   false
                 AMPM_OF_DAY:    true     false       true      true   false
                 DAY_OF_WEEK:   false      true       true      true   false
ALIGNED_DAY_OF_WEEK_IN_MONTH:   false      true       true      true   false
 ALIGNED_DAY_OF_WEEK_IN_YEAR:   false      true       true      true   false
                DAY_OF_MONTH:   false      true       true      true   false
                 DAY_OF_YEAR:   false      true       true      true   false
                   EPOCH_DAY:   false      true       true      true   false
       ALIGNED_WEEK_OF_MONTH:   false      true       true      true   false
        ALIGNED_WEEK_OF_YEAR:   false      true       true      true   false
               MONTH_OF_YEAR:   false      true       true      true   false
             PROLEPTIC_MONTH:   false      true       true      true   false
                 YEAR_OF_ERA:   false      true       true      true   false
                        YEAR:   false      true       true      true   false
                         ERA:   false      true       true      true   false
             INSTANT_SECONDS:   false     false      false      true    true
              OFFSET_SECONDS:   false     false      false      true   false

Accessing Temporal Fields in an Instant – Date and Time

Accessing Temporal Fields in an Instant

The Instant class provides the following selected methods to access temporal fields in an instance of the class:

int getNano()
long getEpochSecond()

Return the number of nanoseconds and the number of seconds represented by this instant from the start of the epoch, respectively. Note that the method names are without the s at the end.

Click here to view code image

int get(TemporalField field)
long getLong(TemporalField field)

The get(field) method will return the value of the specified field in this Instant as an int. Only the following ChronoField constants are supported: NANO_OF_SECOND, MICRO_OF_SECOND, MILLI_OF_SECOND, and INSTANT_SECONDS (p. 1046). The first three fields will always return a valid value, but the INSTANT_SECONDS field will throw a DateTimeException if the value does not fit into an int. All other fields result in an UnsupportedTemporalTypeException.

As the getLong(field) method returns the value of the specified field in this Instant as a long, there is no problem with overflow in returning a value designated by any of the four fields mentioned earlier.

Click here to view code image

boolean isSupported(TemporalField field)

The isSupported(field) determines whether the specified field is supported by this instant.

long toEpochMilli()

Returns the number of milliseconds that represent this Instant from the start of the epoch. The method throws an ArithmeticException in case of number overflow.

The code below shows how the getNano() and getEpochSecond() methods of the Instant class read the value of the nanosecond and the epoch-second fields of an Instant object, respectively.

Click here to view code image

Instant inst = Instant.ofEpochSecond(24L*60*60,    // 1 day and
                                     555_555_555L);// 555555555 ns after epoch.
out.println(inst);                   // 1970-01-02T00:00:00.555555555Z
out.println(inst.getNano());         // 555555555 ns
out.println(inst.getEpochSecond());  // 86400 s

Reading the nanosecond and epoch-second fields of an Instant in different units can be done using the get(field) method. Note the value of the nanosecond field expressed in different units using ChronoField constants. To avoid a DateTimeException when number overflow occurs, the getLong(field) method is used instead of the get(field) method in accessing the epoch-second field.

Click here to view code image

out.println(inst.get(ChronoField.NANO_OF_SECOND));      // 555555555 ns
out.println(inst.get(ChronoField.MICRO_OF_SECOND));     // 555555 micros
out.println(inst.get(ChronoField.MILLI_OF_SECOND));     // 555 ms
out.println(inst.getLong(ChronoField.INSTANT_SECONDS)); // 86400 s
//out.println(inst.get(ChronoField.INSTANT_SECONDS));   // DateTimeException
//out.println(inst.get(ChronoField.HOUR_OF_DAY));       // UnsupportedTemporal-
                                                        // TypeException

The Instant class provides the toEpochMilli() method to derive the position of the instant measured in milliseconds from the epoch; that is, the second and nanosecond fields are converted to milliseconds. Converting 1 day (86400 s) and 555555555 ns results in 86400555 ms.

Click here to view code image

out.println(inst.toEpochMilli());                        // 86400555 ms

Temporal Arithmetic with Instants – Date and Time

Temporal Arithmetic with Instants

The Instant class provides plus and minus methods that return a copy of the original instant that has been incremented or decremented by a specific amount specified in terms of either seconds, milliseconds, or nanoseconds. Each amounts below is explicitly designated as a long to avoid problems if the amount does not fit into an int.

Click here to view code image

Instant event =
    Instant.EPOCH                    //            1970-01-01T00:00:00Z
           .plusSeconds(7L*24*60*60) // (+7days)   1970-01-08T00:00:00Z
           .plusSeconds(6L*60*60)    // (+6hrs)    1970-01-08T06:00:00Z
           .plusSeconds(5L*60)       // (+5mins)   1970-01-08T06:05:00Z
           .plusSeconds(4L)          // (+4s)      1970-01-08T06:05:04Z
           .plusMillis(3L*100)       // (+3ms)     1970-01-08T06:05:04.003Z
           .plusNanos(2L*1_000)      // (+2micros) 1970-01-08T06:05:04.003002Z
           .plusNanos(1L);           // (+1ns)     1970-01-08T06:05:04.003002001Z

However, it is more convenient to express the above calculation using the plus(amount, unit) method, which also allows the amount to be qualified by a unit. This is illustrated by the statement below, which is equivalent to the one above.

Click here to view code image

Instant ptInTime =
     Instant.EPOCH                          // 1970-01-01T00:00:00Z
            .plus(7L, ChronoUnit.DAYS)      // 1970-01-08T00:00:00Z
            .plus(6L, ChronoUnit.HOURS)     // 1970-01-08T06:00:00Z
            .plus(5L, ChronoUnit.MINUTES)   // 1970-01-08T06:05:00Z
            .plus(4L, ChronoUnit.SECONDS)   // 1970-01-08T06:05:04Z
            .plus(3L, ChronoUnit.MILLIS)    // 1970-01-08T06:05:04.003Z
            .plus(2L, ChronoUnit.MICROS)    // 1970-01-08T06:05:04.003002Z
            .plus(1L, ChronoUnit.NANOS);    // 1970-01-08T06:05:04.003002001Z

The code below shows the plus() method of the Instant class that takes a Duration (p. 1064) as the amount to add.

Click here to view code image

Instant start = Instant.EPOCH
                       .plus(20, ChronoUnit.MINUTES);// 1970-01-01T00:20:00Z
Duration length = Duration.ZERO.plusMinutes(90);     // PT1H30M (90 mins)
Instant end = start.plus(length);                    // 1970-01-01T01:50:00Z

The until() method calculates the amount of time between two instants in terms of the unit specified in the method.

Click here to view code image

long eventDuration1 = start.until(end, ChronoUnit.MINUTES);  // 90 minutes
long eventDuration2 = start.until(end, ChronoUnit.HOURS);    // 1 hour

As an Instant does not represent an amount of time, but a point on the timeline, it cannot be used in temporal arithmetic with other temporal objects. Although an Instant incorporates a date, it is not possible to access it in terms of year and month.

Click here to view code image

Instant plusSeconds/minusSeconds(long seconds)
Instant plusMillis/minusMillis(long millis)
Instant plusNanos/minusNanos(long nanos)

Return a copy of this instant, with the specified amount added or subtracted. Note that the argument type is long.

The methods throw a DateTimeException if the result is not a valid instant, and an ArithmeticException if numeric flow occurs during the operation.

Click here to view code image

Instant plus(long amountToAdd, TemporalUnit unit)
Instant minus(long amountToSub, TemporalUnit unit)

Return a copy of this instant with the specified amount added or subtracted, respectively, where the specified TemporalUnit qualifies the amount (p. 1044).

The following units, defined as constants by the ChronoUnit class, can be used to qualify the amount: NANOS, MICROS, MILLIS, SECONDS, MINUTES, HOURS, HALF_DAYS, and DAYS (p. 1044).

A method call can result in any one of these exceptions: DateTimeException (if the operation cannot be performed), UnsupportedTemporalTypeException (if the unit is not supported), or ArithmeticException (if numeric overflow occurs).

Click here to view code image

Instant isSupported(TemporalUnit unit)

Returns true if the specified unit is supported (p. 1044), in which case, the unit can be used in plus/minus operations on an instant. If the specified unit is not supported, the plus/minus methods that accept a unit will throw an exception.

Click here to view code image

Instant plus(TemporalAmount amountToAdd)
Instant minus(TemporalAmount amountToSubtract)

Return a copy of this instant, with the specified amount added or subtracted. The amount is typically defined as a Duration.

A method call can result in any one of these exceptions: DateTimeException (if the operation cannot be performed) or ArithmeticException (if numeric overflow occurs).

Click here to view code image

long until(Temporal endExclusive, TemporalUnit unit)

Calculates the amount of time between two temporal objects in terms of the specified TemporalUnit (p. 1044). The start and end points are this temporal object and the specified temporal argument, where the end point is excluded.

The start point is an Instant, and the end point temporal is converted to an Instant, if necessary.

The following units, defined as constants by the ChronoUnit class, can be used to indicate the unit in which the result should be returned: NANOS, MICROS, MILLIS, SECONDS, MINUTES, HOURS, HALF_DAYS, and DAYS (p. 1044).

The until() method can result in any one of these exceptions: DateTimeException (the temporal amount cannot be calculated or the end temporal cannot be converted to the appropriate temporal object), UnsupportedTemporalTypeException (the unit is not supported), or ArithmeticException (numeric overflow occurred).