Functional Interfaces

Back

Loading concept...

🏭 The Magic Factory: Java’s Functional Interfaces

Imagine you own a magic factory. This factory has special machines—each one does ONE specific job. You don’t care HOW the machine works inside. You just know: put something in, get something out.

That’s exactly what Functional Interfaces are in Java! They’re like blueprints for these single-purpose machines.


🎯 What is a Functional Interface?

A Functional Interface is an interface with exactly ONE abstract method. Think of it as a machine with ONE button that does ONE thing.

@FunctionalInterface
interface Greeter {
    void sayHello(String name);
}

The @FunctionalInterface annotation is like a label saying: “This machine has only ONE job!”


📦 The java.util.function Package

Java comes with a toolbox of ready-made functional interfaces. No need to build your own machines—Java already made them for you!

This toolbox lives in java.util.function package. Let’s meet the heroes inside!

graph TD A["java.util.function"] --> B["Predicate"] A --> C["Function"] A --> D["Consumer"] A --> E["Supplier"] A --> F["BiFunction & BiConsumer"] A --> G["Operators"]

🔍 Predicate Interface - The Yes/No Machine

The Story

Imagine a security guard at a party. His ONE job: look at each person and say YES (you can enter) or NO (go away).

That’s a Predicate! It takes something, checks it, and returns true or false.

The Blueprint

@FunctionalInterface
interface Predicate<T> {
    boolean test(T t);
}

Real Example

Predicate<Integer> isAdult = age -> age >= 18;

System.out.println(isAdult.test(25)); // true
System.out.println(isAdult.test(10)); // false

Combining Predicates

Predicates can team up!

Predicate<Integer> isAdult = age -> age >= 18;
Predicate<Integer> isSenior = age -> age >= 65;

// AND - both must be true
Predicate<Integer> adultNotSenior =
    isAdult.and(isSenior.negate());

// OR - at least one true
Predicate<Integer> specialGroup =
    isAdult.negate().or(isSenior);
Method What it does
test(T t) Check and return true/false
and(Predicate) Both must pass
or(Predicate) At least one passes
negate() Flip the result

🔄 Function Interface - The Transformer Machine

The Story

Remember those toy machines where you put in a coin and get a different toy? Or a translator who hears English and speaks Spanish?

That’s a Function! It takes ONE thing and transforms it into ANOTHER thing.

The Blueprint

@FunctionalInterface
interface Function<T, R> {
    R apply(T t);
}
  • T = what goes IN (Type)
  • R = what comes OUT (Result)

Real Example

Function<String, Integer> stringLength =
    str -> str.length();

System.out.println(stringLength.apply("Hello"));
// Output: 5

Function<Integer, String> numberToWord = num -> {
    if (num == 1) return "One";
    if (num == 2) return "Two";
    return "Many";
};

System.out.println(numberToWord.apply(2));
// Output: Two

Chaining Functions

Functions can form an assembly line!

Function<String, String> trim = s -> s.trim();
Function<String, String> upper = s -> s.toUpperCase();
Function<String, Integer> length = s -> s.length();

// Chain them together
Function<String, Integer> pipeline =
    trim.andThen(upper).andThen(length);

System.out.println(pipeline.apply("  hello  "));
// Output: 5

🍽️ Consumer Interface - The Hungry Machine

The Story

Think of a shredder. You feed it paper, and it… eats it. Nothing comes back out. It just CONSUMES.

Or a printer: you give it a document, it prints—but the printer itself doesn’t hand you anything back (the paper comes from inside!).

A Consumer takes something and does something with it. Returns nothing.

The Blueprint

@FunctionalInterface
interface Consumer<T> {
    void accept(T t);
}

Real Example

Consumer<String> printer = msg ->
    System.out.println(msg);

printer.accept("Hello World!");
// Prints: Hello World!

Consumer<List<String>> addGreeting = list ->
    list.add("Welcome!");

List<String> messages = new ArrayList<>();
addGreeting.accept(messages);
// messages now contains "Welcome!"

Chaining Consumers

Consumer<String> print = s ->
    System.out.println(s);
Consumer<String> shout = s ->
    System.out.println(s.toUpperCase() + "!");

Consumer<String> both = print.andThen(shout);
both.accept("hello");
// Prints:
// hello
// HELLO!

🎁 Supplier Interface - The Gift Machine

The Story

Imagine a gumball machine. You don’t put anything in (okay, maybe a coin). You just press the button and OUT comes a gumball!

A Supplier gives you something without needing input. It’s a factory that creates things on demand.

The Blueprint

@FunctionalInterface
interface Supplier<T> {
    T get();
}

Real Example

Supplier<Double> randomNumber = () -> Math.random();

System.out.println(randomNumber.get());
// Output: 0.7234... (random!)

Supplier<LocalDateTime> currentTime =
    () -> LocalDateTime.now();

System.out.println(currentTime.get());
// Output: 2024-01-15T14:30:00

Supplier<List<String>> emptyList =
    () -> new ArrayList<>();

List<String> newList = emptyList.get();

Why Use Suppliers?

  • Lazy creation: Create objects only when needed
  • Factory pattern: Generate new instances on demand
  • Default values: Provide fallback values

👯 BiFunction and BiConsumer - The Twin Machines

The Story

What if your machine needs TWO inputs instead of one? Like a blender that needs fruit AND milk to make a smoothie?

Enter the Bi (meaning “two”) versions!

