Disclaimer: This blog is based on my personal learning and interpretation. I used ChatGPT to assist in drafting the content, which I thoroughly reviewed. The intent is to share knowledge, not to criticise or undermine anyone or anything. The content of this blog are my views and understanding of the topic. I do not intend to demean anything or anyone. I am only trying to share my views on the topic so that you will get a different thought process and angle to look at this topic.
In the world of Java programming, interfaces have long served as the blueprint for building classes that adhere to a specific contract. They define what methods a class should implement, and in doing so, they represent a type that a class must fulfill. It is a kind of behavioral abstraction and serves interoperability or easy switching between implementation strategies. However, with the introduction of functional interfaces in Java 8, this paradigm took an interesting turn.
Before Java 8, interfaces were primarily used to define a set of behaviours that classes must implement. This mechanism enabled abstraction and polymorphism in Java. For example refer the code shared in the adjacent panel.
In this case, Animal is a type that represents any class capable of eat() and sleep() behavior. The interface acts as a contract, and any implementing class is expected to provide concrete behaviors for those methods.
public interface Animal {
void eat();
void sleep();
}
public class Dog implements Animal {
@Override
public void eat() {
System.out.println("Dog eats kibble.");
}
@Override
public void sleep() {
System.out.println("Dog sleeps in the kennel.");
}
}
Java 8 introduced the concept of functional interfaces to support functional programming features, such as lambda expressions and method references. A functional interface is an interface with exactly one abstract method. It does not exist to define a full type with multiple behaviors but rather to represent a single function.
Here is a more advanced example of a functional interface being used with a constructor reference:
In this scenario, the Printer interface represents a contract for a function with no arguments that returns void. By using a method reference (SimplePrinter::new), we're effectively passing the constructor as an implementation of the functional interface. However, on closer inspection, the display method in PrinterDisplay takes a Printer interface as a parameter and calls the print method.
At first glance, it might seem that passing SimplePrinter::new would instantiate an object and invoke its print method. But that’s not how method references for constructors behave in this context. What actually happens is that the constructor is referenced as a function that matches the functional interface—only if their signatures align. Since SimplePrinter has a no-arg constructor and Printer.print() is a no-arg void method, the match seems logical.
Still, when you run the code, it does not invoke the print method of SimplePrinter; instead, it only invokes the constructor due to the method reference. Moreover, the JVM does not instantiate SimplePrinter using the constructor. Nor does it invoke print() method on implementation class.
@FunctionalInterface
public interface Printer {
void print();
}
public class SimplePrinter implements Printer {
public SimplePrinter() {
System.out.println("SimplePrinter constructor called");
}
@Override
public void print() {
System.out.println("SimplePrinter print method called with name: ");
}
}
public class PrinterDisplay {
public void display(Printer printer) {
System.out.println("PrinterDisplay display method called");
printer.print();
}
}
public class HelloApplication {
public static void main(String[] args) {
PrinterDisplay printerDisplay = new PrinterDisplay();
printerDisplay.display(SimplePrinter::new);
}
}
Let us change the contract to include a parameter and observe how the previous example will no longer compile because the constructor reference no longer matches. Modify the functional interface print method as shown in the code panel to include the String parameter.
@FunctionalInterface
public interface Printer {
void print(String name);
}
This change breaks the compatibility with the constructor reference used earlier.
The reason is that SimplePrinter::new is a constructor reference that matches a function taking no parameters and returning a SimplePrinter. However, the updated Printer interface expects a function that takes a String and returns void — which is a completely different functional signature. But as we know in java return type does not matter in the method signature. You can check how method overloading works.
printerDisplay.display(SimplePrinter::new);
As a result, the compiler will raise an error as shown in the code panel. This lambda creates a new instance of SimplePrinter and then calls its print method, correctly passing the String parameter required by the Printer interface method.
Error: incompatible types: cannot infer type arguments for Printer
To understand why the method reference fails after modifying the method signature in a functional interface, we need to peek under the hood at how the JVM interprets functional interfaces and method references.
Before Java 8, when we wanted to implement an interface inline, we would typically use anonymous inner classes as show in the code snippet in the code panel.
This code instructs the compiler to generate a synthetic class at runtime — a compiler-generated, unnamed class that implements the interface and provides the required method implementation. It's effectively the same as writing a separate class that implements Printer.
Printer printer = new Printer() {
@Override
public void print() {
System.out.println("Anonymous printer implementation");
}
};
When functional interfaces were introduced in Java 8, the goal was to reduce boilerplate and support functional programming. A lambda expression like this shown in the code panel, is interpreted by the JVM as a form of the invokedynamic bytecode instruction. The lambda is converted to an anonymous implementation of the Printer interface behind the scenes—not by creating a class file but dynamically at runtime using the LambdaMetafactory. (More on this in upcoming blogs)
Printer printer = () -> System.out.println("Lambda printer");
A method reference like SimplePrinter::new is shorthand for a lambda that creates a new object. So,
Printer printer = SimplePrinter::new;
is equivalent to:
Printer printer = () -> new SimplePrinter();
This works only if the constructor matches the method signature expected by the functional interface. Since Printer.print() takes no parameters and returns void, the constructor reference is valid as long as the constructor requires no parameters and returns an object that implements the Printer interface.
When we change the method signature to:
void print(String name);
we're no longer matching a no-argument constructor. The interface now expects a function that:
Takes a String parameter
Returns void
But SimplePrinter::new is still a constructor reference with:
No parameters
A return type of SimplePrinter
These don't match, so the compiler throws a type inference error. Java does not attempt to automatically construct an object and then call a method on it to satisfy a functional interface. Instead, the lambda or method reference must directly match the abstract method’s signature. Java creates and anonymous class which satisfies the functional interface.
Although both traditional and functional interfaces share the same syntax and are technically both interfaces, their purposes in a Java application are quite distinct. Traditional interfaces are all about defining a type that encapsulates a set of behaviours. Functional interfaces, on the other hand, are lightweight contracts for single operations, enabling a more expressive, functional style of programming.
Understanding this distinction is key to effectively leveraging Java's object-oriented and functional capabilities side by side.