Stop Writing Verbose Code: A Practical Guide to java.util.function

Introduction

In our journey learning about functional programming in Java, we have explored the building blocks:

Now we’re going to explore thejava.util.function package – A powerful toolkit that provides 40+ specialized, ready-to-use interfaces for common programming tasks.

Understanding the java.util.function Package

The java.util.function package has many interfaces, more than 40, and understanding them all is very difficult. But there’s a method I always use: divide and conquer.

If you group its interfaces into a few clear categories it will be easy:

Common Functional Interfaces

This group contains the four main interfaces of the package. If you understand the concept behind these four interfaces, you will easily understand the others. Each one has a single job:

  • Function<T, R> — Transforms a value of type T into a value of type R
  • Consumer<T> — Receives a value of type T and does something with it (no return)
  • Supplier<T> — Takes nothing and returns a value of type T
  • Predicate<T> — Receives a value of type T and returns a boolean (true or false)

1. Function<T, R> Interface — (Transforms)

The Function<T, R> interface represents a function that accepts one argument and produces a result. The T is the type of the input and R is the type of the result. Think of it as a transformation machine: you put something in, and you get something different out.

package java.util.function;

import java.util.Objects;

@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);
 
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }
 
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }
 
    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

This interface has the following methods:

  • R apply(T t) – The abstract method of the interface is responsible for transforming the parameter T into a result R.
  • default <V> Function<V, R> compose(Function<? super V, ? extends T> before) – This default method receives a function and executes it before the apply method.
  • default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) – This default method receives a function and executes it after the apply method.
  • static <T> Function<T, T> identity() – This static method returns a function that always returns its input argument unchanged.

Here is a practical example:

import java.util.function.Function;

/**
 * Practical example of Function<T, R>
 * Demonstrating apply(), andThen() and compose()
 */
public class FunctionExample {
    public static void main(String[] args) {

        // A function that converts a String to its length (String -> Integer)
        Function<String, Integer> getLength = s -> s.length();

        // A function that doubles an integer (Integer -> Integer)
        Function<Integer, Integer> doubleIt = n -> n * 2;

        // apply(): basic transformation
        System.out.println(getLength.apply("Hello"));        // 5

        // andThen(): getLength first, then doubleIt
        Function<String, Integer> getLengthThenDouble = getLength.andThen(doubleIt);
        System.out.println(getLengthThenDouble.apply("Hello")); // 10

        // compose(): doubleIt first, then... wait, types must match
        // Here: tripleIt runs before doubleIt
        Function<Integer, Integer> tripleIt = n -> n * 3;
        Function<Integer, Integer> tripleAndDouble = doubleIt.compose(tripleIt);
        System.out.println(tripleAndDouble.apply(5)); // (5 * 3) * 2 = 30
    }
}

2. Consumer<T> Interface — (Uses)

The Consumer<T> interface represents an operation that accepts a single input and returns no result. Unlike Function, it doesn’t transform — it consumes. Think of it as a printer: you hand it something, it does its job, and nothing comes back.

package java.util.function;
 
import java.util.Objects;
 
@FunctionalInterface
public interface Consumer<T> {
 
    void accept(T t);
 
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

This interface has the following methods:

  • void accept(T t) – The abstract method. Receives the element T and performs the operation. It returns nothing.
  • default Consumer<T> andThen(Consumer<? super T> after) – Chains two consumers together. The current consumer runs first, then the after consumer runs on the same element. Both consumers receive the same input.

Here is a practical example:

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

/**
 * Practical example of Consumer<T>
 * Demonstrating accept() and andThen()
 */
public class ConsumerExample {
    public static void main(String[] args) {

        // A Consumer that prints a name
        Consumer<String> printName = name -> System.out.println("Name: " + name);

        // A Consumer that prints the name in uppercase
        Consumer<String> printUpper = name -> System.out.println("Upper: " + name.toUpperCase());

        // accept(): basic usage
        printName.accept("Evandro"); // Name: Evandro

        // andThen(): chain two consumers — both run on the same input
        Consumer<String> printBoth = printName.andThen(printUpper);
        printBoth.accept("Evandro");
        // Name: Evandro
        // Upper: EVANDRO

        // Real-world usage: iterating a list with forEach
        List<String> languages = List.of("Java", "Python", "Go");
        languages.forEach(printName); // forEach accepts a Consumer
    }
}

3. Supplier<T> Interface — (Creates)

The Supplier<T> is the exact opposite of Consumer. It takes nothing and returns something. Think of it as a factory or a vending machine: you press the button, you get a value back, without giving anything.

package java.util.function;

@FunctionalInterface
public interface Supplier<T> {

    T get();
}

This interface has only one method:

  • T get() – The abstract method. Takes no arguments and returns a value of type T. This is the simplest interface in the package.

Here is a practical example:

import java.util.function.Supplier;
import java.time.LocalDate;
import java.util.UUID;

/**
 * Practical example of Supplier<T>
 * Demonstrating get() with different return types
 */
public class SupplierExample {
    public static void main(String[] args) {

        // A Supplier that returns a greeting message
        Supplier<String> greeting = () -> "Hello, Functional World!";

        // A Supplier using a Method Reference — no arguments, returns a value
        Supplier<LocalDate> today = LocalDate::now;

        // A Supplier that generates a unique ID each time it is called
        Supplier<String> idGenerator = () -> UUID.randomUUID().toString();

        System.out.println(greeting.get());           // Hello, Functional World!
        System.out.println("Today is: " + today.get()); // Today is: 2026-07-16
        System.out.println("ID: " + idGenerator.get()); // ID: a4f3c2d1-...
        System.out.println("ID: " + idGenerator.get()); // ID: b9e1a3f2-... (different each time)
    }
}

4. Predicate<T> Interface — (Tests)

The Predicate<T> is the interface for conditions and validations. It receives a value and returns a boolean — true or false. Think of it as a filter or a question: you pass something in, and it answers “yes” or “no.”

package java.util.function;

import java.util.Objects;

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);

    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    static <T> Predicate<T> not(Predicate<? super T> target) {
        Objects.requireNonNull(target);
        return (Predicate<T>) target.negate();
    }
}

