Java 8 Lambda, Stream, and Date/Time API Deep Dive

Introduction

This article focuses on some of the new syntax features in JDK 1.8, specifically Lambda expressions, the Stream API, and the LocalDate date/time API.

Lambda

What is Lambda?

A Lambda expression (lambda expression) is an anonymous function. It is named after lambda calculus in mathematics, directly corresponding to lambda abstraction. It is an anonymous function, i.e., a function without a function name.

Structure of a Lambda Expression

  • A Lambda expression can have zero or more parameters.
  • The type of a parameter can be explicitly declared or inferred from context. For example, (int a) has the same effect as (a).
  • All parameters must be enclosed in parentheses, separated by commas. For example: (a, b) or (int a, int b) or (String a, int b, float c).
  • Empty parentheses represent an empty parameter set. For example: () -> 42.
  • When there is only one parameter and its type can be inferred, the parentheses can be omitted. For example: a -> a * a.
  • The body of a Lambda expression can contain zero or more statements.
  • If the body of a Lambda expression has only one statement, the curly braces {} can be omitted. The return type of the anonymous function is the same as the type of the body expression.
  • If the body of a Lambda expression contains more than one statement, the expression must be enclosed in curly braces {} (forming a code block). The return type of the anonymous function matches the return type of the code block, or is void if there is no return.

Using Lambda Expressions

Let's start with a simple example to see the effect of Lambda.

Consider iterating over a Map:

Traditional iteration:

Map<String, String> map = new HashMap<>();
map.put("a", "a");
map.put("b", "b");
map.put("c", "c");
map.put("d", "d");

System.out.println("Traditional Map iteration:");
for (String key : map.keySet()) {
    System.out.println("k=" + key + ", v=" + map.get(key));
}

Iteration using Lambda:

System.out.println("Lambda Map iteration:");
map.forEach((k, v) -> {
    System.out.println("k=" + k + ", v=" + v);
});

The same applies to List. Additionally, List can be traversed using the double colon operator:

List<String> list = new ArrayList<String>();
list.add("a");
list.add("bb");
list.add("ccc");
list.add("dddd");

System.out.println("Lambda List iteration:");
list.forEach(v -> {
    System.out.println(v);
});

System.out.println("Double colon operator iteration:");
list.forEach(System.out::println);

Output:

Traditional Map iteration:
k=a, v=a
k=b, v=b
k=c, v=c
k=d, v=d
Lambda Map iteration:
k=a, v=a
k=b, v=b
k=c, v=c
k=d, v=d
Lambda List iteration:
a
bb
ccc
dddd
Double colon operator iteration:
a
bb
ccc
dddd

Besides use in for loops, Lambda can also replace anonymous inner classes.

For example, creating a thread:

// Traditional way
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("Traditional way created!");
    }
};

// Lambda way
Runnable r2 = () -> System.out.println("Lambda way created!");

Note: In this example, the compiler automatically infers the Runnable interface based on the constructor signature Runnable r { }.

Difference between Lambda and Anonymous Classes

A key difference between anonymous classes and Lambda expressions is the use of keywords. For anonymous classes, the keyword this refers to the anonymous class instance. For Lambda expressions, this refers to the enclosing class where the Lambda is written.

Important Notes on Lambda Usage

While Lambda simplifies code, it can also reduce readability.

Stream

What is Stream?

Stream provides a high-level abstraction for Java collection operations and expressions, offering an intuitive way similar to using SQL statements to query data from a database. The Stream API can significantly boost Java programmer productivity, enabling efficient, clean, and concise code. This style treats the collection of elements to be processed as a stream, which is transmitted through a pipeline and can be processed at pipeline nodes, such as filtering, sorting, and aggregating.

Stream Characteristics:

  • Not a data structure: It has no internal storage; it only fetches data from a source (data structure, array, generator function, IO channel) using an operation pipeline. It never modifies the underlying data of the encapsulated data structure. For example, the filter operation of a Stream produces a new Stream without the filtered elements, rather than removing those elements from the source.
  • No index access: However, it can easily generate arrays or Lists.
  • Laziness: Many Stream operations are deferred until it determines how much data is actually needed. Intermediate operations are always lazy.
  • Parallelism: When a Stream is parallelized, there is no need to write multithreading code; all operations on it are automatically performed in parallel.
  • Can be infinite: Collections have a fixed size, but Streams do not have to be. Short-circuiting operations like limit(n) and findFirst() can operate on infinite Streams and complete quickly.
  • Important: All Stream operations must take lambda expressions as parameters.

