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! 🧙♂️✨"