This interface has the following methods:

  • boolean test(T t) – The abstract method. Tests whether the given argument satisfies the condition and returns true or false.
  • default Predicate<T> and(Predicate<? super T> other) – Combines two predicates with AND logic. Returns true only if both predicates return true.
  • default Predicate<T> negate() – Returns the logical negation of this predicate. If the original returns true, this returns false, and vice versa.
  • default Predicate<T> or(Predicate<? super T> other) – Combines two predicates with OR logic. Returns true if at least one predicate returns true.
  • static <T> Predicate<T> not(Predicate<? super T> target) – A static convenience method that negates the given predicate. Useful for method references like Predicate.not(String::isBlank).

Here is a practical example:

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

/**
 * Practical example of Predicate<T>
 * Demonstrating test(), and(), negate(), or(), and Predicate.not()
 */
public class PredicateExample {
    public static void main(String[] args) {

        Predicate<Integer> isPositive = n -> n > 0;
        Predicate<Integer> isEven = n -> n % 2 == 0;

        List<Integer> numbers = List.of(-4, -1, 0, 3, 6, 7, 10);

        // test(): basic usage
        System.out.println(isPositive.test(5));   // true
        System.out.println(isPositive.test(-3));  // false

        // and(): both conditions must be true
        System.out.println("Positive AND even:");
        numbers.stream()
               .filter(isPositive.and(isEven))
               .forEach(System.out::println); // 6, 10

        // negate(): inverts the condition
        System.out.println("NOT positive (zero or negative):");
        numbers.stream()
               .filter(isPositive.negate())
               .forEach(System.out::println); // -4, -1, 0

        // or(): at least one condition must be true
        System.out.println("Positive OR even:");
        numbers.stream()
               .filter(isPositive.or(isEven))
               .forEach(System.out::println); // -4, 3, 6, 7, 10

        // Predicate.not(): great with method references
        List<String> names = List.of("Java", "", "  ", "Spring", "");
        System.out.println("Non-blank names:");
        names.stream()
             .filter(Predicate.not(String::isBlank))
             .forEach(System.out::println); // Java, Spring
    }
}

Putting It All Together

Now that you know each interface individually, here’s the best way to remember all four. Each one answers a different question:

InterfaceInputOutputAbstract MethodThink of it as…
Function<T, R>TRapply(T t)Transforms
Consumer<T>Tvoidaccept(T t)Uses
Supplier<T>noneTget()Creates
Predicate<T>Tbooleantest(T t)Tests

Now let’s combine all four in a single, realistic example:

import java.util.function.*;
import java.util.List;

/**
 * Real-world example combining all four core functional interfaces.
 * Scenario: process a product catalog — filter, transform, and display.
 */
public class FunctionalToolkitDemo {

    record Product(String name, double price, boolean inStock) {}

    public static void main(String[] args) {

        List<Product> catalog = List.of(
            new Product("Java Book",   49.99, true),
            new Product("Keyboard",   129.99, false),
            new Product("Mouse",       39.99, true),
            new Product("Monitor",    299.99, true)
        );

        // Supplier: provides the header message (creates something from nothing)
        Supplier<String> header = () -> "=== Available Products Under $100 ===";

        // Predicate: tests if a product is available AND affordable
        Predicate<Product> isAvailable  = p -> p.inStock();
        Predicate<Product> isAffordable = p -> p.price() < 100;

        // Function: transforms a Product into a formatted String
        Function<Product, String> format = p ->
            String.format("%-15s $%.2f", p.name(), p.price());

        // Consumer: prints each formatted string with a bullet point
        Consumer<String> printItem = item -> System.out.println("  → " + item);

        // Putting it all together
        System.out.println(header.get()); // Supplier in action

        catalog.stream()
               .filter(isAvailable.and(isAffordable)) // Predicate in action
               .map(format)                            // Function in action
               .forEach(printItem);                   // Consumer in action
    }
}

// Output:
// === Available Products Under $100 ===
//   → Java Book       $49.99
//   → Mouse           $39.99

What’s Next? The Stream API

Throughout this post, you’ve seen all four core functional interfaces working together. But notice something: in the final example, we used .stream(), .filter(), .map(), and .forEach().

That’s not a coincidence. The Stream API is the place where Function, Consumer, Supplier, and Predicate truly shine:

  • .filter() accepts a Predicate
  • .map() accepts a Function
  • .forEach() accepts a Consumer
  • .generate() accepts a Supplier

In the next post, we’ll explore the Stream API in detail — and you’ll see exactly how these functional interfaces become the engine of powerful, declarative data processing pipelines.

Conclusion

The java.util.function package gives you four essential tools that cover the vast majority of your functional programming needs. Once you internalize the mental model, choosing the right interface becomes instinctive:

  • Need to transform a value? Use Function<T, R>.
  • Need to do something with a value but not return anything? Use Consumer<T>.
  • Need to produce a value without receiving anything? Use Supplier<T>.
  • Need to test a condition? Use Predicate<T>.

Master these four, and the rest of the java.util.function package — BiFunction, UnaryOperator, BinaryOperator, and all the primitive specializations — will make complete sense. They are all just variations of these same four ideas.

Deixe um comentário