Stream Operation Types:

  • Intermediate: A stream can be followed by zero or more intermediate operations. Their main purpose is to open the stream, perform some degree of data mapping/filtering, and then return a new stream for the next operation. These operations are lazy, meaning merely calling these methods does not start traversing the stream.
  • Terminal: A stream can have only one terminal operation. Once this operation is executed, the stream is "consumed" and cannot be operated on again. Thus, it must be the last operation of the stream. The execution of a Terminal operation actually starts the stream traversal and produces a result or a side effect.

Using Stream

Let's look at a simple example.

In development, we sometimes need to filter data. With traditional methods, we have to iterate and filter the data, which can be cumbersome. Using Stream makes this much more convenient.

First, filtering in the traditional way:

List<String> list = Arrays.asList("Alice", "Bob", "Charlie", "xuwujing");
System.out.println("Before filtering:" + list);
List<String> result = new ArrayList<>();
for (String str : list) {
    if (!"Bob".equals(str)) {
        result.add(str);
    }
}
System.out.println("After filtering:" + result);

Filtering using Stream:

List<String> result2 = list.stream().filter(str -> !"Bob".equals(str)).collect(Collectors.toList());
System.out.println("Stream after filtering:" + result2);

Output:

Before filtering:[Alice, Bob, Charlie, xuwujing]
After filtering:[Alice, Charlie, xuwujing]
Stream after filtering:[Alice, Charlie, xuwujing]

Isn't it concise and convenient?

Stream has many more uses beyond just filter. Let's explore some of them.

1. Creating a Stream

Stream stream = Stream.of("a", "b", "c");
String[] strArray = new String[] { "a", "b", "c" };
stream = Stream.of(strArray);
stream = Arrays.stream(strArray);
List<String> list = Arrays.asList(strArray);
stream = list.stream();

2. Converting Between Streams

Note: A Stream can only be used once. The following code reuses stream2 multiple times for brevity, which would throw an IllegalStateException: stream has already been operated upon or closed.

try {
    Stream<String> stream2 = Stream.of("a", "b", "c");
    // to Array
    String[] strArray1 = stream2.toArray(String[]::new);

    // to Collection
    List<String> list1 = stream2.collect(Collectors.toList());
    List<String> list2 = stream2.collect(Collectors.toCollection(ArrayList::new));
    Set set1 = stream2.collect(Collectors.toSet());
    Stack stack1 = stream2.collect(Collectors.toCollection(Stack::new));

    // to String
    String str = stream.collect(Collectors.joining()).toString();
} catch (Exception e) {
    e.printStackTrace();
}

3. Using map

The map method is used to map each element to a corresponding result (one-to-one).

Example 1: Convert to uppercase

List<String> list3 = Arrays.asList("zhangSan", "liSi", "wangWu");
System.out.println("Before mapping:" + list3);
List<String> list4 = list3.stream().map(String::toUpperCase).collect(Collectors.toList());
System.out.println("After mapping:" + list4);
// After mapping:[ZHANGSAN, LISI, WANGWU]

Example 2: Convert data type

List<String> list31 = Arrays.asList("1", "2", "3");
System.out.println("Before mapping:" + list31);
List<Integer> list41 = list31.stream().map(Integer::valueOf).collect(Collectors.toList());
System.out.println("After mapping:" + list41);
// [1, 2, 3]

Example 3: Get squares

List<Integer> list5 = Arrays.asList(new Integer[] { 1, 2, 3, 4, 5 });
List<Integer> list6 = list5.stream().map(n -> n * n).collect(Collectors.toList());
System.out.println("Squared values:" + list6);
// [1, 4, 9, 16, 25]

4. Using filter

The filter method is used to filter elements based on a given condition.

Example 2: Using filter with findAny to get an if/else value

List<String> list = Arrays.asList("Alice", "Bob", "Charlie", "xuwujing");
String result3 = list.stream().filter(str -> "Bob".equals(str)).findAny().orElse("Not found!");
String result4 = list.stream().filter(str -> "David".equals(str)).findAny().orElse("Not found!");

System.out.println("Stream filter 2:" + result3);
System.out.println("Stream filter 3:" + result4);
// Stream filter 2:Bob
// Stream filter 3:Not found!

Example 3: Using filter with mapToInt to calculate a sum

List<User> lists = new ArrayList<User>();
lists.add(new User(6, "Alice"));
lists.add(new User(2, "Bob"));
lists.add(new User(3, "Charlie"));
lists.add(new User(1, "Alice"));

