Top Java 8 Features with Examples – A Complete Guide

Updated on September 19, 2024

Article Outline

Java is a very useful, popular and flexible programming language. Java, from the very beginning till today has included a number of functionalities that improve performance, make coding less complex, or make the development of software easy. With every new version and update, Java’s usability, security, and operational efficiency grow to meet the worldwide demand for the software development process.

 

In this blog, we will be online exploring some of the new features that were introduced in Java 8. Key concepts such as lambda expressions, functional interfaces and method references will be discussed in this comprehensive guide. Enhancement of stream APIs and the Optional class, new Date/Time APIs, API improvements to Collection and Concurrency, and a few other updates will also be discussed here.

What is Java 8?

Java 8 can be treated as the first non-maintenance release of the Java platform. It was announced by Oracle in March 2014. It came with major improvements in the language and also in its core libraries with the introduction of the concept of stream API and functional programming. The target of Java 8 was to minimise code, eliminate boilerplate code and enhance the compatibility and expressiveness of Java.

 

With Java 8, the performance and security of the Java Runtime Environment were also enhanced. This release has new features that allow for the use of modern tools and libraries which help in making code that is more succinct, clear, and easy to maintain. The features present in Java 8 are inclined towards the current programming language trends, thus becoming competitive among programmers around the world.

*Image
Get curriculum highlights, career paths, industry insights and accelerate your technology journey.
Download brochure

Lambda Expressions

In earlier versions of Java, Functional interfaces ( interfaces with one abstract method) were mostly implemented with the help of anonymous inner classes. This method was very tedious as a lot of algorithmic code had to be created for even simple operations like list sorting and thread creation.

 

For example, to sort a list of strings by length, we had to create an anonymous inner class for the Comparator interface:

