Functional Interfaces in Java

An interface with exactly one abstract method can be implemented by a lambda, and the standard library supplies the ones you need most.

Definition

A functional interface has exactly one abstract method. That single method is the target a lambda or method reference implements.

@FunctionalInterface
public interface Validator {
    boolean isValid(String value);
}
Validator notBlank = value -> value != null && !value.isBlank();
Validator isEmail = value -> value.contains("@");

System.out.println(notBlank.isValid("  "));    // false

The annotation is optional but valuable: it makes the intent explicit and turns adding a second abstract method into a compile error.

Default and static methods do not count

@FunctionalInterface
public interface Validator {

    boolean isValid(String value);                 // the one abstract method

    default Validator and(Validator other) {       // default, allowed
        return value -> isValid(value) && other.isValid(value);
    }

    static Validator alwaysValid() {               // static, allowed
        return value -> true;
    }
}
Validator strict = notBlank.and(isEmail);
System.out.println(strict.isValid("a@b.com"));   // true

Methods inherited from Object, such as equals, also do not count towards the total.

The built in interfaces

InterfaceMethodTakesReturnsUse for
Predicate<T>testTbooleanA condition
Function<T,R>applyTRA transformation
Consumer<T>acceptTnothingA side effect
Supplier<T>getnothingTProducing a value
UnaryOperator<T>applyTTSame type transformation
BinaryOperator<T>applyT, TTCombining two values
BiFunction<T,U,R>applyT, URTwo inputs
BiPredicate<T,U>testT, UbooleanA two argument condition
BiConsumer<T,U>acceptT, UnothingMap iteration
RunnablerunnothingnothingA task
Callable<V>callnothingV, may throwA task with a result

Predicate

Predicate<String> notBlank = value -> !value.isBlank();
Predicate<String> isLong = value -> value.length() > 10;

System.out.println(notBlank.and(isLong).test("a long enough value"));   // true
System.out.println(notBlank.or(isLong).test("hi"));                     // true
System.out.println(notBlank.negate().test(""));                         // true
System.out.println(Predicate.not(String::isBlank).test("x"));           // true

Function

Function<String, Integer> length = String::length;
Function<Integer, String> describe = n -> n + " characters";

System.out.println(length.andThen(describe).apply("java"));   // 4 characters
System.out.println(describe.compose(length).apply("java"));   // the same
System.out.println(Function.<String>identity().apply("x"));   // x

andThen runs this function first; compose runs the argument first. Reading them out loud settles the order every time.

Consumer and Supplier

Consumer<String> print = System.out::println;
Consumer<String> log = value -> logger.info(value);
print.andThen(log).accept("saved");        // both run, in order

Supplier<List<String>> empty = ArrayList::new;
Supplier<Double> random = Math::random;

A Supplier is how laziness is expressed. Optional.orElseGet(supplier) and Map.computeIfAbsent both take one so the value is produced only when it is actually needed.

Operators

UnaryOperator<String> trim = String::strip;
BinaryOperator<Integer> add = Integer::sum;

List<String> values = new ArrayList<>(List.of(" a ", " b "));
values.replaceAll(trim);                    // takes a UnaryOperator

int total = Stream.of(1, 2, 3).reduce(0, add);   // takes a BinaryOperator

Primitive versions

IntPredicate isEven = n -> n % 2 == 0;             // no Integer boxing
IntFunction<String> describe = n -> "value " + n;
ToIntFunction<String> parse = Integer::parseInt;
IntUnaryOperator doubled = n -> n * 2;
IntSupplier constant = () -> 7;

There is a primitive variant for int, long and double across the whole family. In a loop over millions of values these avoid a boxing allocation on every step.

Writing your own

@FunctionalInterface
public interface NoteTransformer {
    Note transform(Note note);

    default NoteTransformer then(NoteTransformer next) {
        return note -> next.transform(transform(note));
    }
}
NoteTransformer pipeline = ((NoteTransformer) Note::stripHtml)
        .then(Note::normaliseTitle)
        .then(Note::addSummary);

Define your own only when the built in ones do not express the domain. NoteTransformer reads better than UnaryOperator<Note> at a call site, and it can carry domain specific default methods.

Common mistakes

  • Adding a second abstract method and breaking every lambda that used the interface.
  • Creating a custom interface where Function or Predicate already fits.
  • Using boxed interfaces in hot loops instead of the primitive variants.
  • Expecting the built in interfaces to allow checked exceptions. They do not.
  • Confusing andThen with compose.

Best practices

  • Annotate custom functional interfaces with @FunctionalInterface.
  • Prefer the standard interfaces so your API is instantly familiar.
  • Define your own when the domain name genuinely adds clarity.
  • Use the primitive variants where performance matters.
  • Compose with and, or, negate and andThen rather than nesting lambdas by hand.

Practice

  1. Write a Predicate<String> that accepts values between 3 and 20 characters, built from two composed predicates.
  2. Why does adding a second abstract method break existing lambdas?
  3. Chain three Function objects to trim, lower case and then measure a string.
  4. Which built in interface fits: producing a default configuration, logging a value, converting a note to JSON, testing whether a note is published?
  5. Rewrite a boxed Predicate<Integer> as an IntPredicate and explain the gain.

Conclusion

A functional interface is one abstract method with a name. Learn the six core shapes, use the primitive variants when it matters, compose rather than nest, and define your own only when the domain deserves its own vocabulary.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →
Java

Introduction to Java

Java is a statically typed, object oriented language that compiles to bytecode and runs on a virtual machine, which is what makes it portable.

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.