Last Updated on September 15, 2024

A predicate, in mathematical terms, is a function that returns a boolean value. In the context of Java 8, a Java Predicates are a functional interface that represents a boolean-valued function of one argument. It’s part of the java.util.function package and is designed to be used as the assignment target for lambda expressions or method references.

The introduction of Predicates in Java 8 was motivated by the need to make the language more amenable to functional programming paradigms. Prior to Java 8, implementing simple boolean checks often required verbose anonymous inner classes. With Predicates, developers can now express these checks more succinctly and readably using lambda expressions or method references.

Structure of Predicates

The Predicate interface is defined in the java.util.function package and has the following structure.

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
    
    // Default methods
    default Predicate<T> and(Predicate<? super T> other) { ... }
    default Predicate<T> or(Predicate<? super T> other) { ... }
    default Predicate<T> negate() { ... }
    
    // Static methods
    static <T> Predicate<T> isEqual(Object targetRef) { ... }
    static <T> Predicate<T> not(Predicate<? super T> target) { ... } // Added in Java 11
}

@FunctionalInterface annotation indicates that Predicate is a functional interface, meaning it has exactly one abstract method. This allows Predicate to be used with lambda expressions and method references.

The Predicate interface is generic, allowing it to work with any type of object.

The the single abstract method of the Predicate interface is test(T t) method. It takes an object of type T as input and returns a boolean value. This method encapsulates the boolean-valued function that the Predicate represents.

Default methods:

  • and(Predicate<? super T> other): Returns a composed Predicate that represents a short-circuiting logical AND of this Predicate and another.
  • or(Predicate<? super T> other): Returns a composed Predicate that represents a short-circuiting logical OR of this Predicate and another.
  • negate(): Returns a Predicate that represents the logical negation of this Predicate.

The default methods (and(), or(), negate()) allow for easy composition of complex conditions from simpler ones, promoting modularity and reusability.

Static method:

  • isEqual(Object targetRef): Returns a Predicate that tests if two arguments are equal according to Objects.equals(Object, Object).

Example Usage

Let’s explore some code examples to illustrate the use of Predicates in various scenarios

Basic Usage

In this example, we create two simple Predicates: one that checks if a number is positive, and another that checks if a string is empty.

import java.util.function.Predicate;
public class BasicPredicateExample {
    public static void main(String[] args) {
        // Creating a Predicate using lambda expression
        Predicate<Integer> isPositive = n -> n > 0;
        
        // Using the Predicate
        System.out.println(isPositive.test(5));  // true
        System.out.println(isPositive.test(-3)); // false
        System.out.println(isPositive.test(0));  // false
        
        // Creating a Predicate using method reference
        Predicate<String> isEmpty = String::isEmpty;
        
        System.out.println(isEmpty.test(""));     // true
        System.out.println(isEmpty.test("Hello")); // false
    }
}

Combining Predicates

This example demonstrates how to combine Predicates using the and(), or(), and negate() methods. These methods allow you to create more complex conditions from simpler ones.

import java.util.function.Predicate;
public class CombiningPredicatesExample {
    public static void main(String[] args) {
        Predicate<String> hasLengthOf10 = s -> s.length() == 10;
        Predicate<String> startWithA = s -> s.startsWith("A");
        
        // Combining Predicates using and()
        Predicate<String> hasLengthOf10AndStartsWithA = hasLengthOf10.and(startWithA);
        
        System.out.println(hasLengthOf10AndStartsWithA.test("Abcdefghij")); // true
        System.out.println(hasLengthOf10AndStartsWithA.test("Abcdefgh"));   // false
        System.out.println(hasLengthOf10AndStartsWithA.test("Bbcdefghij")); // false
        
        // Combining Predicates using or()
        Predicate<String> hasLengthOf10OrStartsWithA = hasLengthOf10.or(startWithA);
        
        // true
        System.out.println(hasLengthOf10OrStartsWithA.test("Abcdefghij"));
        // true 
        System.out.println(hasLengthOf10OrStartsWithA.test("Abcdefgh"));   
       // true
        System.out.println(hasLengthOf10OrStartsWithA.test("Bbcdefghij")); 
       // false
        System.out.println(hasLengthOf10OrStartsWithA.test("Bbcdefgh"));   
      
        // Using negate()
        Predicate<String> doesNotStartWithA = startWithA.negate();
        
        System.out.println(doesNotStartWithA.test("Apple"));  // false
        System.out.println(doesNotStartWithA.test("Banana")); // true
    }
}