import java.util.*; public class BeforeJava8Example { public static void main(String[] args) { List<String> names = Arrays.asList("John", "Alice", "Bob", "Steve");   // Sorting the list using an anonymous inner class Collections.sort(names, new Comparator<String>() { @Override public int compare(String s1, String s2) { return Integer.compare(s1.length(), s2.length()); } });   // Print sorted names System.out.println(names); } }

In this example, an anonymous inner class is used to define a custom comparator for sorting strings by length. This approach is verbose and harder to read.

Introduction to Lambda Expressions

Lambda expressions provide a simpler way to represent instances of functional interfaces using a concise syntax. They allow us to write less code while achieving the same functionality. Lambda expressions help to remove the boilerplate code associated with anonymous inner classes.

 

Syntax of a Lambda Expression:

The basic syntax of a lambda expression is:

(parameters) -> expression

 

Or, for more complex expressions:

 

(parameters) -> { statements; }

 

  • Parameters: Input values (if any) that the lambda takes.
  • Arrow Token (->): Separates the parameters from the body of the lambda.
  • Body: The code that implements the functional interface method. It can be a single expression or a block of statements.

 

Now, let’s rewrite the earlier example using a lambda expression:

import java.util.*; public class Java8LambdaExample { public static void main(String[] args) { List<String> names = Arrays.asList("John", "Alice", "Bob", "Steve"); // Sorting the list using a lambda expression Collections.sort(names, (s1, s2) -> Integer.compare(s1.length(), s2.length()));   // Print sorted names System.out.println(names); } }

Explanation:

 

  • Lambda Expression: (s1, s2) -> Integer.compare(s1.length(), s2.length()) replaces the anonymous inner class. The lambda takes two parameters, s1 and s2, and returns the result of comparing their lengths.
  • Reduced Boilerplate: We no longer need to explicitly define the Comparator interface and its compare method. The lambda expression directly provides the sorting logic, making the code shorter and more readable.

Functional Interfaces

Before Java 8, to define a contract for a single method, we used regular interfaces and often implemented them using anonymous inner classes. A functional interface is an interface with only one abstract method. Even though the concept existed before, it wasn’t explicitly supported or named as “functional interfaces.”

 

For example, to create a simple interface for calculating the square of a number, we would have to write a lot of code using an anonymous inner class:

interface SquareCalculator { int calculate(int x); }   public class BeforeJava8FunctionalInterface { public static void main(String[] args) { // Using an anonymous inner class to implement the functional interface SquareCalculator calculator = new SquareCalculator() { @Override public int calculate(int x) { return x * x; } };   // Calculate the square of a number System.out.println(calculator.calculate(5)); } }

In the code above, we use an interface named SquareCalculator with a single abstract method calculate(int x). We then implement this interface using an anonymous inner class, which is verbose and makes the code harder to read.

Introduction to Functional Interfaces in Java 8

Also, Java 8 introduced the @FunctionalInterface annotation to denote the functional interface clearly and to limit the number of abstract methods to one. The idea of using the functional interface is to make the code more efficient by eliminating the use of an anonymous inner class and switching to the utilisation of lambda expressions.

How Functional Interfaces Work with Lambda Expressions

Lambdas can be used as an instance of a functional interface. It is important to understand that by means of functional interfaces, we may pass methods to some other method (which can accept this behaviour), so the code design becomes more elegant and compact.

 

Example: Functional Interface with Lambda Expression

 

Let’s rewrite the previous example using a functional interface and a lambda expression:

@FunctionalInterface interface SquareCalculator { int calculate(int x); }   public class Java8FunctionalInterfaceExample { public static void main(String[] args) { // Using a lambda expression to implement the functional interface SquareCalculator calculator = (x) -> x * x;   // Calculate the square of a number System.out.println(calculator.calculate(5)); } }

Explanation:

 

  • Functional Interface: The SquareCalculator interface is marked with the @FunctionalInterface annotation, indicating that it has a single abstract method.
  • Lambda Expression: (x) -> x * x is the lambda expression that provides the implementation of the calculate method. It takes one parameter x and returns x * x.
  • Simplified Code: The lambda expression replaces the need for an anonymous inner class, making the code shorter and easier to read.

Method Reference

Before Java 8, to perform operations like printing elements of a list or applying a function, developers often used anonymous inner classes or manually called methods within loops. This approach required a lot of boilerplate code, even for simple actions. For example, to print all names in a list, we had to use a loop and explicitly call System.out.println() for each element:

import java.util.*; public class BeforeJava8MethodReference { public static void main(String[] args) { List<String> names = Arrays.asList("John", "Alice", "Bob", "Steve");   // Using a for-each loop to print each name for (String name : names) { System.out.println(name); } } }

While this code is simple, it still involves multiple lines to perform a straightforward task. Java 8 offers a way to make this even more concise using method references.

Introduction to Method References in Java 8

Method references were also introduced in Java 8 to improve code readability and reduce the amount of code by allowing programmers to reference existing methods in a more straightforward manner. One can pass methods as arguments in functional interfaces as method references and there is no need to define the body using the involved lambdas when that method exists.

 

Types of Method References

 

  1. Reference to a Static Method: ClassName::staticMethodName
  2. Reference to an Instance Method of a Particular Object: instance::instanceMethodName
  3. Reference to an Instance Method of an Arbitrary Object of a Particular Type: ClassName::instanceMethodName
  4. Reference to a Constructor: ClassName::new

 

Example: Using Method References

 

Here’s how you can use method references to simplify the earlier example:

import java.util.*;   public class Java8MethodReferenceExample { public static void main(String[] args) { List<String> names = Arrays.asList("John", "Alice", "Bob", "Steve");   // Using a method reference to print each name names.forEach(System.out::println); } }

Explanation:

 

  • Method Reference:out::println is a method reference to the println method of the PrintStream class. It replaces the need for writing out the call explicitly for each element.
  • Simplified Code: The method reference removes the boilerplate code and directly calls the existing method, making it more concise and readable.

Streams

Before Java 8, operations on collections such as filtering, mapping, and reducing required using loops and explicit iteration. These operations often resulted in verbose and error-prone code. For example, if we wanted to filter a list of integers to find the even numbers and then square them, we would have to write nested loops or multiple lines of code:

import java.util.*;   public class BeforeJava8Streams { public static void main(String[] args) { List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); List<Integer> evenSquares = new ArrayList<>();   // Using a loop to filter and square even numbers for (Integer number : numbers) { if (number % 2 == 0) { evenSquares.add(number * number); } }   // Print the result System.out.println(evenSquares); } }

This code uses a for loop to iterate through the list, checks each number to see if it is even, and then squares it. While this approach works, it involves a lot of boilerplate code, making it less readable and harder to maintain.

Introduction to Streams in Java 8

The Stream API was incorporated in Java 8 with the aim of easing the work done on collections in a functional manner. There are various operations offered by streams which include filtering, mapping and reducing and those operations can be friendlier and efficient. They allow developers to process data without mutation or low-level control.

 

How Streams Work and Improve Code

 

A stream can be defined as a sequence of elements which can either be operated upon one at a time or in many directions. They are not containers and do not hold data but rather act on data sources within collection-type objects such as arrays, channels, or IO. Inline streaming APIs also allow extending the operations of filtering, mapping, and obtaining objects of particular types, and even including the outcome all in plain lines of code.

 

Example: Using Streams to Filter and Square Even Numbers

 

Here’s how you can rewrite the earlier example using the Stream API:

import java.util.*; import java.util.stream.*;   public class Java8StreamExample { public static void main(String[] args) { List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);   // Using Streams to filter and square even numbers List<Integer> evenSquares = numbers.stream() .filter(number -> number % 2 == 0) // Filter even numbers .map(number -> number * number)    // Square each number .collect(Collectors.toList());     // Collect results into a List   // Print the result System.out.println(evenSquares); } }

Explanation:

 

  • Stream Creation:stream() creates a stream from the list of integers.
  • Filter Operation: .filter(number -> number % 2 == 0) filters out only even numbers.
  • Map Operation: .map(number -> number * number) squares each even number.
  • Collect Operation: .collect(Collectors.toList()) collects the result into a new list.

Comparable and Comparator

There have been enhancements to the Comparator interface as of Java 8 which aim at making it easy to carry out sorting operations. Now the Comparator interface has a number of default and static methods that can simply define the sorting procedure, such as comparing(), thenComparing(), reversed() and nullsFirst(). These methods have made the code less complicated and more readable during the sorting of collections.

 

Example: Using Comparator with Java 8 Enhancements

 

Let’s consider sorting a list of Person objects by their names and ages:

import java.util.*; class Person { String name; int age; Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return name + " (" + age + ")"; } } public class Java8ComparatorExample { public static void main(String[] args) { List<Person> people = Arrays.asList( new Person("Alice", 30), new Person("Bob", 25), new Person("Charlie", 35) ); // Sorting using Java 8 Comparator enhancements people.sort(Comparator.comparing(Person::getName).thenComparing(Person::getAge));   // Print sorted list System.out.println(people); } }

Explanation:

 

  • comparing(): Creates a comparator based on the getName method.
  • thenComparing(): Adds a secondary comparison by age using the getAge method.
  • Cleaner Code: The code is more concise and readable compared to previous methods, allowing chaining and sorting by multiple fields easily.

Optional Class

Optional Class was added in version 8 to tackle the Null Pointer Exception issue and to deal with the case where a value can be present or absent in a precise and controlled way. The optional class is a value presence container object that holds a value or nothing.

 

Example: Using Optional to Avoid NullPointerException

 

Here’s an example of how to use Optional to handle a potentially null value:

import java.util.Optional;   public class Java8OptionalExample { public static void main(String[] args) { String name = "John";   // Wrapping a non-null value in an Optional Optional<String> optionalName = Optional.ofNullable(name);   // Using Optional to handle the value safely optionalName.ifPresentOrElse( n -> System.out.println("Name is: " + n), () -> System.out.println("Name is not available") );   // Providing a default value if name is null String defaultName = optionalName.orElse("Unknown"); System.out.println("Default name: " + defaultName); } }

Explanation:

 

  • ofNullable(): Wraps a potentially null value in an Optional container.
  • ifPresentOrElse(): Checks if a value is present and performs actions accordingly.
  • orElse(): Provides a default value if the wrapped value is absent.

Date/Time API

Java 8 introduced a new java.time package to improve handling of dates and times. Unlike the old Date class and calendars, the new Time API is safe for use across many threads as it makes use of immutable objects. Calendar was one of the mutable classes which were not thread-safe. The new Date Time API does not have these limitations. The new API provides an approach of being immutable and consistent, enabling the handling of date and time to be less tedious in comparison to the old one.

 

Example: Using the New Date/Time API

 

Let’s look at how to work with dates using the new LocalDate, LocalTime, and LocalDateTime classes:

import java.time.LocalDate; import java.time.LocalTime; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter;   public class Java8DateTimeExample { public static void main(String[] args) { // Current date LocalDate currentDate = LocalDate.now(); System.out.println("Current Date: " + currentDate);   // Current time LocalTime currentTime = LocalTime.now(); System.out.println("Current Time: " + currentTime);   // Current date and time LocalDateTime currentDateTime = LocalDateTime.now(); System.out.println("Current Date and Time: " + currentDateTime);   // Formatting date DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm"); String formattedDateTime = currentDateTime.format(formatter); System.out.println("Formatted Date and Time: " + formattedDateTime); } }

Explanation:

 

  • LocalDate, LocalTime, LocalDateTime: Represents date, time, and date-time without timezone information.
  • DateTimeFormatter: Formats date and time according to a specified pattern.
  • Immutable and Thread-Safe: All date/time objects are immutable, preventing accidental changes and ensuring thread safety.

Collection API Improvements

In Java 8, several changes were made to the Collection API so that operations on collections became fast and easy as well as more flexible in their usage. One of the major changes was the introduction of the forEach() method which enables iteration of collections with the use of lambda expressions.

 

Example: Using Collection API Improvements

 

Here’s an example demonstrating some of the new methods added to the Collection interface:

import java.util.*; import java.util.stream.Collectors;   public class Java8CollectionExample { public static void main(String[] args) { List<String> names = new ArrayList<>(Arrays.asList("John", "Alice", "Bob", "Steve"));   // Using forEach to print all names names.forEach(name -> System.out.println(name));   // Removing names that start with 'A' names.removeIf(name -> name.startsWith("A")); System.out.println("After removing names starting with 'A': " + names);   // Converting list to uppercase using stream List<String> upperCaseNames = names.stream() .map(String::toUpperCase) .collect(Collectors.toList()); System.out.println("Uppercase Names: " + upperCaseNames); } }

Explanation:

 

  • forEach(): Iterates over each element in the collection.
  • removeIf(): Removes elements based on a condition.
  • stream(): Converts the collection into a stream for further processing.

Concurrency API Improvements

In Java 8 Concurrent Utilities were provided to make it easier to write concurrent programs. The java.util.concurrent package now has classes like CompletableFuture and new features in the Executor Service package targeted at asynchronous programming.

 

Example

 

CompletableFuture provides a flexible way to write asynchronous code without explicitly managing threads. It allows chaining of tasks and handling exceptions in a clean way.

import java.util.concurrent.CompletableFuture;   public class Java8ConcurrencyExample { public static void main(String[] args) { // Creating an asynchronous task CompletableFuture<Void> future = CompletableFuture.runAsync(() -> { try { Thread.sleep(1000); System.out.println("Asynchronous Task Completed!"); } catch (InterruptedException e) { System.out.println("Task Interrupted"); } });   // Continue with the main thread System.out.println("Main thread is running...");   // Wait for the task to be completed future.join(); } }

Explanation:

 

  • runAsync(): Runs a task asynchronously without blocking the main thread.
  • join(): Waits for the asynchronous task to complete before moving forward.
  • Asynchronous Programming: Improves performance by allowing tasks to run concurrently without managing threads manually.

Miscellaneous

Java 8 brought several smaller but significant improvements to enhance various aspects of the language, such as the Collectors class, I/O operations, base64 encoding/decoding, JDBC, and more. Let’s explore these changes with examples.

Collectors Class

The Collectors class provides various utility methods to collect stream elements into different data structures like lists, sets, and maps. It also supports complex operations like grouping, partitioning, and joining elements.

 

Example: Using Collectors to Group Elements

import java.util.*; import java.util.stream.Collectors;   public class Java8CollectorsExample { public static void main(String[] args) { List<String> names = Arrays.asList("John", "Alice", "Bob", "Steve", "Anna");   // Grouping names by their first letter Map<Character, List<String>> groupedNames = names.stream() .collect(Collectors.groupingBy(name -> name.charAt(0)));   System.out.println(groupedNames); } }

IO Enhancements

Java 8 introduced methods like Files.lines() to read files as streams, making I/O operations more flexible and efficient. It also improved file attribute handling using the java.io.file package.

 

Example: Reading a File as a Stream

import java.nio.file.*; import java.io.IOException;   public class Java8IOExample { public static void main(String[] args) throws IOException { Path path = Paths.get("example.txt");   // Reading file lines as a stream Files.lines(path).forEach(System.out::println); } }

Base64 Encode Decode

Java 8 introduced the Base64 class in java.util for encoding and decoding data in base64 format, a common need for handling binary data in text form.

 

Example: Encoding and Decoding using Base64

import java.util.Base64;   public class Java8Base64Example { public static void main(String[] args) { String original = "Java 8 Base64 Example"; String encoded = Base64.getEncoder().encodeToString(original.getBytes()); System.out.println("Encoded: " + encoded);   String decoded = new String(Base64.getDecoder().decode(encoded)); System.out.println("Decoded: " + decoded); } }

JDBC Enhancements

Java 8 improved JDBC by adding methods like executeLargeUpdate() to handle large updates and introducing support for the java.time package for date/time management.

 

Example: Using executeLargeUpdate in JDBC

import java.sql.*;   public class Java8JDBCExample { public static void main(String[] args) throws SQLException { Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password"); Statement stmt = conn.createStatement();   // Using executeLargeUpdate to handle large data long count = stmt.executeLargeUpdate("UPDATE my_table SET value = 100 WHERE id > 1000"); System.out.println("Rows updated: " + count); } }

Type and Repeating Annotations

Java 8 allows annotations to be applied to any use of a type, such as generics and casts, and supports repeating annotations where the same annotation can be used multiple times.

Example: Using Repeating Annotations

@interface Schedule { String day(); }   @Schedule(day = "Monday") @Schedule(day = "Friday") public class Java8AnnotationsExample {}

Nashorn JavaScript Engine

Java 8 introduced the Nashorn JavaScript engine, allowing Java applications to run JavaScript code natively and interact with Java objects.

 

Example: Running JavaScript from Java

import javax.script.*;   public class Java8NashornExample { public static void main(String[] args) throws ScriptException { ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); engine.eval("print('Hello from JavaScript');"); } }

Parallel Array Sorting

Java 8 added the Arrays.parallelSort() method, allowing arrays to be sorted in parallel using multiple threads, significantly improving performance for large arrays.

 

Example: Using Parallel Array Sorting

import java.util.Arrays;   public class Java8ParallelSortExample { public static void main(String[] args) { int[] numbers = {3, 6, 1, 8, 4, 5};   // Sorting the array in parallel Arrays.parallelSort(numbers);   System.out.println(Arrays.toString(numbers)); } }

Conclusion

Java 8 brought a significant shift to the Java programming language, introducing features that make development more concise, readable, and efficient. With the help of lambda expressions and the Stream API as well as the newly created Date/Time API, Java 8 has managed to help the coders create more functional and up-to-date code. Such advancements make Java more marketable and even more up-to-date with recent developments, expanding its range in software development.

 

Furthermore,  the Collection API, the management of concurrency and other features increased the potential and performance enhancements of Java. Whether you are using functional programming, performing complex data processing, or controlling concurrency, Java 8 has tools to solve such problems and make codes better and more elegant.

FAQs
The Stream API provides a way to process data in collections efficiently using functional-style operations.
Java 8 offered the new Date & Time API which is a comprehensive and easy-to-use function for effective working with Date and time.
Optional helps avoid NullPointerException by providing a container for values that might be null.
Method references simplify calling methods by referring to them directly, reducing boilerplate code.
MetaSpace replaces PermGen and dynamically grows as needed, reducing memory leaks.
Nashorn allows Java applications to run JavaScript code natively and interact with Java objects.

Updated on September 19, 2024

Link
left dot patternright dot pattern

Programs tailored for your success

Popular

Management

Data Science

Finance

Technology

Future Tech

Upskill with expert articles

View all
Hero Vired logo
Hero Vired is a leading LearnTech company dedicated to offering cutting-edge programs in collaboration with top-tier global institutions. As part of the esteemed Hero Group, we are committed to revolutionizing the skill development landscape in India. Our programs, delivered by industry experts, are designed to empower professionals and students with the skills they need to thrive in today’s competitive job market.
Blogs
Reviews
Events
In the News
About Us
Contact us
Learning Hub
18003093939     ·     hello@herovired.com     ·    Whatsapp
Privacy policy and Terms of use

|

Sitemap

© 2024 Hero Vired. All rights reserved