Java 8 Features: Functional Programming—a complete tutorial with examples 2025

Java 8, released in 2014, introduced a game-changer for Java developers: functional programming features. But fear not if you’re just getting around to them! This guide will break down the key concepts in a clear and practical way.

Java 8 features

Why Functional Programming in Java 8?

Imagine writing code that reads like a recipe. Instead of focusing on how things happen (loops, conditionals), you describe what you want to achieve with the data. This is the essence of functional programming, and it leads to cleaner, more concise code.

Write Concise Code with Lambda Expressions

Lambda expressions mean a block of code that you can pass around so it can be executed later, once or multiple times. It uses a lambda operator (->).

  • Points to be noted:
    • The body of a lambda expression can contain zero, one, or more statements.
    • When there is a single statement, curly brackets are not mandatory, and the return type of the anonymous function is the same as that of the body expression.
    • If there are more than one statement, then those must be enclosed in curly brackets (a code block), and the return type of the anonymous function is the same as the type of the value returned within the code block, or void if nothing is returned.
    Java
    // Traditional approach
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            System.out.println("Hello, World!");
        }
    };
    
    // Using lambda expression
    Runnable runnable = () -> System.out.println("Hello, World!");

    Revolutionizing with Functional Interfaces

    A functional interface, introduced in Java 8, is an interface that has only a single abstract method. Conversely, if you have any interface that has only a single abstract method, then that will effectively be a functional interface. One of the most important uses of functional interfaces is that implementations of their abstract methods can be passed around as lambda expressions. Java 8 provides built-in functional interfaces like Predicate, Function, Supplier and Consumer. Here’s how you can use them:

    Java
    // Predicate example: Check if a number is even
    Predicate<Integer> isEven = num -> num % 2 == 0;
    System.out.println(isEven.test(5)); // Output: false
    
    // Function example: Convert string to uppercase
    Function<String, String> toUpperCase = str -> str.toUpperCase();
    System.out.println(toUpperCase.apply("hello")); // Output: HELLO
    
    // Consumer example: Print each element of a list
    Consumer<String> print = System.out::println;
    List<String> fruits = Arrays.asList("Apple", "Banana", "Orange");
    fruits.forEach(print);
    

    1. Streams: Streams are like conveyor belts for data; they can be defined as a sequence of elements from a source (collections) that supports aggregate operations like filtering, finding max, min, and sum on them. Stream elements support sequential and parallel aggregate operations. It is conceptually a fixed data structure in which elements are computed on demand. Streams bring functional programming to Java. A stream pipeline consists of a source, followed by zero or more intermediate operations, and a terminal operation.

    Example: Source->Filter->Collect or Source-> Filter-> Map-> Collect
    Streams can be created from Collections, Lists, Sets, arrays, lines of a file, etc.
    Intermediate operations such as filter, map, and sort return a new stream so that we can chain multiple operations. Terminal operations such as forEach, collect, and reduce are either void or return a non-stream result.

    Java
    List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
    
    // Filter names starting with 'A' and print them
    names.stream()
         .filter(name -> name.startsWith("A"))
         .forEach(System.out::println);

    Other Important Java 8 features

    1. Optional: Before Java 8, handling null values was cumbersome. The Optional class helps you avoid NullPointerExceptions. It’s a wrapper for a value that may or may not be present. You can use methods like and orElse to safely access the value or provide a default if it’s missing.

    Java
    Optional<String> optionalName = Optional.ofNullable(null);
    String name = optionalName.orElse("Unknown");
    System.out.println(name); // Output: Unknown

    2. Method References: It is a new feature added in Java 8. A shorthand notation of a lambda expression to call a method. Method reference is used to refer to a method of functional interface. The operator (::) is used in method references to separate the class or object from the method name.

    Java
    List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
    
    // Using lambda expression
    names.forEach(name -> System.out.println(name));
    
    // Using method reference
    names.forEach(System.out::println);

    3. Default Methods: Java 8 allows interfaces to have method implementations. This is particularly useful for adding common functionality to existing interfaces without modifying all the implementing classes. Default methods provide a default behavior, while static methods are utility functions usable without creating an instance.

    Java
    interface Greeting {
        void sayHello();
    
        default void sayHi() {
            System.out.println("Hi!");
        }
    }
    
    class EnglishGreeting implements Greeting {
        public void sayHello() {
            System.out.println("Hello!");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Greeting englishGreeting = new EnglishGreeting();
            englishGreeting.sayHello(); // Output: Hello!
            englishGreeting.sayHi();    // Output: Hi!
        }
    }

    4. Completable Future: Imagine you have tasks that take time to complete, like downloading a file or fetching data from a server. Traditionally, you might block your main thread while waiting for these tasks to finish. Completable Future offers a way to handle these tasks asynchronously, freeing up your main thread to continue executing other code.

    Key Concepts:
    • Represents an asynchronous operation: A Completable Future holds the eventual result of an asynchronous task.
    • Non-blocking: The main thread doesn’t wait for the asynchronous task to finish.
    • Composable: You can chain multiple asynchronous operations together.
    • Handling Completion and Errors: Provides methods to handle successful completion, errors, or cancellation of the asynchronous task.
    Java
    CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
        // Simulate a long-running computation
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "Hello, World!";
    });
    
    // Attach a callback to the future
    future.thenAccept(result -> System.out.println("Result: " + result)); // Output: Result: Hello, World!
    
    // Continue with other tasks while waiting for the future to complete
    System.out.println("Do something else while waiting...");

    5. Date and Time API: This overhaul replaced the old and cumbersome java.util.Date class. Java 8 introduces a new Date and Time API, which is more comprehensive and developer-friendly than the legacy java.util.Date and java.util.Calendar classes, which provide classes like LocalDate, LocalTime, and DateTimeFormatter for working with dates and times. Here’s an example:

    Java
    // Creating a LocalDate object
    LocalDate today = LocalDate.now();
    System.out.println("Today's date: " + today); // Output: Today's date: 2025-04-20
    
    // Formatting a LocalDate object
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");
    String formattedDate = today.format(formatter);
    System.out.println("Formatted date: " + formattedDate); // Output: Formatted date: 20-04-2025
    Conclusion of Java 8 features

    Java 8 introduced a wealth of features that have transformed Java development. Let’s explore some of the gems:

    • Lambda Expressions and Method References: These features allow you to write concise and expressive code for operations on data. Lambda expressions act like tiny anonymous functions, while method references provide a shortcut to existing methods.
    • Stream API: This powerful API revolutionized how you work with collections. Streams process data elements sequentially, allowing you to filter, transform, and analyze data with ease.
    • Date and Time API: Gone are the days of wrestling with the old java.util.Date class. The new Date and Time API offers clear and intuitive classes for working with dates, times, and durations.
    • Completable Future: Asynchronous programming got a major boost with Completable Future. This class helps you manage asynchronous tasks without blocking your main thread, leading to more responsive applications.
    Benefits:
    • Cleaner Code: The features promote a more concise and readable coding style.
    • Increased Efficiency: Stream API and Completable Future optimize data processing and asynchronous operations.
    • Improved maintainability: clear and expressive code is easier to understand and modify in the future.

    Mastering these features will make you a more proficient Java developer. So, dive in, experiment, and unleash the power of Java 8!. The most recent version is Java 21. Check out the difference between Java 8 and Java 21.

    Leave a Comment

    Comments

    No comments yet. Why don’t you start the discussion?

    Leave a Reply

    Your email address will not be published. Required fields are marked *