int sum = lists.stream().filter(u -> "Alice".equals(u.getName())).mapToInt(u -> u.getId()).sum();

System.out.println("Sum:" + sum);
// 7

5. Using flatMap

The flatMap method is used to map each element to a corresponding result (one-to-many).

Example: Get words from a sentence

String worlds = "The way of the future";
List<String> list7 = new ArrayList<>();
list7.add(worlds);
List<String> list8 = list7.stream().flatMap(str -> Stream.of(str.split(" ")))
        .filter(world -> world.length() > 0).collect(Collectors.toList());
System.out.println("Words:");
list8.forEach(System.out::println);
// Words:
// The
// way
// of
// the
// future

6. Using limit

The limit method is used to obtain a stream with a specified number of elements.

Example 1: Get the first n elements

Random rd = new Random();
System.out.println("First three elements:");
rd.ints().limit(3).forEach(System.out::println);
// First three elements:
// 1167267754
// -1164558977
// 1977868798

Example 2: Using limit with skip

skip discards the first n elements.

List<User> list9 = new ArrayList<User>();
for (int i = 1; i < 4; i++) {
    User user = new User(i, "pancm" + i);
    list9.add(user);
}
System.out.println("Before truncation:");
List<String> list10 = list9.stream().map(User::getName).limit(3).skip(2).collect(Collectors.toList());
System.out.println("After truncation:" + list10);
// Before truncation:
// Name:pancm1
// Name:pancm2
// Name:pancm3
// After truncation:[pancm3]

Note: The getName method in the User entity prints the name.

7. Using sorted

The sorted method is used to sort the stream in ascending order.

Example 1: Random values and sort

Random rd2 = new Random();
System.out.println("First three sorted elements:");
rd2.ints().limit(3).sorted().forEach(System.out::println);
// First three sorted elements:
// -2043456377
// -1778595703
// 1013369565

Example 2: Optimized sorting

Tip: Sorting after limit can be more efficient.

// Normal sorting
List<User> list11 = list9.stream().sorted((u1, u2) -> u1.getName().compareTo(u2.getName())).limit(3)
        .collect(Collectors.toList());
System.out.println("Sorted data:" + list11);

// Optimized sorting
List<User> list12 = list9.stream().limit(3).sorted((u1, u2) -> u1.getName().compareTo(u2.getName()))
        .collect(Collectors.toList());
System.out.println("Optimized sorted data:" + list12);
// Sorted data:[{"id":1,"name":"pancm1"}, {"id":2,"name":"pancm2"}, {"id":3,"name":"pancm3"}]
// Optimized sorted data:[{"id":1,"name":"pancm1"}, {"id":2,"name":"pancm2"}, {"id":3,"name":"pancm3"}]

8. Using peek

peek performs an operation on each element and returns a new Stream.

Example: Double operations

System.out.println("Using peek:");
Stream.of("one", "two", "three", "four")
        .filter(e -> e.length() > 3)
        .peek(e -> System.out.println("Before transformation: " + e))
        .map(String::toUpperCase)
        .peek(e -> System.out.println("After transformation: " + e))
        .collect(Collectors.toList());

// Before transformation: three
// After transformation: THREE
// Before transformation: four
// After transformation: FOUR

9. Using parallel

parallelStream is an alternative for parallel processing of streams.

Example: Count empty strings

List<String> strings = Arrays.asList("a", "", "c", "", "e", "", " ");
long count = strings.parallelStream().filter(string -> string.isEmpty()).count();
System.out.println("Number of empty strings:" + count);

10. Using max/min/distinct

Example 1: Get max and min

List<String> list13 = Arrays.asList("zhangsan", "lisi", "wangwu", "xuwujing");
int maxLines = list13.stream().mapToInt(String::length).max().getAsInt();
int minLines = list13.stream().mapToInt(String::length).min().getAsInt();
System.out.println("Max length:" + maxLines + ", Min length:" + minLines);
// Max length:8, Min length:4

Example 2: Get distinct values

String lines = "good good study day day up";
List<String> list14 = new ArrayList<String>();
list14.add(lines);
List<String> words = list14.stream()
        .flatMap(line -> Stream.of(line.split(" ")))
        .filter(word -> word.length() > 0)
        .map(String::toLowerCase)
        .distinct()
        .sorted()
        .collect(Collectors.toList());
System.out.println("After distinct:" + words);
// After distinct:[day, good, study, up]

