Unveiling the Pivotal Features from Java 8 to Java 20 – A Comprehensive Exploration
Java, as one of the most popular and widely used programming languages, has gone through a remarkable journey of evolution. Since Java 8, the language has adopted a six-month release cadence, ensuring that new features and improvements are delivered more frequently to developers. In addition to these regular releases, Java also offers Long-Term Support (LTS) versions, which are backed by Oracle JDK and maintained for a longer period, typically 2-4 years. As of May 2023, Java 20 is the latest version, Java 17 is the current LTS version, and Java 21, planned for September 2023, is the next LTS release.
We will focus on the main features introduced in Java from version 8 to 20. We will not discuss features that were added and later removed, nor will we cover every single improvement or change. Instead, we will delve into key features accompanied by practical examples to better understand their application and significance in enhancing developer productivity and enriching the overall programming experience.
1. Java 8 (March 2014) - Lambda Expressions and Stream API
Prior to Java 8, the language experienced a substantial gap in development, with several years passing between the release of Java 7 and Java 8. This extended period resulted in a multitude of new features and improvements being introduced in Java 8, marking a significant leap in the evolution of the language. Many of the features added in Java 8 have since become integral to modern Java development practices, making it a milestone release that continues to have a lasting impact. In this overview, we will explore the main features that were introduced in Java 8 and the reasons they have been so influential in shaping the future of the language.
- Lambda Expressions
Lambda expressions were introduced to simplify the use of functional programming constructs, primarily when working with functional interfaces.
A functional interface is an interface with a single abstract method (SAM). Lambda expressions provide a concise way to create anonymous implementations of these interfaces.
The syntax for a lambda expression is:
(parameters) -> expression
or
(parameters) -> expression
An exemple using the Comparator functional interface to sort a list of integers:
Without lambda expression (using anonymous inner class):
List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9);
Collections.sort(numbers, new Comparator<Integer>() {
@Override
public int compare(Integer a, Integer b) {
return a - b;
}
});
With lambda expression:
List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9);
Collections.sort(numbers, (a, b) -> a - b);
- Stream API
The Stream API, introduced in Java 8, is a powerful addition to the language that allows you to perform functional-style operations on collections of data, such as filtering, mapping, and reducing. The API is designed to work seamlessly with lambda expressions and method references, enabling more concise and expressive code.
A stream is a sequence of elements that can be processed in parallel or sequentially. It provides a high-level abstraction for working with collections, allowing you to express complex operations using a functional approach.
Here are some key features of the Stream API:
Lazy evaluation: Intermediate operations are not executed until a terminal operation is invoked on the stream.
Immutability: Streams do not modify the underlying data source.
Optional parallelism: You can easily switch between sequential and parallel processing of stream elements.
Here is an exemple:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");
// Filter names starting with "C" and convert them to uppercase
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("C"))
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(filteredNames); // Output: [CHARLIE]
names.stream() creates a stream from the list of names.
.filter(name -> name.startsWith("C")) applies a filter that retains only the names starting with the letter “C”.
.map(String::toUpperCase) transforms each remaining name to its uppercase form.
.collect(Collectors.toList()) is a terminal operation that collects the resulting names into a new list.
Some common operations provided by the Stream API include filter, map, flatMap, reduce, collect, forEach, anyMatch, allMatch, noneMatch, findFirst, and findAny. The API also supports working with primitive types (IntStream, LongStream, and DoubleStream) and parallel processing using the parallelStream() method.
- Optional
The Optional class was introduced in Java 8 as a container object that may or may not contain a non-null value. Before Optional, a common practice was to use null to denote the absence of a value. However, this often leads to NullPointerExceptions, which can be a source of bugs.
Optional is designed to help developers deal with null values in a more deliberate and safer way, providing a clear API to represent the absence or presence of a value. By using Optional, you are making it explicit in your method signatures that there might not be a value returned, forcing users of your API to think about handling the absence of a value.
Here’s a basic example of Optional usage:
public class Main {
public static void main(String[] args) {
Optional<String> nonEmpty = Optional.of("Hello");
Optional<String> empty = Optional.empty();
System.out.println(nonEmpty.orElse("Default")); // Outputs: Hello
System.out.println(empty.orElse("Default")); // Outputs: Default
}
}
The Optional class in Java was specifically designed to be used as a method return type, as a more expressive alternative to null.
When a method signature returns an Optional, it is clear to the clients of that method that there might not be a value returned, indicating an optional return value. It forces the client to think about the case where the returned value is absent.
Here’s a simple example:
public class UserService {
private Map<String, User> users;
public UserService(Map<String, User> users) {
this.users = users;
}
public Optional<User> findUser(String username) {
return Optional.ofNullable(users.get(username));
}
}
UserService userService = ... // Initialize user service
Optional<User> user = userService.findUser("username");
if (user.isPresent()) {
// Do something with the user
} else {
// Handle the case where the user was not found
}
The Optional class provides several other methods to work with the optional value, such as:
isPresent(): Returns true if there is a value present, otherwise false.
ifPresent(Consumer<? super T> action): If a value is present, performs the given action with the value, otherwise does nothing.
orElse(T other): Returns the value if present, otherwise returns other.
orElseGet(Supplier<? extends T> other): Returns the value if present, otherwise returns the result produced by the supplying function.
orElseThrow(): If a value is present, returns the value, otherwise throws NoSuchElementException.
orElseThrow(Supplier<? extends X> exceptionSupplier): If a value is present, returns the value, otherwise throws an exception produced by the exception supplying function.
Remember, Optional isn’t intended to replace every single null reference in your codebase but rather to be used as a method return type where you expect that sometimes there will be no meaningful value to return.
2. Java 9 (September 2017)
The most significant feature introduced in Java 9 was the Java Platform Module System (JPMS), also known as Project Jigsaw. This allowed developers to organize their code into modules, making it easier to manage and understand dependencies within their codebase.
- Java Platform Module System (JPMS)
The Java Platform Module System (JPMS), introduced in Java 9, allows developers to create modular Java applications. Each module can encapsulate packages and declare its dependencies, providing better application structure and improving maintainability and performance.
Here is an example of how to use JPMS:
Suppose you have two modules, com.example.foo and com.example.bar. The com.example.foo module depends on com.example.bar.
package com.example.bar;
public class Bar {
// ... class content
}
In this example, com.example.foo declares in its module-info.java that it requires com.example.bar. Conversely, com.example.bar declares that it exports the package com.example.bar, making it accessible to other modules.
This is a simple example. JPMS can handle much more complex scenarios, including requiring specific versions of modules, and different types of module (like open modules). It’s also possible to create modular JAR files, which can be run on both modular and non-modular Java applications, increasing compatibility.
Remember that while JPMS can greatly improve the structure and maintainability of your Java applications, it does require careful planning and design to use effectively. If used poorly, it could make your application more difficult to understand and maintain.
3. Java 10 (March 2018) Compiler and GC optimisation
4. Java 11 (September 2018) LTS standadise var and http2/client
5. Java 12 (March 2019) preview for switch expression
6. Java 13 (September 2019) preview switch expression and text blocks, ZGC
7. Java 14 (March 2020) Preview Pattern matching and Record, standard switch expression,