BiFunction Blueprint

@FunctionalInterface
interface BiFunction<T, U, R> {
    R apply(T t, U u);
}
  • Takes TWO inputs (T and U)
  • Returns ONE output (R)

BiFunction Example

BiFunction<String, String, String> combine =
    (a, b) -> a + " " + b;

System.out.println(combine.apply("Hello", "World"));
// Output: Hello World

BiFunction<Integer, Integer, Integer> add =
    (a, b) -> a + b;

System.out.println(add.apply(5, 3));
// Output: 8

BiFunction<String, Integer, String> repeat =
    (str, times) -> str.repeat(times);

System.out.println(repeat.apply("Hi", 3));
// Output: HiHiHi

BiConsumer Blueprint

@FunctionalInterface
interface BiConsumer<T, U> {
    void accept(T t, U u);
}

BiConsumer Example

BiConsumer<String, Integer> printTimes =
    (msg, n) -> {
        for (int i = 0; i < n; i++) {
            System.out.println(msg);
        }
    };

printTimes.accept("Hooray!", 3);
// Prints Hooray! three times

BiConsumer<Map<String, Integer>, String> addEntry =
    (map, key) -> map.put(key, key.length());

Map<String, Integer> wordLengths = new HashMap<>();
addEntry.accept(wordLengths, "Hello");
// map now has {"Hello": 5}

⚙️ Unary and Binary Operators

The Story

Sometimes your machine takes something and gives back THE SAME TYPE of thing. Like a toaster: bread goes in, bread (toasted!) comes out. Still bread.

These special cases have their own names!

UnaryOperator - Same Type In and Out

@FunctionalInterface
interface UnaryOperator<T> extends Function<T, T> {
    T apply(T t);
}

It’s a Function where input and output are the SAME type.

UnaryOperator<Integer> doubleIt = n -> n * 2;
System.out.println(doubleIt.apply(5));
// Output: 10

UnaryOperator<String> addExcitement = s -> s + "!";
System.out.println(addExcitement.apply("Wow"));
// Output: Wow!

UnaryOperator<List<Integer>> shuffle = list -> {
    Collections.shuffle(list);
    return list;
};

BinaryOperator - Two Same-Type Inputs

@FunctionalInterface
interface BinaryOperator<T> extends BiFunction<T, T, T> {
    T apply(T t1, T t2);
}

It’s a BiFunction where BOTH inputs and output are the SAME type.

BinaryOperator<Integer> multiply = (a, b) -> a * b;
System.out.println(multiply.apply(4, 5));
// Output: 20

BinaryOperator<String> longest = (a, b) ->
    a.length() >= b.length() ? a : b;
System.out.println(longest.apply("cat", "elephant"));
// Output: elephant

BinaryOperator<Integer> max = Integer::max;
System.out.println(max.apply(10, 25));
// Output: 25

🗺️ The Complete Family Tree

graph TD A["Functional Interfaces"] --> B["Takes Input?"] B -->|No| C["Supplier&lt;br/&gt;Returns something"] B -->|Yes| D["How many inputs?"] D -->|One| E["Returns something?"] D -->|Two| F["Returns something?"] E -->|Yes| G["Function&lt;br/&gt;T → R"] E -->|No| H["Consumer&lt;br/&gt;T → void"] E -->|Yes, same type| I["UnaryOperator&lt;br/&gt;T → T"] F -->|Yes| J["BiFunction&lt;br/&gt;T,U → R"] F -->|No| K["BiConsumer&lt;br/&gt;T,U → void"] F -->|Yes, same type| L["BinaryOperator&lt;br/&gt;T,T → T"] A --> M["Returns boolean?"] M -->|Yes| N["Predicate&lt;br/&gt;T → boolean"]

🎯 Quick Decision Guide

I want to… Use this
Check if something is true/false Predicate<T>
Transform one thing to another Function<T,R>
Do something with a value (no return) Consumer<T>
Get a value from nothing Supplier<T>
Combine two values into one BiFunction<T,U,R>
Use two values (no return) BiConsumer<T,U>
Transform to same type UnaryOperator<T>
Combine two same-type values BinaryOperator<T>

🌟 Real-World Power

These interfaces shine when used with Streams and Collections:

List<String> names = Arrays.asList(
    "Alice", "Bob", "Charlie", "David"
);

// Predicate - filter
names.stream()
    .filter(name -> name.length() > 4)
    .forEach(System.out::println);
// Output: Alice, Charlie, David

// Function - transform
names.stream()
    .map(String::toUpperCase)
    .forEach(System.out::println);
// Output: ALICE, BOB, CHARLIE, DAVID

// Consumer - perform action
names.forEach(name ->
    System.out.println("Hello, " + name));

💡 Remember This!

  1. Predicate = Security Guard (Yes/No)
  2. Function = Translator (Transform)
  3. Consumer = Shredder (Eat, no return)
  4. Supplier = Gumball Machine (Give without input)
  5. Bi-versions = Need TWO inputs
  6. Operators = Same type in and out

You now have the keys to the magic factory! These simple machines, when combined, can build incredibly powerful programs.

Happy coding! 🚀

Loading story...

Story - Premium Content

Please sign in to view this story and start learning.

Upgrade to Premium to unlock full access to all stories.

Stay Tuned!

Story is coming soon.

Story Preview

Story - Premium Content

Please sign in to view this concept and start learning.

Upgrade to Premium to unlock full access to all content.