While Java 8 provides many built-in functional interfaces in the java.util.function
package (like Function
, Predicate
, Consumer
, and Supplier
), there are times when these don’t match your specific use case. In such cases, you can create your own custom functional interfaces tailored to your needs.
A functional interface is simply an interface with a single abstract method (SAM). These interfaces can be used as targets for lambda expressions and method references, enabling clean and expressive code.
@FunctionalInterface
AnnotationJava provides the @FunctionalInterface
annotation to explicitly mark an interface as functional. This annotation is optional but recommended—it tells the compiler to enforce that the interface contains only one abstract method. If you accidentally add a second abstract method, the compiler will raise an error, helping prevent mistakes.
Let’s create a functional interface for a custom operation that takes two integers and returns a string:
@FunctionalInterface
interface IntToStringOperation {
String apply(int a, int b);
// Optional: default method
default void printExample() {
System.out.println("This interface converts two ints into a string.");
}
// Optional: static method
static void showInfo() {
System.out.println("IntToStringOperation is a custom functional interface.");
}
}
This interface has one abstract method, apply
, and optional default
and static
methods for extended functionality.
public class CustomFunctionalInterfaceDemo {
public static void main(String[] args) {
IntToStringOperation concat = (a, b) -> "Combined: " + a + b;
System.out.println(concat.apply(10, 20)); // Output: Combined: 1020
concat.printExample();
IntToStringOperation.showInfo();
}
}
@FunctionalInterface
interface IntToStringOperation {
String apply(int a, int b);
// Optional: default method
default void printExample() {
System.out.println("This interface converts two ints into a string.");
}
// Optional: static method
static void showInfo() {
System.out.println("IntToStringOperation is a custom functional interface.");
}
}
public class CustomFunctionalInterfaceDemo {
public static void main(String[] args) {
IntToStringOperation concat = (a, b) -> "Combined: " + a + b;
System.out.println(concat.apply(10, 20)); // Output: Combined: 1020
concat.printExample(); // Default method
IntToStringOperation.showInfo(); // Static method
}
}
You can also define interfaces with no parameters:
@FunctionalInterface
interface MessageProvider {
String getMessage();
}
public class MessageExample {
public static void main(String[] args) {
MessageProvider provider = () -> "Hello from a custom interface!";
System.out.println(provider.getMessage());
}
}
MessageProvider
is clearer than Supplier<String>
).Creating your own functional interfaces allows for expressive, type-safe, and reusable functional constructs tailored to your application’s needs.
Prior to Java 8, interfaces could only contain abstract methods, meaning every implementing class had to provide the method’s behavior. Java 8 introduced default and static methods in interfaces, bringing more flexibility and power—especially in the context of functional programming.
A default method provides a method implementation directly within the interface using the default
keyword. This allows interfaces to evolve without breaking existing implementations. Default methods are especially useful in functional interfaces, where they can offer reusable behaviors or compose functions.
Example 1: Default Method for Composition
@FunctionalInterface
interface Formatter {
String format(String input);
// Compose uppercase formatting with prefix
default Formatter withPrefix(String prefix) {
return (s) -> prefix + format(s);
}
}
public class DefaultMethodExample {
public static void main(String[] args) {
Formatter upperCase = s -> s.toUpperCase();
Formatter withGreeting = upperCase.withPrefix("Hello, ");
System.out.println(withGreeting.format("world")); // Output: Hello, WORLD
}
}
In this example, withPrefix
is a default method that returns a new composed Formatter
.
Static methods in interfaces are utility methods related to the interface’s behavior. These can be called without an instance and are often used as factory or helper methods.
Example 2: Static Factory Method in Functional Interface
@FunctionalInterface
interface MathOperation {
int operate(int a, int b);
static MathOperation multiply() {
return (a, b) -> a * b;
}
}
public class StaticMethodExample {
public static void main(String[] args) {
MathOperation op = MathOperation.multiply();
System.out.println(op.operate(4, 5)); // Output: 20
}
}
Default and static methods work well together. You can create reusable, composable, and testable patterns using both.
Example 3: Combining Defaults and Statics
@FunctionalInterface
interface Validator {
boolean isValid(String value);
default Validator and(Validator other) {
return s -> this.isValid(s) && other.isValid(s);
}
static Validator notEmpty() {
return s -> s != null && !s.isEmpty();
}
}
public class ValidatorExample {
public static void main(String[] args) {
Validator validator = Validator.notEmpty().and(s -> s.length() >= 3);
System.out.println(validator.isValid("abc")); // Output: true
System.out.println(validator.isValid("")); // Output: false
}
}
Summary: Default methods support behavior sharing and composition without forcing all implementations to override them, while static methods offer reusable helpers. Together, they enrich functional interfaces with utility, composability, and backwards compatibility.
One of the key strengths of functional programming is the ability to compose small, reusable functions to build more complex behavior. Java’s functional interfaces like Function
and Predicate
provide built-in methods such as andThen
, compose
, and
, or
, and negate
to support this composition. These methods allow chaining and combining logic in a clear and expressive way.
The Function<T, R>
interface provides two important methods:
andThen(Function after)
: Executes the current function first, then passes the result to the after
function.compose(Function before)
: Executes the before
function first, then passes the result to the current function.This is useful for building data transformation pipelines.
Example 1: Chaining Functions
import java.util.function.Function;
public class FunctionCompositionExample {
public static void main(String[] args) {
Function<String, String> trim = String::trim;
Function<String, String> toUpper = String::toUpperCase;
Function<String, Integer> length = String::length;
// Compose: trim -> toUpper -> length
Function<String, Integer> composed = trim.andThen(toUpper).andThen(length);
System.out.println(composed.apply(" hello ")); // Output: 5
}
}
Here, the input string is trimmed, converted to uppercase, and its length is calculated—all using function composition.
The Predicate<T>
interface includes:
and(Predicate other)
: True if both predicates are true.or(Predicate other)
: True if either predicate is true.negate()
: Inverts the result of the predicate.These allow building complex conditions from simpler tests.
Example 2: Combining Predicates
import java.util.List;
import java.util.function.Predicate;
public class PredicateCombinationExample {
public static void main(String[] args) {
List<String> names = List.of("Alice", "", null, "Bob", " ", "Charlie");
Predicate<String> notNull = s -> s != null;
Predicate<String> notEmpty = s -> !s.isEmpty();
Predicate<String> notBlank = s -> !s.trim().isEmpty();
// Combined predicate to filter valid names
Predicate<String> isValid = notNull.and(notEmpty).and(notBlank);
names.stream()
.filter(isValid)
.forEach(System.out::println); // Output: Alice, Bob, Charlie
}
}
This example filters a list to remove null
, empty, or blank strings using predicate composition.
import java.util.function.Function;
import java.util.function.Predicate;
public class EmailValidation {
public static void main(String[] args) {
Function<String, String> normalize = email -> email.toLowerCase().trim();
Predicate<String> containsAt = email -> email.contains("@");
Predicate<String> validDomain = email -> email.endsWith(".com");
String rawEmail = " USER@Example.COM ";
String cleaned = normalize.apply(rawEmail);
boolean isValid = containsAt.and(validDomain).test(cleaned);
System.out.println("Normalized: " + cleaned); // Output: user@example.com
System.out.println("Valid email: " + isValid); // Output: true
}
}
Function and predicate composition allows you to:
By chaining behavior, your code becomes declarative, expressive, and easy to test—hallmarks of good functional programming in Java.
Functional programming in Java simplifies operations on collections by allowing us to declaratively express filtering, mapping, and processing using lambda expressions and functional interfaces. In this example, we'll work with a list of Person
objects. We'll filter out only adults (age ≥ 18), and then transform each Person
into a String
greeting message.
We’ll use:
Predicate<Person>
to filter.Function<Person, String>
to map.Stream
API to process the list.import java.util.*;
import java.util.function.*;
import java.util.stream.*;
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 CollectionFilterTransform {
public static void main(String[] args) {
List<Person> people = Arrays.asList(
new Person("Alice", 22),
new Person("Bob", 15),
new Person("Charlie", 30),
new Person("Daisy", 17)
);
// Predicate to filter adults
Predicate<Person> isAdult = p -> p.getAge() >= 18;
// Function to convert Person to greeting message
Function<Person, String> toGreeting = p -> "Hello, " + p.getName() + "!";
// Process: filter adults, transform to greeting strings
List<String> greetings = people.stream()
.filter(isAdult)
.map(toGreeting)
.collect(Collectors.toList());
// Output the result
greetings.forEach(System.out::println);
}
}
Hello, Alice!
Hello, Charlie!
Predicate<Person>
filters out anyone under 18.Function<Person, String>
maps a Person
to a String
greeting.p -> p.getAge() >= 18
) makes the logic concise.filter().map().collect()
processes the list declaratively.Comparator.comparing(Person::getAge)
.BiFunction
to customize greetings based on age..filter(p -> p.getName().startsWith("A"))
.This example highlights how Java’s functional programming model transforms verbose loops into clean, readable, and composable operations using predicates, functions, and the stream API.