Streamlining Your Codebase: A Deep Dive into Java 8's Predicate, Consumer, and Supplier

In the ever-evolving world of Java programming, Java 8 introduced a great shift with the inclusion of functional interfaces. These interfaces enable developers to write more concise and expressive code by leveraging lambda expressions. Among the most versatile functional interfaces introduced in Java 8 are Predicate, Consumer, and Supplier. In this blog, we will explore the capabilities and use cases of these powerful interfaces.

What is Functional Interfaces?

In Java, a functional interface is an interface that contains only one abstract method but can have many default and static methods. By allowing only one abstract method, these interfaces facilitate the use of lambda expressions and method references, resulting in more expressive and efficient code.Java 8 introduced a special annotation @FunctionalInterface to explicitly mark interfaces as functional interfaces. This annotation is optional, but using it helps prevent the accidental addition of more abstract methods to the interface.

In Simple Words, any Interface with a Single Abstract Method (SAM) is a functional Interface.

Here are some functional interfaces in Java8:

//Below Interfaces have only one methods in it and they are:
Runnable =>  run()
Comparable => compareTo(T o)
Callable   => call()
Comparator => compare(T o1 , T o2)
Predicate => test(T o)
Consumer => accept(T o)
Supplier => get()

Predicate

The Predicate functional interface is ideal for scenarios where you need to filter data based on a specific condition. It accepts a single argument as an input and returns a boolean value indicating whether the given passed argument meets the condition defined within the lambda expression. A Predicate can be used in various scenarios such as filtering lists, data validation, and conditional operations.

A predicate has a test() method which takes in a single argument and returns a boolean value.

boolean test(T t)

Example: Filtering a list of Users

Suppose you have a list of User Objects with attributes like username, email , mobile and age, each representing a registered user on your application. Now, you want to validate the usernames in a list of User objects and filter out the users who have valid usernames. We can achieve this using the Predicate functional interface.

User.java

import java.util.*;

class User {
    private String username;
    private String email;
    private int mobile;
    private int age;

    public User(String username, String email, int mobile, int age) {
        this.username = username;
        this.email = email;
        this.mobile = mobile;
        this.age = age;
    }
    public String getUsername() {
        return username;
    }

    public String getEmail() {
        return email;
    }

   public int getMobile() {
        return mobile;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "User{" + "username='" + username +
             '\'' + ", email='" + email + '\'' + 
             ", mobile='" + mobile + '\'' +  ", age=" + age +  '}';
    }
}

Main.java

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

public class Main{
    public static void main(String[] args) {
        List<User> users = new ArrayList<>();
        users.add(new User("the_Seer_Programmar77", "seer@gmail.com",9345682189,23));
        users.add(new User("alice!@#", "alice@example.com",9832174721, 25));
        users.add(new User("bob_456", "bob@example.com",7865432678, 28));
        users.add(new User("abhinav", "abhinav@example.com",6543218754, 22));

        // Predicate to validate usernames (should contain letters, numbers, and underscores and be at least 6 characters long)
        Predicate<User> isValidUsername = user -> user.getUsername()
               .matches("[a-zA-Z0-9_]+") && user.getUsername().length() >= 6;

        // Filtering the list of users using the Predicate
        List<User> validUsers = filterUsers(users, isValidUsername);

        // Displaying the valid users
        System.out.println("Valid Users:");
        validUsers.forEach(System.out::println);
    }

    // Method to filter users based on the given Predicate
    private static List<User> filterUsers(List<User> users, Predicate<User> predicate) {
        List<User> filteredList = new ArrayList<>();
        for (User user : users) {
            if (predicate.test(user)) {
                filteredList.add(user);
            }
        }
        return filteredList;
    }
}

Output

Valid Users:
User{username='the_Seer_Programmar77', email='seer@gmail.com', mobile =9345682189, age=23}
User{username='bob_456', email='bob@example.com', mobile = 7865432678 , age=28}

Consumer

The Consumer functional interface is utilized when an operation needs to be performed on each element of a collection. It takes a single argument and does not return any value. The consumer can be used for various scenarios such as logging, printing, and batch processing.

The consumer interface has two methods which include one abstract method i.e. accept() and one default method.

void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after);

Let's take an example to understand the consumer interface:

Suppose we have an Employee object with the attributes name and salary and now we want to update salaries using consumer.

Employee.java

import java.util.*;

class Employee {
    private String name;
    private double salary;

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    public String getName() {
        return name;
    }

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", salary=" + salary +
                '}';
    }
}

Main.java

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

public class Main{
    public static void main(String[] args) {
        List<Employee> employees = new ArrayList<>();
        employees.add(new Employee("Abhinav", 50000.0));
        employees.add(new Employee("Rishabh", 60000.0));
        employees.add(new Employee("Bob", 45000.0));

        // Consumer to give a 5% raise to employee salaries
        Consumer<Employee> giveRaise = employee -> {
            double raisedSalary = employee.getSalary() * 1.05;
            employee.setSalary(raisedSalary);
        };
        // Applying the raise using the Consumer
        employees.forEach(giveRaise);
        // Displaying the updated employee salaries
        System.out.println("Updated Employee Salaries:");
        employees.forEach(System.out::println);
    }
}

Output:

Updated Employee Salaries:
Employee{name='Abhinav', salary=52500.0}
Employee{name='Rishabh', salary=63000.0}
Employee{name='Bob', salary=47250.0}

Supplier

The Supplier functional interface is applied when you need to generate or provide a value lazily. The Supplier functional interface can be beneficial when you want to create objects on demand, especially in scenarios where creating objects might be resource-intensive.In short, this functional interface is used in all contexts when there is no input but an output is expected.

Let's take an example where we'll use Supplier to generate a random 6-digit otp.

Main.java

import java.util.*;
import java.util.function.Supplier;

public class Main{
    public static void main(String[] args) {

        Supplier<String> otpGenerator = () -> {
            StringBuilder otp = new StringBuilder();
            for (int i = 0; i < 6; i++) {
                otp.append((int) (Math.random() * 10));
            }
            return otp.toString();
        };

        String otp = otpGenerator.get();
        System.out.println("Generated OTP: " + otp);
    }
}

Output:

Generated OTP: 839647

"Congratulations, you've successfully unlocked the power of Java 8 functional interfaces! Now you're armed with the mystical magic of Predicates, Consumers, and Suppliers! No more loops or spells are needed; So go forth, noble developer, and let the functional magic guide you to cleaner, more delightful code! 🧙‍♂️✨"