11. Using Match

  • allMatch: Returns true if all elements in the Stream match the predicate.
  • anyMatch: Returns true if atleast one element in the Stream matches the predicate.
  • noneMatch: Returns true if no element in the Stream matches the predicate.

Example:

boolean all = lists.stream().allMatch(u -> u.getId() > 3);
System.out.println("All greater than 3:" + all);
boolean any = lists.stream().anyMatch(u -> u.getId() > 3);
System.out.println("Any greater than 3:" + any);
boolean none = lists.stream().noneMatch(u -> u.getId() > 3);
System.out.println("None greater than 3:" + none);
// All greater than 3:false
// Any greater than 3:true
// None greater than 3:false

12. Using reduce

The reduce operation combines Stream elements.

Example 1: Concatenate strings

String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat);
System.out.println("Concatenated string:" + concat);

Example 2: Get minimum value

double minValue = Stream.of(-4.0, 1.0, 3.0, -2.0).reduce(Double.MAX_VALUE, Double::min);
System.out.println("Minimum value:" + minValue);
// Minimum value:-4.0

Example 3: Sum

// Sum without identity
int sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get();
System.out.println("Sum without identity:" + sumValue);
// Sum with identity
sumValue = Stream.of(1, 2, 3, 4).reduce(1, Integer::sum);
System.out.println("Sum with identity:" + sumValue);
// Sum without identity:10
// Sum with identity:11

Example 4: Filter and concatenate

concat = Stream.of("a", "B", "c", "D", "e", "F")
        .filter(x -> x.compareTo("Z") > 0)
        .reduce("", String::concat);
System.out.println("Filtered and concatenated:" + concat);
// Filtered and concatenated:ace

13. Using iterate

iterate is similar to reduce. It takes a seed value and a UnaryOperator (e.g., f). The seed becomes the first element of the stream, f(seed) the second, f(f(seed)) the third, and so on. When using iterate, the pipeline must have an operation like limit to restrict the Stream size.

Example: Generate an arithmetic sequence

System.out.println("Generate an arithmetic sequence starting from 2:");
Stream.iterate(2, n -> n + 2).limit(5).forEach(x -> System.out.print(x + " "));
// Generate an arithmetic sequence starting from 2:
// 2 4 6 8 10

14. Using Supplier

Custom stream generation rules can be defined by implementing the Supplier interface.

Example: Random get two user information

System.out.println("Custom stream generation:");
Stream.generate(new UserSupplier()).limit(2).forEach(u -> System.out.println(u.getId() + ", " + u.getName()));

// First run:
// Custom stream generation:
// 10, pancm7
// 11, pancm6

// Second run:
// Custom stream generation:
// 10, pancm4
// 11, pancm2

// Third run:
// Custom stream generation:
// 10, pancm4
// 11, pancm8

class UserSupplier implements Supplier<User> {
    private int index = 10;
    private Random random = new Random();

    @Override
    public User get() {
        return new User(index++, "pancm" + random.nextInt(10));
    }
}

15. Using groupingBy/partitioningBy

  • groupingBy: Groups elements.
  • partitioningBy: Partitions elements.

Example 1: Group by

System.out.println("Group by id:");
Map<Integer, List<User>> personGroups = Stream.generate(new UserSupplier2()).limit(5)
        .collect(Collectors.groupingBy(User::getId));
Iterator it = personGroups.entrySet().iterator();
while (it.hasNext()) {
    Map.Entry<Integer, List<User>> persons = (Map.Entry) it.next();
    System.out.println("id " + persons.getKey() + " = " + persons.getValue());
}

// Group by id:
// id 10 = [{"id":10,"name":"pancm1"}]
// id 11 = [{"id":11,"name":"pancm3"}, {"id":11,"name":"pancm6"}, {"id":11,"name":"pancm4"}, {"id":11,"name":"pancm7"}]

class UserSupplier2 implements Supplier<User> {
    private int index = 10;
    private Random random = new Random();

    @Override
    public User get() {
        return new User(index % 2 == 0 ? index++ : index, "pancm" + random.nextInt(10));
    }
}

Example 2: Partition by

System.out.println("Partition by age:");
Map<Boolean, List<User>> children = Stream.generate(new UserSupplier3()).limit(5)
        .collect(Collectors.partitioningBy(p -> p.getId() < 18));

System.out.println("Children: " + children.get(true));
System.out.println("Adults: " + children.get(false));

