Overview
This post discusses the new language features of Java 8. Java 8’s release is the most awaited and is a major feature release of Java programming language.
These new features include functional interfaces, interface default method, lambda expressions, method references, Optional, and Stream API. We will also talk about some of built-in functions that implement Function Interface and the New Date and Time API.
Functional Interface
Functional Interface is an interface with single abstract method (SAM). Static or the new default method is not counted. To indicate that an interface is a Functional Interface, annotation @FunctionalInterface
on class level is needed. While an interface with SAM could still function as Functional Interface (qualified as lambda expression) even if it does not have such annotation (example is AWT’s ActionListener
), it is recommended that it be annotated.
Some old built-in interfaces with SAM, such as Comparator
and Runnable
, have been annotated with @FunctionalInterface
and can be used as lambda expressions.
Example
@FunctionalInterface
public interface Calculator {
long calculate(long x, long y);
}
We can implement above interface in two ways prior to Java 8.
-
Anonymous class
Calculator division = new Calculator() { @Override public long calculate(long x, long y) { return x / y; } }; long quotient = division.calculate(10, 2);
-
Concrete class
class Division implements Calculator { @Override public long calculate(long x, long y) { return x / y; } }
Calculator division = new Division() ; long quotient = division.calculate(10, 2);
With Java 8, there is a third way to to implement an interface and that is by using Lambda Expressions.
Lambda Expressions
Lambda expressions encapsulate a single unit of behavior and pass it to other code. To be able to create lambda expression, you need first a Funtional Interface. This is the reason why a Functional Interface has a single abstract method. Instead of an anonymous class, you can use lambda expression which is a concise alternative and shorthand replacement for it.
Syntax
parameter -> expression or statement body
Example
(Integer x) -> { return x; };
Characteristics of Lambda Expression
- Optional type declaration
(x) -> { return x; };
- Optional parentheses
x -> { return x; };
Parentheses are required if there are multiple parameters. Parameters are separated by comma (,
).
(x, y) -> { return x + y; };
- Optional curly braces (
{}
) and optionalreturn
keyword
x -> x;
Curly braces are required if body has multiple statements. return
keyword is required for a function with return value if body has multiple statements. No return
keyword is required if function does not return a value as in regular method.
x -> {
System.out.println("Hello world!");
return x;
};
x -> {
System.out.println("Hello world!");
System.out.println("Welcome to Java 8 Programming!");
};
Remember our Calculator
interface? Here is how to implement it using lambda expression:
Calculator multiplication = (a, b) -> a * b; // or (a, b) -> Math.multiplyExact(a, b);
Calculator division = (a, b) -> a / b;
Calculator addition = (a, b) -> a + b; // or (a, b) -> Math.addExact(a, b);
Calculator subtraction = (a, b) -> a - b; // or (a, b) -> Math.subtractExact(a, b);
long product = multiplication.calculate(5, 5);
long quotient = division.calculate(10, 2);
long sum = addition.calculate(5, 5);
long difference = subtraction.calculate(10, 4);
Built-in Functions
Java 8 ships 43 built-in functions under java.util.function
package. Thirty eight (38) of them are specialization for primitive and other functions.
The following are the basic functions:
Function
Consumer
Supplier
Predicate
Function
Function represents a function that accepts one argument and produces a result. The type parameter T
represents the type of the input to the function while the second one R represents the type of result. The functonal method of this functional interface is apply(Object)
.
@FunctionalInterface
public interface Function<T,R>
Examples
Below is an example of Function that accepts a String argument and returns a String.
Function<String, String> f1 = a -> a;
String message = f1.apply("Welcome to Java 8!");
This one accepts a String argument and returns an integer.
Function<String, Integer> f2 = a -> a.length();
int textLength = f2.apply("Welcome to Java 8!");
Consumer
Consumer represents an operation that accepts a single input argument and returns no result. The type parameter T
represents the type of the input to the operation. The functonal method of this functional interface is accept(Object)
.
@FunctionalInterface
public interface Consumer<T>
Examples
Both accept String input argument and returns no value.
Consumer<String> c1 = a -> System.out.println(a);
c1.accept("Welcome to Java 8!");
Consumer<String> c2 = a -> {
String message = "Welcome to " + a + "!";
System.out.println(message);
};
c2.accept("Java 8");
Supplier
Supplier Represents a supplier of results. The type parameter T
represents the type of results supplied by the supplier. This is a functional interface whose functional method is get()
.
@FunctionalInterface
public interface Supplier<T>
Examples
This example returns a value of String type.
Supplier<String> stringSupplier = () -> "Welcome to Java 8!";
String message = stringSupplier.get();
And this one returns a value of User type.
Supplier<User> userSupplier = () -> new User();
User user = userSupplier.get();
Predicate
Predicate represents a predicate (boolean-valued function) of one argument. The type parameter T
represents the type of the input to the predicate. This is a functional interface whose functional method is test(Object)
.
@FunctionalInterface
public interface Predicate<T>
Examples
To check if argument is equal to “YES” regardless of case:
Predicate<String> answerPredicate = a -> "YES".equalsIgnoreCase(a);
boolean isYes = answerPredicate.test("Yes");
To check if person is of legal age (18 and above):
public static Predicate<Integer> isLegalAge() {
return age -> age >= 18;
}
boolean legalAge = isLegalAge().test(18);
Built-in Functions - Specialization
Other built-in functions are specializations for primitive types and for basic or another specialization functions.
To check all other built-in functions, please visit this Javadoc.
Existing Interface - Comparator
Comparator
is an existing interface in Java since version 1.2. This interface has a single abstract method, compare(T o1, T o2)
, and, therefore, can be used as lambda expression similar to functions added to Java 8.
Prior to Java 8, we would use Comparator in sorting a collection the following way:
List<String> choices = Arrays.asList("c", "a", "d", "b", "e");
Collections.sort(choices, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
});
With Java 8:
Comparator<String> comparator = (a, b) -> a.compareTo(b);
Collections.sort(choices, comparator);
Collections.sort(choices, (String a, String b) -> {
return a.compareTo(b);
});
Collections.sort(choices, (String a, String b) -> a.compareTo(b));
Collections.sort(choices, (a, b) -> a.compareTo(b));
Default Methods
Default methods enable you to add new functionality to the interfaces of your libraries and ensure binary compatibility with code written for older versions of those interfaces. They are interface methods that have an implementation, similar to static methods, and the default
keyword at the beginning of the method signature.
Syntax
default type methodName(type parameter) {
body
}
For example, default method forEach()
was added to Iterable
interface. This method takes a Consumer
argument and performs action for each element (similar to enhanced for-each
construct). List
interface which extends Iterable
can now be used with forEach()
method:
List<String> days = Arrays.asList("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday");
days.forEach(day -> System.out.println(day));
Example
public interface Calculator {
double calculate(double number);
default double sqrt(double number) {
return Math.sqrt(number);
}
}
Calculator sc = x -> x * x;
double square = sc.calculate(10.0); // 100.0
double sqrt = sc.sqrt(square); // 10.0
Method References
Java 8 enables you to pass references of methods or constructors via the :: keyword. Method references are compact, easy-to-read lambda expressions for methods that already have a name. They are preferred over lambda expression if the expression does nothing but calls an existing method.
Syntax
Reference::methodName
Types of Method References
Type | Example |
---|---|
Reference to a static method | ContainingClass::staticMethodName |
Reference to an instance method of a particular object | containingObject::instanceMethodName |
Reference to an instance method of an arbitrary object of a particular type | ContainingType::methodName |
Reference to a constructor | ClassName::new |
Example
Reference to a static method
- Example 1
class Person {
private LocalDate birthday;
public Person() {}
public Person(LocalDate birthday) {
this.birthday = birthday;
}
// setters, getters
public static int compareByAge(Person a, Person b) {
return a.birthday.compareTo(b.birthday);
}
}
Person p1 = new Person(LocalDate.of(2000, 6, 21));
Person p2 = new Person(LocalDate.of(2000, 6, 15));
Person p3 = new Person(LocalDate.of(1998, 3, 28));
Person[] persons = {p1, p2, p3};
Lambda Expression
Arrays.sort(persons, (p1, p2) -> Person.compareByAge(p1, p2));
Method Reference
Arrays.sort(persons, Person::compareByAge);
- Example 2
@FunctionalInterface
public interface StringToIntConverter {
int convert(String string);
}
Lambda Expression
StringToIntConverter converter = s -> Integer.valueOf(s);
int i = converter.convert("10");
Method Reference
StringToIntConverter converter = Integer::valueOf;
int i = converter.convert("10");
Reference to an instance method of a particular object
- Example 1
public class Something {
public int getYear(LocalDate localDate) {
return localDate.getYear();
}
}
Lambda Expression
Something something = new Something();
Function<LocalDate, Integer> f = d -> something.getYear(d);
int year = f.apply(p1.getBirthday());
Method Reference
Something something = new Something();
Function<LocalDate, Integer> f = something::getYear;
int year = f.apply(p1.getBirthday());
- Example 2
public interface PersonDao {
Person findById(int id);
}
Lambda Expression
PersonDao personDao = new PersonDaoImpl();
Function<Integer, Person> f = id -> personDao.findById(id);
Person person = f.apply(1);
Method Reference
PersonDao personDao = new PersonDaoImpl();
Function<Integer, Person > f = personDao::findById;
Person person = f.apply(1);
Reference to an instance method of an arbitrary object of a particular type
- Example 1
Lambda Expression
Function<String, String> f = s -> s.toUpperCase();
String httpGet = f.apply("get");
Method Reference
Function<String, String> f = String::toUpperCase;
String httpGet = f.apply("get");
- Example 2
Lambda Expression
Person person = ...
Function<Person, LocalDate> f = p -> p.getBirthday();
LocalDate birthday = f.apply(person);
Method Reference
Person person = ...
Function<Person, LocalDate> f = Person::getBirthday;
LocalDate birthday = f.apply(person);
Reference to a constructor
- Example 1
Lambda Expression
Function<LocalDate, Person> f = d -> new Person(d);
LocalDate birthday = LocalDate.of(2000, 10, 12);
Person person = f.apply(birthday);
Method Reference
Function<LocalDate, Person> f = Person::new;
LocalDate birthday = LocalDate.of(2000, 10, 12);
Person person = f.apply(birthday);
- Example 2
Lambda Expression
Supplier<Person> s = () -> new Person();
Person person = s.get();
Method Reference
Supplier<Person> s = Person::new;
Person person = s.get();
When to Use Nested Classes, Local Classes, Anonymous Classes, and Lambda Expressions
-
Local class - Use it if you need to create more than one instance of a class, access its constructor, or introduce a new, named type (because, for example, you need to invoke additional methods later).
-
Anonymous class - Use it if you need to declare fields or additional methods.
-
Lambda expression or Method reference
- Use it if you are encapsulating a single unit of behavior that you want to pass to other code. For example, you would use a lambda expression if you want a certain action performed on each element of a collection, when a process is completed, or when a process encounters an error.
- Use it if you need a simple instance of a functional interface and none of the preceding criteria apply (for example, you do not need a constructor, a named type, fields, or additional methods).
-
Nested class
- Use it if your requirements are similar to those of a local class, you want to make the type more widely available, and you don’t require access to local variables or method parameters.
- Use a non-static nested class (or inner class) if you require access to an enclosing instance’s non-public fields and methods. Use a static nested class if you don’t require this access.
Optional
null
reference is the source of many problems because it is often used to denote the absence of a value- Java SE 8 introduces a new class called
java.util.Optional
that can alleviate some of these problems. - Thus,
Optional
object is a container object which may or may not contain a non-null value. - If a value is present,
isPresent()
will return true andget()
will return the value.
How to create Optional objects?
-
Empty Optional
Optional<String> msgOptional = Optional.empty();
-
Optional with non-null value
String message = “Welcome to Java 8!”; Optional<String> msgOptional = Optional.of(message);
If
message
werenull
, aNullPointerException
would immediately be thrown, rather than getting a latent error once you try to access properties of themessage
String object. -
Optional object that may hold a null value
String message = null; Optional<String> msgOptional = Optional.ofNullable(message);
If message
were null
, the resulting Optional
object would be empty (Optional.empty()
) and would not throw NullPointerException
.
Check Value Presence and Do Action
-
isPresent()
-
Returns
true
if the wrapped value is notnull
Optional<String> msgOptional = …; if (msgOptional.isPresent()) { System.out.println(msgOptional.get()); }
Before
Optional
, we would do:String message = …; if (message != null) { System.out.println(message); }
-
-
ifPresent()
-
Allows to run some codes if it returns
true
; takes aConsumer
function which takes the object being checked as an argument.Optional<String> msgOptional = …; msgOptional.ifPresent(m -> System.out.println(m));
Or
msgOptional.ifPresent(System.out::println);
-
-
orElse()
-
Retrieves value wrapped inside an
Optional
object if it is present and its argument otherwise.Optional<String> msgOptional = …; String message = msgOptional.orElse(“Welcome!”);
Before
Optional
, we would do:String message = …; String msg = message != null ? Message : “Welcome!”;
-
-
orElseGet()
-
Similar to
orElse()
, it retrieves value wrapped inside anOptional
object if it is present and its argument otherwise, however, it takes its default value from aSupplier
which is invoked and returns the value of invocation.Optional<String> msgOptional = …; String message = msgOptional.orElseGet(() -> “Welcome!”);
-
-
orElseThrow()
-
Similar to
orElse()
andorElseGet()
, it retrieves value wrapped inside anOptional
object if it is present and its argument otherwise. Instead of returning a default value when wrapped value is not present, it throws an exception.Optional<String> messageOptional = …; String message = messageOptional.orElseThrow(() -> new IllegalArgumentException());
Or
Optional<String> messageOptional = …; String message = messageOptional.orElseThrow(IllegalArgumentException::new);
-
-
get()
-
Can only return value if it is present, otherwise, it returns
NoSuchElementException
.Optional<String> messageOptional = …; String message = messageOptional.get();
Make sure to be defensive against possible
NoSuchElementException
when using this method:String message = messageOptional.isPresent() ? messageOptional.get() : “Welcome!”;
-
-
filter()
-
If a value is present, and the value matches the given predicate, return an
Optional
describing the value, otherwise return an emptyOptional
.Example
Optional<String> answerOptional = Optional.of(“YES”) .filter(b -> b.equalsIgnorecase(“yes”));
-
If a value is present, and the value matches the given predicate, return an
Optional
describing the value, otherwise return an emptyOptional
.Example
LocalDate birthday = LocalDate.of(2000, 7, 23); Optional<LocalDate> legalAgeOptional1 = Optional.of(birthday) .filter(this::isLegalAge);
public boolean isLegalAge(LocalDate birthday) { LocalDate currentDate = LocalDate.now(); // 2019-5-24 Period interval = Period.between(birthday, currentDate); return interval.getYears() > 17; }
Above code will return
Optional
with a LocalDate value since filter meets the predicate.
-
-
map()
-
Transforms value - if a value is present, apply the provided mapping function to it, and if the result is non-null, return an
Optional
describing the result. Otherwise return an emptyOptional
.Example 1
List<String> frameworks = Arrays.asList("Spring", “Spring Fu”, "Quarkus", "Micronaut", "Play", "Struts", "Spark", "Summer Fun"); Optional<List<String>> listOptional = Optional.of(frameworks); int size = listOptional .map(List::size) // Lambda expression .map(list -> list.size()) .orElse(0);
Above code will return an integer value since mapping function is to get the size of
List
.Example 2
class Person { public Optional<String> getName() { return Optional.of("Digong"); } }
Person person = new Person(); Optional<Person> personOptional = Optional.of(person); Optional<Optional<String>> nameOptionalWrapper = personOptional.map(Person::getName); Optional<String> nameOptional = nameOptionalWrapper.orElse(Optional.empty()); String name = nameOptional.orElse("");
With
.map()
, we still need to getOptional
that wraps anotherOptional
(line 3). Then,.orElse()
, line 4, gets theOptional
wrapped inOptional
. Finally, the last.orElse()
extracts the name value.
-
-
flatMap()
-
If a value is present, apply the provided
Optional
-bearing mapping function to it, return that result, otherwise return an emptyOptional
. This method is similar tomap()
, but the provided mapper is one whose result is already anOptional
, and if invoked,flatMap()
does not wrap it with an additionalOptional
.Person person = new Person(); Optional<Person> personOptional = Optional.of(person); Optional<String> nameOptional = personOptional.flatMap(Person::getName); // Lambda expression: personOptional.flatMap(p -> p.getName()); String name = nameOptional.orElse("");
.flatMap()
already returns theOptional
wrapped in anotherOptional
. We have saved a single line.
-
Stream API
- A sequence of elements supporting sequential and parallel aggregate operations.
- Collections in Java 8 are extended so you can simply create streams either by calling
Collection.stream()
orCollection.parallelStream()
Stream
operations are either intermediate or terminal- Intermediate operations return the
Stream
itself so you can chain multiple method calls in a rowfilter
sorted
map
- Terminal operations return a result of a certain type
forEach
allMatch
findFirst
count
reduce
collect
- Intermediate operations return the
Stream - filter()
Returns a stream consisting of the elements of this stream that match the given predicate.
Example:
List<String> frameworks = new ArrayList<>();
frameworks.add("Spring");
frameworks.add("Spring Fu");
frameworks.add("Quarkus");
frameworks.add("Micronaut");
frameworks.add("Play");
frameworks.add("Struts");
frameworks.add("Spark");
frameworks.add("Summer Fun");
List<String> frameworks = frameworks();
List<String> frameworksStartWithS = frameworks.stream()
.filter(framework -> framework.startsWith("S"))
.collect(Collectors.toList());
Stream - sorted()
Returns a stream consisting of the elements of this stream, sorted according to natural order.
Example
List<String> frameworks = frameworks();
List<String> frameworksSorted = frameworks.stream()
.sorted()
.collect(Collectors.toList());
// reversed
List<String> frameworksReversed = frameworks.stream()
.sorted(Collections.reverseOrder())
.collect(Collectors.toList());
Stream - map()
Returns a stream consisting of the results of applying the given function to the elements of this stream.
Example
List<String> frameworks = frameworks();
Lambda Expression
List<String> frameworksToUpperCase = frameworks.stream()
.map(framework -> framework.toUpperCase())
.collect(Collectors.toList());
Method Reference
List<String> frameworksToUpperCase = frameworks.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
Stream - forEach()
Returns an iterator over elements.
Example
List<String> frameworks = frameworks();
Lambda Expression
frameworks.forEach(framework -> System.out.println(framework));
Method Reference
frameworks.forEach(System.out::println);
Stream - allMatch()
Returns whether all elements of this stream match the provided predicate.
Example
List<String> frameworks = frameworks();
boolean allMatched = frameworks.stream()
.allMatch(framework -> framework.split(" ").length > 1);
Stream - findFirst()
Returns whether all elements of this stream match the provided predicate.
Example
List<String> frameworks = frameworks();
Optional<String> frameworkOptional = frameworks.stream()
.filter(framework -> framework.contains("Fund"))
.findFirst();
Stream - count()
Returns the count of elements in this stream.
Example
List<String> frameworks = frameworks();
long countAll = frameworks.stream()
.count();
long countStartsWithS = frameworks.stream()
.filter(framework -> framework.startsWith("S"))
.count();
Stream - reduce()
Performs a reduction on the elements of this stream, using an associative accumulation function, and returns an Optional describing the reduced value, if any.
Example
Integer[] grades = {90, 85, 92, 98, 88, 93};
Optional<Integer> totalOptional = Stream.of(grades)
.reduce((g1, g2) -> g1 + g2);
totalOptional.ifPresent(System.out::println);
Stream - collect()
Performs a mutable reduction operation on the elements of this stream using a Collector. A Collector encapsulates the functions used as arguments to collect(Supplier, BiConsumer, BiConsumer), allowing for reuse of collection strategies and composition of collect operations such as multiple-level grouping or partitioning.
Example 1
Integer[] grades = {90, 85, 92, 98, 88, 93};
List<Integer> gradeList = Stream.of(grades)
.filter(grade -> grade > 89)
.collect(Collectors.toList());
Example 2
List<User> users = Arrays.asList(
new User("John", "ADMIN"),
new User("Luke", "USER"),
new User("Matthew", "USER"),
new User("Mark", "USER"),
new User("Paul", "ADMIN"));
Map<String, User> userMap = users.stream()
.collect(Collectors.toMap(User::getRole, user -> user));
New Date and Time API
- The Date-Time APIs, introduced in JDK 8, are a set of packages that model the most important aspects of date and time
- The core classes in the java.time package use the calendar system defined in ISO-8601 (based on the Gregorian calendar system) as the default calendar.
java.time
- Classes for date, time, date and time combined, time zones, instants, duration, and clocks.java.time.chrono
- API for representing calendar systems other than ISO-8601. Several predefined chronologies are provided and you can also define your own chronology.java.time.format
- Classes for formatting and parsing dates and time.java.time.temporal
- Extended API, primarily for framework and library writers, allowing interoperations between the date and time classes, querying, and adjustment. Fields and units are defined in this package.java.time.zone
- Classes that support time zones, offsets from time zones, and time zone rules. on the Gregorian calendar system) as the default calendar.
- Old Date API
- Existing classes aren’t thread-safe, leading to potential concurrency issues for users—not something the average developer would expect to deal with when writing date-handling code.
- Some of the date and time classes also exhibit quite poor API design. For example, years in
java.util.Date
start at 1900, months start at 1, and days start at 0—not very intuitive. - These issues, and several others, have led to the popularity of third-party date and time libraries, such as Joda-Time.
The project, Date and Time API, has been led jointly by the author of Joda-Time (Stephen Colebourne) and Oracle, under JSR 310, and appears in the new Java SE 8 package java.time
.
New Date and Time API - Clock
Clock
- A clock providing access to the current instant, date and time using a time-zone.
Example
Clock clock = Clock.systemDefaultZone();
long millis = clock.millis();
Instant instant = clock.instant();
Date legacyDate = Date.from(instant);
New Date and Time API - Timezone
ZoneId
- A time-zone ID, such as
Asia/Manila
. AZoneId
is used to identify the rules used to convert between, anInstant
and aLocalDateTime
.
Example
Set<ZoneId> availableZoneIds = ZoneId.getAvailableZoneIds();
ZoneId zone1 = ZoneId.of("Asia/Manila");
ZoneId zone2 = ZoneId.of("Europe/Paris");
System.out.println(zone1.getRules()); // ZoneRules[currentStandardOffset=+08:00]
System.out.println(zone2.getRules()); // ZoneRules[currentStandardOffset=+01:00]
New Date and Time API - LocalTime
LocalTime
- A time without a time-zone in the ISO-8601 calendar system, such as
08:24:15
.
Example
ZoneId zone1 = ZoneId.of("Asia/Manila");
ZoneId zone2 = ZoneId.of("Europe/Paris");
LocalTime now1 = LocalTime.now(zone1);
LocalTime now2 = LocalTime.now(zone2);
System.out.println(now1.isBefore(now2)); // false
long hoursBetween = ChronoUnit.HOURS.between(now1, now2);
long minutesBetween = ChronoUnit.MINUTES.between(now1, now2);
System.out.println(hoursBetween); // -6
System.out.println(minutesBetween); // -360
New Date and Time API - LocalDate
LocalDate
- A date without a time-zone in the ISO-8601 calendar system, such as
2019-05-24
.
Example
LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
LocalDate yesterday = tomorrow.minusDays(2);
LocalDate independenceDay = LocalDate.of(2019, Month.MAY, 24);
DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();
System.out.println(dayOfWeek); // FRIDAY
New Date and Time API - LocalDateTime
LocalDateTime
- A date-time without a time-zone in the ISO-8601 calendar system, such as
2019-05-24T08:24:15
.
Example
LocalDateTime ldt = LocalDateTime.of(2019, Month.MAY, 24, 14, 55, 59);
DayOfWeek dayOfWeek = ldt.getDayOfWeek();
System.out.println(dayOfWeek); // FRIDAY
Month month = ldt.getMonth();
System.out.println(month); // MAY
long minuteOfDay = ldt.getLong(ChronoField.MINUTE_OF_DAY);
System.out.println(minuteOfDay); // 895
References
- https://docs.oracle.com/javase/8/docs/technotes/guides/language/enhancements.html#javase8
- https://www.tutorialspoint.com/java8/index.htm
- https://winterbe.com/posts/2014/03/16/java-8-tutorial/
- https://www.oracle.com/technetwork/articles/java/java8-optional-2175753.html
- https://www.baeldung.com/java-optional
- https://www.baeldung.com/java-8-streams
- https://docs.oracle.com/javase/8/docs/technotes/guides/datetime/index.html