Java 8 Features: Streams, Lambdas, and More

Java 8 Features: Streams, Lambdas, and More

Introduction to Java 8

Java 8, officially known as JDK 1.8, was released in March 2014 and brought with it a slew of new features that were designed to increase the productivity of developers and improve the performance of Java applications. One of the most significant changes was the introduction of lambda expressions, which allowed for cleaner and more concise code. Java 8 also introduced a new Streams API, which made it easier to process collections of data in a functional style.

Another important feature of Java 8 is the new Date and Time API, which was designed to fix the flaws of the old Date and Calendar classes. The new API is based on the ISO standard and is both more powerful and easier to use.

Java 8 also improved the existing concurrency API with the addition of CompletableFuture, which provides a simpler and more flexible way to write asynchronous code. Additionally, Java 8 brought enhancements to the JVM, including improved garbage collection and JavaScript runtime improvements.

Here is an example of a simple lambda expression in Java 8:

() -> System.out.println("Hello, World!");

This lambda expression can be used as an implementation of a functional interface with a single abstract method, like Runnable:

Runnable r = () -> System.out.println("Hello, World!");
new Thread(r).start();

And here is an example of using the new Streams API to filter and collect a list of strings:

List myList = Arrays.asList("a1", "a2", "b1", "c2", "c1");
List filteredList = myList.stream()
                                  .filter(s -> s.startsWith("c"))
                                  .collect(Collectors.toList());
System.out.println(filteredList); // prints [c2, c1]

Overall, Java 8 has been well received by the development community and continues to be widely used today, thanks to its powerful new features and improved performance.

Exploring Streams in Java 8

Streams in Java 8 represent a significant advancement in how we can manipulate collections of data. Unlike Collections, Streams are not a data structure. Instead, they provide a high-level abstraction for performing operations on sequences of elements. With Streams, developers can write more readable and maintainable code by chaining operations together and expressing complex data processing queries with less effort.

One of the key characteristics of Streams is that they are lazy. Computations on elements are only performed when needed, which can lead to performance improvements, especially when dealing with large datasets. Additionally, Streams can be easily parallelized, allowing for concurrent processing without the need to manage threads manually.

Let’s explore some common operations that can be performed using Streams:

  • Filtering: Selects elements that satisfy a predicate.
  • Mapping: Transforms each element into another form.
  • Sorting: Orders elements according to a provided Comparator or natural order.
  • Collecting: Gathers elements into a collection or another form of result.
  • Reducing: Combines elements to produce a single result.

Here is an example of a Stream operation that filters a list of integers, maps them to their squares, and collects the results into a list:

List numbers = Arrays.asList(1, 2, 3, 4, 5);
List squares = numbers.stream()
                               .filter(n -> n % 2 != 0)
                               .map(n -> n * n)
                               .collect(Collectors.toList());
System.out.println(squares); // prints [1, 9, 25]

We can also perform reduction operations like finding the sum of elements in a Stream:

int sum = numbers.stream().reduce(0, (a, b) -> a + b);
System.out.println(sum); // prints 15

Or find the maximum element:

Optional max = numbers.stream().reduce(Integer::max);
max.ifPresent(System.out::println); // prints 5 if the stream is not empty

Streams in Java 8 offer a powerful and flexible way to work with collections of data. They provide a clear and concise approach to expressing complex data processing tasks, which can lead to more readable and maintainable code. With the ability to perform parallel operations with ease, Streams are an essential tool for any Java developer looking to leverage the full power of Java 8.

Understanding Lambdas and Functional Interfaces

Lambdas, also known as lambda expressions, are one of the most talked-about features introduced in Java 8. They represent a step towards functional programming in Java, which allows you to write more concise and readable code. Understanding lambdas is closely tied to understanding functional interfaces, a concept this is key to working with lambda expressions effectively.

A functional interface is an interface that has only one abstract method (excluding the methods from Object class). These interfaces are also known as Single Abstract Method (SAM) interfaces. The purpose of a functional interface is to provide a type that can be used to declare a lambda expression. Let’s look at a simple example of a functional interface:

@FunctionalInterface
public interface SimpleFunctionalInterface {
    void execute();
}

Now, we can use this interface as the type for a lambda expression:

SimpleFunctionalInterface sfi = () -> System.out.println("Executing...");
sfi.execute(); // prints "Executing..."

Lambda expressions can take zero, one or more parameters, and they can return a value or be void. Here are some examples:

  • Zero parameters: () -> System.out.println("Zero parameters")
  • One parameter: (String s) -> System.out.println(s)
  • Two parameters: (int a, int b) -> a + b

Lambdas can also access final or effectively final variables from the enclosing scope:

final String greeting = "Hello";
SimpleFunctionalInterface hello = () -> System.out.println(greeting + ", World!");
hello.execute(); // prints "Hello, World!"

