Skip to Content
Course content

69: Built-In Functional Interfaces: Function, Predicate, Supplier

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this happen a dozen times with developers moving from traditional imperative Java to a more functional style. They try to build a flexible filtering system, but they end up fighting the compiler for an hour because they're treating every lambda like a generic "chunk of code" rather than a specific type.

public class OrderService {
    public void processOrder(Order order, Predicate<Order> validationRule) {
        if (validationRule.test(order)) {
            System.out.println("Order is valid. Processing...");
        } else {
            System.out.println("Order failed validation.");
        }
    }

    public static void main(String[] args) {
        Order myOrder = new Order(101, 50.00);
        OrderService service = new OrderService();

        // BUG: This line won't compile
        service.processOrder(myOrder, o -> o.getTotalAmount()); 
    }
}

The 'Incompatible Types' Headache

If you try to compile the code above, Java will throw a fit. It'll tell you that it found a double (from getTotalAmount()) but it required a boolean. The mistake here is a conceptual one: the developer wanted to "check the amount," but they passed a transformer (something that extracts a value) into a slot designed for a filter (something that returns true/false).

A Predicate is a very specific contract. It says: "Give me an object, and I will tell you true or false." By passing o -> o.getTotalAmount(), you aren't providing a true/false answer; you're just handing back a number. The compiler doesn't care that you *intend* to use that number for a check later; it only cares that the test() method of a Predicate must return a boolean.

The Fix: Matching the Lambda to the Interface

To fix this, we need to actually perform a comparison within the lambda so that the result is a boolean. This fulfills the Predicate contract.

// FIXED: Now it returns a boolean, satisfying the Predicate interface
service.processOrder(myOrder, o -> o.getTotalAmount() > 20.00);

Now, the lambda reads as: "Take an order o, check if the amount is greater than 20, and return that result." The compiler is happy because the types align.

Filtering Logic with Predicate

As you've seen, Predicate<T> is your go-to for any logic that asks a yes/no question. It has one primary abstract method: boolean test(T t). I use these constantly when I want to make my methods generic. Instead of writing five different methods like filterByPrice(), filterByDate(), and filterByStatus(), I write one method that accepts a Predicate and let the caller decide the logic.

Transforming Data with Function

While a Predicate filters, a Function<T, R> transforms. It takes an input of type T and returns a result of type R. Its main method is R apply(T t).

Think of this as a pipe. You put a User object in, and you get a String (like their email) out. If you're extracting data or converting one object type to another (like a Database Entity to a DTO), this is the interface you want.

// A function that takes an Order and returns its Order ID as a String
Function<Order, String> idExtractor = o -> "ORD-" + o.getId();
String label = idExtractor.apply(myOrder);

Generating Values with Supplier

Finally, we have the Supplier<T>. This is the simplest of the three because it takes absolutely no arguments but returns a value of type T. Its method is T get().

I usually use Suppliers for "lazy initialization" or factory-style logic. If you have an expensive object to create, you don't want to create it until the exact moment it's actually needed. A Supplier wraps that creation logic in a lambda, delaying the execution until you call .get().

// A supplier that provides a new timestamp every time it's called
Supplier<LocalDateTime> now = () -> LocalDateTime.now();
System.out.println(now.get());

Choosing the Right Tool

  • Need a boolean? Use Predicate<T>.
  • Need to change A into B? Use Function<T, R>.
  • Need to create or provide something? Use Supplier<T>.



📋 Practical Task

Building a Dynamic Product Discount Engine

You are tasked with creating a DiscountManager that can handle various business rules without hard-coding them. You need to implement a system that filters eligible products, calculates a discounted price, and generates a log message for the transaction.

Requirements:

  • Create a Product class with name (String), price (double), and category (String).
  • Create a method applyDiscount in your DiscountManager class that takes:
    1. A Product object.
    2. A Predicate<Product> to determine if the product is eligible for a discount.
    3. A Function<Double, Double> to calculate the new price based on the old price.
    4. A Supplier<String> to provide a custom log prefix (e.g., "PROMO2023: ").
  • The applyDiscount method should:
    • Check if the product is eligible using the predicate.
    • If eligible, use the function to calculate the new price and print: "[Prefix] Product [Name] discounted to [New Price]".
    • If not eligible, print: "[Prefix] Product [Name] is not eligible for discount."

Test Scenario: Apply a 10% discount only to products in the "Electronics" category, using "SALE_EVENT" as the log prefix.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.