// Partition by age:
// Children: [{"id":16,"name":"pancm7"}, {"id":17,"name":"pancm2"}]
// Adults: [{"id":18,"name":"pancm4"}, {"id":19,"name":"pancm9"}, {"id":20,"name":"pancm6"}]

class UserSupplier3 implements Supplier<User> {
    private int index = 16;
    private Random random = new Random();

    @Override
    public User get() {
        return new User(index++, "pancm" + random.nextInt(10));
    }
}

16. Using summaryStatistics

IntSummaryStatistics is a state object for collecting statistics such as count, min, max, sum, and average.

Example:

List<Integer> numbers = Arrays.asList(1, 5, 7, 3, 9);
IntSummaryStatistics stats = numbers.stream().mapToInt((x) -> x).summaryStatistics();

System.out.println("Max: " + stats.getMax());
System.out.println("Min: " + stats.getMin());
System.out.println("Sum: " + stats.getSum());
System.out.println("Average: " + stats.getAverage());

// Max: 9
// Min: 1
// Sum: 25
// Average: 5.0

This concludes the introduction to Stream. JDK 1.8's Stream API has many more features, and you are encouraged to explore the official documentation.

LocalDateTime

Introduction

JDK 1.8 not only introduced lambda expressions and Stream but also a brand new date/time API. Before JDK 1.8, Java's handling of dates, calendars, and time was widely criticized. java.util.Date was mutable, and SimpleDateFormat was not thread-safe, severely limiting its use. Hence, the java.time package was introduced, where all classes are immutable and thread-safe.

Key Classes

  • Instant: Represents an instantaneous point on the time-line.
  • LocalDate: A date without time-of-day, format yyyy-MM-dd.
  • LocalTime: A time without date, format HH:mm:ss.SSS.
  • LocalDateTime: Combines date and time, but without time-zone information.
  • ZonedDateTime: A date-time with a time-zone, including offset from UTC/Greenwich.

Usage

1. Getting the Current Date/Time

Use the static factory method now().

LocalDate nowDate = LocalDate.now();
LocalDateTime nowDateTime = LocalDateTime.now();
System.out.println("Current date:" + nowDate);
System.out.println("Current date/time:" + nowDateTime);
// Current date:2018-12-19
// Current date/time:2018-12-19T15:24:35.822

2. Getting Year, Month, Day, Hour, Minute, Second

Use get methods after obtaining the date/time object.

LocalDateTime ldt = LocalDateTime.now();
System.out.println("Year:" + ldt.getYear());   // 2018
System.out.println("Day of year:" + ldt.getDayOfYear()); // 353
System.out.println("Month:" + ldt.getMonthValue());
System.out.println("Hour:" + ldt.getHour());
System.out.println("Minute:" + ldt.getMinute());
System.out.println("Current time:" + ldt.toString());
// Year:2018
// Day of year:353
// Month:12
// Hour:15
// Minute:24
// Current time:2018-12-19T15:24:35.833

3. Formatting Time

Use DateTimeFormatter.