Java 8 comes with many built-in functional interfaces ready to be used. Some of the most commonly used ones are:

  • Consumer: Represents an operation that accepts a single input argument and returns no result.
  • Predicate: Represents a predicate (boolean-valued function) of one argument.
  • Function: Represents a function that accepts one argument and produces a result.
  • Supplier: Represents a supplier of results.

Lambda expressions combined with these built-in functional interfaces make it easy to write clean and succinct code. For example, using a Predicate to filter a list:

List names = Arrays.asList("Alice", "Bob", "Charlie");
List filteredNames = names.stream()
                                  .filter(name -> name.startsWith("A"))
                                  .collect(Collectors.toList());
System.out.println(filteredNames); // prints [Alice]

Lambdas and functional interfaces are powerful features in Java 8 that enable developers to write more expressive and less verbose code. They are particularly useful when used with the Streams API but are also invaluable for simplifying the use of existing APIs and writing cleaner event handlers, among other use cases.

Enhanced Concurrency with CompletableFuture

Java 8’s CompletableFuture is a welcome addition to the concurrency toolkit for asynchronous programming. It provides a way to write non-blocking code by executing tasks in a separate thread and then completing them in the future. This allows the main thread to continue processing other tasks while waiting for the results of asynchronous operations.

CompletableFuture can be used to represent any future result of an asynchronous computation, and it comes with a rich set of methods to compose, combine, and handle possible outcomes without getting into the low-level details of thread management.

Here’s an example of creating a CompletableFuture:

CompletableFuture completableFuture = new CompletableFuture();

Once we have a CompletableFuture, we can complete it with a value:

completableFuture.complete("Future's result");

One of the powerful features of CompletableFuture is its ability to chain multiple asynchronous operations. For example, we can take the result from one CompletableFuture, apply a function to it, and return a new CompletableFuture:

CompletableFuture future = completableFuture
    .thenApply(result -> result.toUpperCase())
    .thenApply(result -> "Processed " + result);

We can also handle errors gracefully using the exceptionally method:

CompletableFuture futureWithExceptionHandling = completableFuture
    .exceptionally(ex -> "Error occurred: " + ex.getMessage());

Another advantage of CompletableFuture is its ability to run multiple tasks in parallel and then combine their results:

CompletableFuture future1 = CompletableFuture.supplyAsync(() -> "Result of Future 1");
CompletableFuture future2 = CompletableFuture.supplyAsync(() -> "Result of Future 2");

CompletableFuture combinedFuture = future1
    .thenCombine(future2, (result1, result2) -> result1 + ", " + result2);

The combinedFuture will be completed when both future1 and future2 are completed, and its result will be the combination of both results.

CompletableFuture also supports synchronization mechanisms like join() and get(), which block until the future is completed:

String result = combinedFuture.join(); // Blocks until the result is available
System.out.println(result); // Prints the combined result

CompletableFuture in Java 8 provides a robust framework for asynchronous programming, allowing developers to write cleaner, non-blocking code. With its comprehensive API, handling asynchronous tasks becomes more straightforward and manageable, leading to more efficient and responsive applications.

Other Notable Features in Java 8

While Streams, Lambdas, and CompletableFuture are some of the highlights of Java 8, there are several other features that are worth mentioning. These features further contribute to the robustness and versatility of the Java platform.

One such feature is the introduction of default methods in interfaces. This allows developers to add new methods to interfaces without breaking the existing implementation of classes. Here’s an example:

public interface Vehicle {
    default void print() {
        System.out.println("I am a vehicle!");
    }
}

Another feature is the new java.util.Optional class. Optional is a container object used to contain not-null objects. Optional object is used to represent null with absent value. This class has various utility methods to facilitate code to handle values as ‘available’ or ‘not available’ instead of checking null values.

Optional optional = Optional.of("Hello");

if(optional.isPresent()){
    System.out.println(optional.get()); // prints Hello
}

Java 8 also introduced a new Nashorn JavaScript Engine, which allows us to embed JavaScript code within Java applications. Here’s how you might use it:

ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
ScriptEngine nashorn = scriptEngineManager.getEngineByName("nashorn");

String code = "var welcome = 'Hello'; welcome;";
Object result = nashorn.eval(code);

System.out.println(result); // prints Hello

Additionally, Java 8 improved type annotations where you can apply annotations anywhere you use a type. For example:

List names = Arrays.asList("Alice", "Bob", "Charlie");

These are just a few of the many enhancements that Java 8 has introduced. Each of these features can greatly improve the way developers write code and build applications, making Java an even more powerful tool for development.

Source: https://www.plcourses.com/java-8-features-streams-lambdas-and-more/


You might also like this video