These three concepts build on each other more than they seem to at first glance: annotations give the compiler metadata to act on, one specific annotation (@FunctionalInterface) marks a particular kind of interface, and that kind of interface is exactly what makes lambda expressions possible. This post walks through all three, in that order.
Annotations
Metadata(s) for the compiler.
An annotation in Java is metadata attached to code — a class, method, field, or interface — that doesn't change what the code does, but gives instructions to the compiler, a tool, or the JVM about how to treat it. You've probably already used a few without thinking about it: @override, @deprecated, @SuppressWarnings.
@Override
public String toString() {
return "example";
}
@override doesn't add behavior — it just tells the compiler "check that this method actually overrides something in the superclass, and fail if it doesn't." That's the general pattern: annotations are instructions about the code, not part of its logic.
One of the most useful annotations for modern Java is @FunctionalInterface, which brings us to the next concept.
Functional interfaces
A functional interface is an interface with exactly one abstract method. This single-method shape is also called a SAM interface — Single Abstract Method.
interface A {
void show();
}
To make Java enforce that an interface stays this way, you annotate it:
@FunctionalInterface
interface A {
void show();
}
Now, if a second abstract method sneaks in, it's going to give a compile-time error instead of a bug which you will see later:
@FunctionalInterface
interface A {
void show();
void play(); // compile error: not a functional interface anymore
}
Note that this restriction only applies to abstract methods. A functional interface can still have any number of default or static methods, since those already have a body and don't count toward the "single abstract method" rule:
@FunctionalInterface
interface A {
void show();
default void log() {
System.out.println("Logging from A");
}
}
Implementing a functional interface with an anonymous class
Before lambdas, this is what implementing a small interface inline looked like:
public class StoreApplication {
public static void main(String[] args) {
A obj = new A() {
@Override
public void show() {
System.out.println("implementing interface directly....");
}
};
obj.show();
}
}
It works, but it's verbose for something this simple — which is exactly the problem lambda expressions were introduced to solve.
Lambda expressions
Functional interfaces are what make lambda expressions possible in Java 8+: since a functional interface guarantees there's exactly one method to implement, Java can let you skip the class name, the method name, and the boilerplate, and just write the method body.
Implementing a functional interface with a lambda
@FunctionalInterface
interface A {
void show();
}
public class StoreApplication {
public static void main(String[] args) {
A obj = () -> System.out.println("implementing interface using lambda....");
obj.show();
}
}
Passing an argument via lambda
@FunctionalInterface
interface A {
void show(int i);
}
public class StoreApplication {
public static void main(String[] args) {
A obj = (i) -> System.out.println("implementing interface using lambda...." + i);
obj.show(45);
}
}
A functional interface that returns a value
@FunctionalInterface
interface A {
int add(int a, int b);
}
public class StoreApplication {
public static void main(String[] args) {
A calculator = (i, j) -> {
System.out.println("Performing addition of numbers " + i + " and " + j);
return i + j;
};
System.out.println("The sum is equal to " + calculator.add(45, 30));
}
}
Same contract, same enforcement from @FunctionalInterface — just far less ceremony to implement it.
Types of interfaces, at a glance
Now that we've covered the functional/SAM interface in depth, it's worth placing it alongside the other interface types you'll run into:
Normal interface — two or more methods, no restrictions on count.
Functional / SAM interface — exactly one abstract method; can be
implemented with a lambda.
Marker interface — has no methods at all. It exists purely to "tag" a class with metadata that other code can check via instanceof, so the JVM or a framework can treat tagged classes differently. Serializable and Cloneable are the classic examples — implementing Serializable doesn't add any methods, but it tells the JVM "this object is allowed to be serialized."
That last one brings things full circle: marker interfaces were Java's original way of attaching metadata to a class before annotations existed. Today, annotations have largely taken over that role — which is exactly where this post started.
Wrapping up
Annotations give the compiler instructions, @FunctionalInterface uses that mechanism to enforce a single-method contract, and lambdas exist to make implementing that contract painless. Once you see the chain connecting them, a lot of modern Java code (streams, Comparator, Runnable, custom callbacks) starts making a lot more sense.
Got a favorite use of lambdas or functional interfaces in your own code? Drop it in the comments.
Top comments (2)
Dear dev supports, that link is fraudulent.