In this article, we will learn Java Functional Interfaces which are coming by default in Java. These interfaces are; Supplier, Consumer, Predicate, Function, Runnable, and Callable. First of all, I highly suggest you use Java 8 and higher versions of Java to work with these interfaces. Currently, the latest LTS version is Java 17 and I will do these examples with Java 17 JDK. You can download the Java 17 JDK here and for installation please read here.
Java Functional Interfaces
Let’s start with the Supplier interface.
Supplier Interface
The Supplier Interface does not allow input, it returns a value based on a defined type. It is like a function without parameters, with a return type. Now, let’s see this behavior with examples.
public class SupplierInterface { //Supplier function declarations. Supplier<String> textSupplier = () -> "Hello SW Test Academy!"; Supplier<Integer> numberSupplier = () -> 1234; Supplier<Double> randomSupplier = () -> Math.random(); Supplier<Double> randomSupplierMR = Math::random; //With Method Reference (MR) @Test public void supplierTest() { //Calling Supplier functions. System.out.println(textSupplier.get()); System.out.println(numberSupplier.get()); System.out.println(randomSupplier.get()); System.out.println(randomSupplierMR.get()); } }
Output
Consumer Interface
The Consumer Interface takes an input, it does not return a value. It is like a function with a parameter, without a return type. BiConsumer Interface takes two inputs and does not return anything. That’s why it is called “Bi” Consumer. If we chain multiple consumers with andThen method, first the first consumer will be executed and the second one executed. It is working like left to right flow.
Now, it is an examples time for each case. :)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class ConsumerInterface { //Consumer function declarations. Consumer<String> upperCaseConsumer = (text) -> System.out.println(text.toUpperCase()); Consumer<String> lowerCaseConsumer = (text) -> System.out.println(text.toLowerCase()); Consumer<Double> logOfTenConsumer = (number) -> System.out.println(Math.log10(number)); //BiConsumer takes two parameters and does not return anything! BiConsumer<Integer, Integer> powConsumer = (base, power) -> System.out.println(Math.pow(base, power)); @BeforeEach public void setup(TestInfo testInfo) { System.out.println("Test name: " + testInfo.getDisplayName()); } @AfterEach public void tearDown(){ System.out.println(); } @Order(1) @Test public void consumerTest() { //Calling Consumer functions. upperCaseConsumer.accept("Hello SW Test Academy!"); lowerCaseConsumer.accept("Hello SW Test Academy!"); logOfTenConsumer.accept(1000.00); } @Order(2) @Test public void biConsumerTest() { //Calling BiConsumer function. powConsumer.accept(3,2); } @Order(3) @Test public void consumerChainTest() { //Consumer chaining with andThen method. upperCaseConsumer .andThen(lowerCaseConsumer) .accept("Hello SW Test Academy!"); } }
Output
Function Interface
The Function Interface takes an input, it returns a defined type. It is like a function with a parameter, with a return type. The first declaration is input, the second is the return type. The BiFunction Interface takes two inputs rather than one input. That’s the only difference between Function and BiFunction interfaces. Also, UnaryOperator interface takes and returns the same type.
If we chain the functions with andThen method, the execution order will be like the left to right flow. First, the first function will be run, then the others will be run. If we want to run these functions right to left we can use compose method rather than the andThen method.
Now, examples time to see all of these behaviors in action!
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class FunctionInterface { //FunctionInterface function declarations. (Input Type, Return Type) Function<String, String> toUpperCase = (text) -> text.toUpperCase(); Function<String, String> toLowerCase = (text) -> text.toLowerCase(); Function<Integer, Double> log10 = (number) -> Math.log10(number); //Method Reference Declarations (Input Type, Return Type) Function<String, String> toUpperCaseMR = String::toUpperCase; Function<String, String> toLowerCaseMR = String::toLowerCase; Function<Integer, Double> log10MR = Math::log10; //BiFunction Example (Input Type, Input Type, Return Type) BiFunction<Integer, Integer, Integer> powerOf = (base, power) -> (int) Math.pow(base, power); //UnaryOperator Example (Input and Return type are same.) UnaryOperator<String> appendText = (text) -> "I am appending: " + text; @BeforeEach public void setup(TestInfo testInfo) { System.out.println("Test name: " + testInfo.getDisplayName()); } @AfterEach public void tearDown(){ System.out.println(); } @Order(1) @Test public void functionTest() { //Calling functions. String upperCaseResult = toUpperCase.apply("hello sw test academy!"); Double log10Result = log10.apply(10000); System.out.println(upperCaseResult); System.out.println(log10Result); } @Order(2) @Test public void functionChainWithAndThen() { //Function chaining. First do the first function then do the second one. String chainResult1 = toUpperCase.andThen(toLowerCase).apply("heLLo sW teSt ACadEmy!"); String chainResult2 = toLowerCase.andThen(toUpperCase).apply("heLLo sW teSt ACadEmy!"); System.out.println(chainResult1); System.out.println(chainResult2); } @Order(3) @Test public void functionChainWithCompose() { //Function chaining. First do the second function then do the first one. Vise versa of andThen. String chainResult1 = toUpperCase.compose(toLowerCase).apply("heLLo sW teSt ACadEmy!"); String chainResult2 = toLowerCase.compose(toUpperCase).apply("heLLo sW teSt ACadEmy!"); System.out.println(chainResult1); System.out.println(chainResult2); } @Order(4) @Test public void biFunctionTest() { //Calling functions. int result = powerOf.apply(3, 2); System.out.println("Power of 3 over 2 is: " + result); } @Order(5) @Test public void unaryOperatorTest(){ //Calling UnaryOperator System.out.println(appendText.apply("Hello SW Test Academy!")); } }
Output
Predicate Interface
The predicate takes an input, it returns a boolean value as true or false. It is like a function with a parameter, with the boolean return type. BiPredicate Interface takes two inputs and returns a boolean value.
Negate method does the inversion for the value.
And method is working as logical AND operation.
Or method is working as a logical OR operation.
Let’s see all of these with examples.
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class PredicateInterface { //Predicate function declaration. String sampleText = "Hello SW Test Academy"; Predicate<String> containsPredicate = (text) -> sampleText.contains(text); //BiPredicate function declaration. BiPredicate<String, String> containsBiPredicate = (text, pattern) -> text.contains(pattern); BiPredicate<String, String> containsBiPredicateMR = String::contains; //Method reference version. @BeforeEach public void setup(TestInfo testInfo) { System.out.println("Test name: " + testInfo.getDisplayName()); } @AfterEach public void tearDown(){ System.out.println(); } @Order(1) @Test public void predicateTest() { //Calling Predicate functions. boolean result = containsPredicate.test("SW"); boolean resultOfNegate = containsPredicate.negate().test("SW"); //negate is inverse operation like "does not contain". boolean andResult = containsBiPredicate.and(containsBiPredicate.negate()).test("SW", "SW"); //Logical AND operation. boolean orResult = containsBiPredicate.or(containsBiPredicate.negate()).test("SW", "SW"); //Logical OR operation. System.out.println(result); System.out.println(resultOfNegate); System.out.println(andResult); System.out.println(orResult); } @Order(2) @Test public void predicateListTest() { List<Predicate<String>> predicateList = new ArrayList<>(); predicateList.add(containsPredicate); predicateList.add(containsPredicate.negate()); predicateList .forEach(predicate -> System.out.println(predicate.test("SW"))); } @Order(3) @Test public void biPredicateTest() { //Calling BiPredicate functions. boolean result = containsBiPredicate.test("Hello SW Test Academy", "SW"); System.out.println(result); } }
Output
Runnable Interface
The Runnable does not allow input, it does not return value. It is like a function without parameters, without return type. Let’s make it tangible with examples.
public class RunnableInterface { //Runnable function declarations. Runnable runFunction = () -> System.out.println("I am running!"); @Test public void runnableTest() { //Calling Runnable functions. runFunction.run(); } //Running Asynchronously public static void main(String[] args) { Runnable runFunction = () -> System.out.println("I am running!"); Runnable runWithDelay = () -> { Uninterruptibles.sleepUninterruptibly(3000, TimeUnit.MILLISECONDS); System.out.println("I am running Asynchronously!"); }; new Thread(runWithDelay).start(); runFunction.run(); } }
Test output:
Main Class output:
Callable Interface
Callable does not allow input, it returns a value. It is like a function without parameters, with the return type. An example is as follows.
public class CallableInterface { //Callable function declarations. Callable<Double> callFunction = () -> Math.random() * 100; Supplier<Double> supplierFunction = () -> Math.random() * 100; @SneakyThrows @Test public void callableTest() { //Calling functions. int callResult = callFunction.call().intValue(); int getResult = supplierFunction.get().intValue(); System.out.println(callResult); System.out.println(getResult); } }
Output
GitHub Project
https://github.com/swtestacademy/java-functional/tree/main/src/test/java/functional/interfaces
Thanks for reading.
Onur Baskirt

Onur Baskirt is a Software Engineering Leader with international experience in world-class companies. Now, he is a Software Engineering Lead at Emirates Airlines in Dubai.
I am happy with this post😄😄