Predicates with Collections

Predicates can be used effectively with the Stream API to filter collections.

It demonstrates the power of combining functional programming concepts with Predicates for concise and expressive code

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class PredicateWithCollectionsExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("John", "Jane", "Adam", "Eve", "Alice", "Bob");
        
        // Predicate to check if a name starts with 'J'
        Predicate<String> startsWithJ = name -> name.startsWith("J");
        
        // Using Predicate with Stream API
        List<String> namesStartingWithJ = names.stream()
                                               .filter(startsWithJ)
                                               .collect(Collectors.toList());
        System.out.println("Names starting with J: " + namesStartingWithJ);
        
        // Combining multiple conditions
        Predicate<String> lengthGreaterThan3 = name -> name.length() > 3;
        Predicate<String> startsWithJAndLengthGreaterThan3 =                                                                                           startsWithJ.and(lengthGreaterThan3);
        
        List<String> filteredNames = names.stream()
                                   .filter(startsWithJAndLengthGreaterThan3)
                                   .collect(Collectors.toList());
        System.out.println("Names starting with J and length > 3: " + filteredNames);
    }
}

Custom Objects

Predicates can be used to filter custom objects.

This example shows the creation of multiple Predicates for different criteria and how they can be combined for more complex filtering operations.

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

class Person {
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() { return name; }
    public int getAge() { return age; }
    
    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

public class CustomObjectFilteringExample {
    public static void main(String[] args) {
        List<Person> people = Arrays.asList(
            new Person("Alice", 25),
            new Person("Bob", 30),
            new Person("Charlie", 35),
            new Person("David", 40),
            new Person("Eve", 45)
        );
        
        // Predicate to filter adults (age >= 18)
        Predicate<Person> isAdult = person -> person.getAge() >= 18;
        
        // Predicate to filter seniors (age >= 60)
        Predicate<Person> isSenior = person -> person.getAge() >= 60;
        
        // Predicate to filter people with names starting with 'A' or 'B'
        Predicate<Person> nameStartsWithAOrB = person -> 
            person.getName().startsWith("A") ||
                   person.getName().startsWith("B");
        
        // Combining predicates
        Predicate<Person> isAdultWithNameStartingAOrB = 
                          isAdult.and(nameStartsWithAOrB);
        
        List<Person> filteredPeople = people.stream()
                                      .filter(isAdultWithNameStartingAOrB)
                                      .collect(Collectors.toList());
        
        System.out.println("Adults with names starting with A or B: " + filteredPeople);
        
        // Using negate() to find non-seniors
        List<Person> nonSeniors = people.stream()
                                        .filter(isSenior.negate())
                                        .collect(Collectors.toList());
        
        System.out.println("Non-seniors: " + nonSeniors);
    }
}

Conclusion

Predicates provide a powerful and flexible way to express boolean-valued functions, enabling more concise and expressive code, particularly when working with collections and the Stream API.

Effective use of Predicates can lead to more maintainable, readable, and efficient code. They are particularly valuable in data processing tasks, validation logic, and anywhere complex boolean conditions need to be expressed and manipulated.

As Java continues to evolve, Predicates remain a fundamental part of its functional programming toolkit. Understanding and leveraging Predicates can significantly enhance a Java developer’s ability to write clean, efficient, and expressive code.

Leave a Comment

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

Scroll to Top