LocalDateTime ldt = LocalDateTime.now();
System.out.println("Formatted time: " + ldt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")));
// Formatted time:2018-12-19 15:37:47.119

4. Adding/Subtracting Time

Add or subtract years, months, days, hours, minutes, seconds.

LocalDateTime ldt = LocalDateTime.now();
System.out.println("After 5 days:" + ldt.plusDays(5));
System.out.println("Before 5 days formatted:" + ldt.minusDays(5).format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
System.out.println("Before 1 month:" + ldt.minusMonths(1).format(DateTimeFormatter.ofPattern("yyyyMM")));
System.out.println("After 1 month:" + ldt.plusMonths(1));
System.out.println("With year 2099:" + ldt.withYear(2099));
// After 5 days:2018-12-24T15:50:37.508
// Before 5 days formatted:2018-12-14
// Before 1 month:201712
// After 1 month:2018-02-04T09:19:29.499
// With year 2099:2099-12-19T15:50:37.508

5. Creating a Specific Date/Time

Use of method.

LocalDate ld3 = LocalDate.of(2017, Month.NOVEMBER, 17);
LocalDate ld4 = LocalDate.of(2018, 02, 11);

6. Comparing Date/Time Differences

Compare differences in years, months, days, hours, minutes, seconds.

Example 1: Difference in years, months, days

LocalDate ld = LocalDate.parse("2017-11-17");
LocalDate ld2 = LocalDate.parse("2018-01-05");
Period p = Period.between(ld, ld2);
System.out.println("Years difference: " + p.getYears() + " Months difference: " + p.getMonths() + " Days difference: " + p.getDays());
// Years difference: 0 Months difference: 1 Days difference: 19

Note: Here, months are not a full year, and days are not a full month. The actual difference is 1 month and 19 days, i.e., 49 days.

Example 2: Total difference using ChronoUnit

ChronoUnit is a standard set of date period units.

LocalDate startDate = LocalDate.of(2017, 11, 17);
LocalDate endDate = LocalDate.of(2018, 01, 05);
System.out.println("Months difference:" + ChronoUnit.MONTHS.between(startDate, endDate));
System.out.println("Days difference:" + ChronoUnit.DAYS.between(startDate, endDate));
// Months difference:1
// Days difference:49

Note: ChronoUnit can also calculate differences in hours, minutes, and seconds.

Example 3: Precise time difference using Duration

Duration models time in seconds and nanoseconds.

Instant inst1 = Instant.now();
System.out.println("Current timestamp:" + inst1);
Instant inst2 = inst1.plus(Duration.ofSeconds(10));
System.out.println("After addition:" + inst2);
System.out.println("Difference in milliseconds:" + Duration.between(inst1, inst2).toMillis());
System.out.println("Difference in seconds:" + Duration.between(inst1, inst2).getSeconds());
// Current timestamp:2018-12-19T08:14:21.675Z
// After addition:2018-12-19T08:14:31.675Z
// Difference in milliseconds:10000
// Difference in seconds:10

Example 4: Comparing time

LocalDateTime ldt4 = LocalDateTime.now();
LocalDateTime ldt5 = ldt4.plusMinutes(10);
System.out.println("Is current time after?:" + ldt4.isAfter(ldt5));
System.out.println("Is current time before?:" + ldt4.isBefore(ldt5));
// false
// true

7. Time Zone Calculations

Get the time in other time zones.

Example 1: Using Clock class

Clock provides access to the current instant, date, and time using a time-zone.

Clock clock = Clock.systemUTC();
System.out.println("Current timestamp:" + clock.millis());
Clock clock2 = Clock.system(ZoneId.of("Asia/Shanghai"));
System.out.println("Timestamp in Asia/Shanghai:" + clock2.millis());
Clock clock3 = Clock.system(ZoneId.of("America/New_York"));
System.out.println("Timestamp in America/New_York:" + clock3.millis());
// Current timestamp:1545209277657
// Timestamp in Asia/Shanghai:1545209277657
// Timestamp in America/New_York:1545209277658

Example 2: Using ZonedDateTime and ZoneId

ZoneId zoneId = ZoneId.of("America/New_York");
ZonedDateTime dateTime = ZonedDateTime.now(zoneId);
System.out.println("Current time in New York:" + dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")));
System.out.println("Current time in New York with zone:" + dateTime);
// Current time in New York:2018-12-19 03:52:22.494
// Current time in New York with zone:2018-12-19T03:52:22.494-05:00[America/New_York]

Summary of Java 8 Date/Time API:

  • ZoneId is used to get time zones.
  • LocalDate and LocalTime classes are provided.
  • All date/time classes in Java 8 are immutable and thread-safe, unlike the mutable and non-thread-safe java.util.Date and SimpleDateFormat.
  • The main package is java.time, containing classes for date, time, and intervals. Two sub-packages are java.time.format for formatting and java.time.temporal for lower-level operations.
  • A time zone represents a region where a standard time is used. Each zone has an ID, typically in the format region/city (e.g., Asia/Tokyo), along with an offset from Greenwich/UTC. For example, Tokyo's offset is +09:00.
  • OffsetDateTime combines LocalDateTime and ZoneOffset to represent a full date and time with an offset from Greenwich/UTC.
  • DateTimeFormatter is used for formatting and parsing. Unlike SimpleDateFormat, it is immutable and thread-safe. It provides many built-in formatters and allows custom formats. The parse() method parses a string into a date, throwing DateTimeParseException on error. The format() method formats a date, throwing DateTimeException on error.
  • Note the subtle difference between date formats "MMM d yyyy" and "MMM dd yyyy". The first can parse both "Jan 2 2014" and "Jan 14 2014", while the second throws an exception for "Jan 2 2014" because it requires two digits for the day. To fix this, single-digit days must be zero-padded (e.g., "Jan 02 2014").

References

Tags: java JDK 1.8 lambda Stream API Date/Time API

Posted on Thu, 29 Oct 2026 16:29:18 +0000 by peterj