Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 17

Functional Interfaces: Lambda Expressions & Method References

Recommended resources

Hello! Welcome to the first lesson of our module on "Functional Java with Streams & Lambdas."

In our previous lesson, we saw how Java 8 enables a more concise way to implement the Comparator interface using lambda expressions and method references, like Comparator.comparing(Employee::getName). This was a deliberate preview of the powerful features introduced in Java 8.

Today, we will build a formal foundation for these concepts. Our goal is to explain functional interfaces and implement them using lambda expressions and method references. Mastering this is non-negotiable for any modern Java developer, and it's a fundamental topic in technical interviews for senior roles. We will explore:

  • What makes an interface "functional."
  • How to replace verbose anonymous inner classes with clean lambda expressions.
  • How to make your code even more readable with method references.

1. Functional Interfaces: The Target for Lambdas

Before Java 8, interfaces like Runnable and Comparator were typically implemented using bulky anonymous inner classes. The key characteristic they share is that they have only one abstract method to implement. Java 8 gave this concept a formal name: Functional Interface.

A functional interface is any interface that has exactly one abstract method (SAM).

  • It can have any number of default or static methods.
  • You can mark it with the @FunctionalInterface annotation. This is optional but recommended, as it allows the compiler to verify that you are correctly following the SAM rule.

Pre-Java 8 and Post-Java 8 Examples

Let's watch a video that defines functional interfaces and discusses some classic examples and interview-related nuances.

Top 20+ Java 8 Interview Questions & Answers [Most Important] | JavaTechie

This video from 'Java Techie' clearly defines what a functional interface is and provides important examples, including a common interview question about the Comparator interface.

Watch the segment from 01:48 to 04:45. Pay close attention to the definition and the explanation for why Comparator is a functional interface despite having two abstract methods (compare and equals). This is a frequent interview question.

As the video explains, Comparator is a functional interface because the equals method is inherited from the Object class, and methods from Object do not count against the single abstract method rule.

While Java had functional interfaces before version 8, the java.util.function package was added to provide a standard set of interfaces for common use cases. Here are the four most important ones:

Interface Abstract Method What it does Analogy
Predicate<T> boolean test(T t) Takes one argument and returns a boolean. A yes/no question.
Consumer<T> void accept(T t) Takes one argument and returns nothing. An action or command.
Function<T, R> R apply(T t) Takes one argument and returns a result. A transformation.
Supplier<R> R get() Takes no arguments and returns a result. A factory or producer.

For a more detailed look at these core interfaces, the following video provides clear examples for each.

Functional Interfaces and Lambda Expressions in Java with Examples | Geekific

The 'Geekific' channel provides a structured overview of these four main functional interfaces with practical code examples.

Watch the following segments to understand each interface: Predicate: from 07:33 to 09:42. Supplier and Consumer: from 09:42 to 12:29. Function: from 01:58 to 04:55 (you can skip the andThen and compose parts for now).


2. Lambda Expressions: Anonymous Functions

A lambda expression is a concise, unnamed block of code that you can use to provide an implementation for a functional interface's abstract method. It's essentially an anonymous function.

From Anonymous Class to Lambda

Let's look at the evolution. Before Java 8, to create a Runnable, you would use an anonymous inner class:

// Pre-Java 8: Anonymous Inner Class
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("Running in a thread");
    }
};

With a lambda expression, this becomes a single line:

// Java 8: Lambda Expression
Runnable r2 = () -> System.out.println("Running in a thread");

The compiler infers that you are implementing the run() method of the Runnable interface. The basic syntax is:
(parameters) -> { body }

  • The parentheses () contain the method parameters (empty for run()).
  • The arrow -> separates the parameters from the body.
  • The body {...} contains the implementation. If the body is a single expression, you can omit the curly braces.

This transition from verbose anonymous classes to concise lambdas is a key feature of modern Java.

Java 8: Lambdas, Part 1

This article from Oracle, written by one of the Java language architects, explains the motivation and syntax of lambdas.

Read the following sections: 'Lambda expressions': Focus on the comparison between Listing 2 (anonymous class) and Listing 3 (lambda). 'Syntax': Understand the three parts of a lambda (parameters, arrow, body). 'Type inference': See how the compiler is smart enough to let you omit parameter types.

Test your understanding!

You have a List<String> names. You want to sort it by the length of the strings. Write a Comparator using an anonymous inner class, and then rewrite it using a lambda expression.

Show answer

Anonymous Inner Class:

Comparator<String> byLength_Anonymous = new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return Integer.compare(s1.length(), s2.length());
    }
};
names.sort(byLength_Anonymous);

Lambda Expression:

Comparator<String> byLength_Lambda = (s1, s2) -> Integer.compare(s1.length(), s2.length());
names.sort(byLength_Lambda);

Or even more concisely:

names.sort((s1, s2) -> Integer.compare(s1.length(), s2.length()));

Variable Capture and this

For senior-level interviews, you need to know two more details about lambdas:

  1. Variable Capture: Lambdas can "capture" variables from their enclosing scope. However, they can only access local variables that are final or effectively final (meaning the variable's value is never changed after initialization).
  2. Lexical Scoping: The this keyword inside a lambda refers to the this of the enclosing class, not the lambda itself. This is different from an anonymous inner class, where this refers to the instance of the anonymous class.

The Oracle article provides an excellent explanation of these more advanced points.

Java 8: Lambdas, Part 1

Let's revisit the Oracle article to understand these two crucial, and often-tested, concepts.

Read the sections 'Lexical scoping' and 'Variable capture'. This will clarify how lambdas interact with their surrounding code, a key difference from anonymous inner classes.


3. Method References: A Shorthand for Lambdas

Sometimes, your lambda expression does nothing but call an existing method. For example:

// Lambda that just calls println
Consumer<String> printer = s -> System.out.println(s);

In these cases, you can make the code even more readable by using a method reference.

// Method reference
Consumer<String> printer = System.out::println;

The :: operator separates the class or object from the method name. Method references are a compact and readable alternative to lambdas that simply invoke a single method.

There are four main kinds of method references:

Lambda Expressions vs. Equivalent Method References in Java 8+
This table clearly compares lambda expressions with their equivalent method reference syntax for various scenarios.

Let's watch a quick video that demonstrates this with code.

Top 20+ Java 8 Interview Questions & Answers [Most Important] | JavaTechie

This clip from 'Java Techie' provides a hands-on demonstration of converting a lambda into a method reference for both static and instance methods.

Watch from 39:00 to 42:16. Notice how a lambda that calls a static method is converted to ClassName::methodName, and one that calls an instance method is converted to instance::methodName.

Now, the syntax from our last lesson, Comparator.comparing(Employee::getName), should make complete sense. It's a method reference to the getName instance method of the Employee class, passed to the static comparing method of Comparator.


4. A Deeper Look: How Lambdas Work (Optional)

A common misconception is that lambdas are just syntactic sugar for anonymous inner classes. This isn't true. The compiler and JVM use a more efficient mechanism.

When the compiler encounters a lambda, it doesn't generate a .class file for it. Instead, it generates a special bytecode instruction called invokedynamic, which was introduced in Java 7. At runtime, the JVM uses a helper class called LambdaMetaFactory to dynamically create an object that implements the functional interface. This approach is more performant and uses less memory than creating a new class file for every lambda.

For a deeper dive into this interview-relevant topic, the following article is an excellent summary.

Internal Working of Lambda Expressions in Java

If you're interested in the JVM-level implementation details, this article from Medium provides a concise explanation of invokedynamic and LambdaMetaFactory.

Read the entire article, focusing on the concepts of 'Invokedynamic Instruction' and 'LambdaMetaFactory'.


Conclusion

In this lesson, we established the fundamental concepts of functional programming in Java. You now have a solid understanding of the relationship between functional interfaces, lambdas, and method references.

Key Takeaways for Your Interview:

  • A Functional Interface has a Single Abstract Method (SAM) and is the "target type" for a lambda expression.
  • A Lambda Expression is a concise, anonymous function that implements the abstract method of a functional interface. It provides cleaner code than anonymous inner classes.
  • A Method Reference is an even more compact shorthand for a lambda that does nothing but call an existing method. Always prefer a method reference over a simple lambda for readability.
  • Be ready to discuss the nuances: why Comparator is a functional interface, the rules of variable capture (effectively final), and how this behaves (lexical scoping).

Preview of the Next Lesson:
With this foundation in place, we are ready to tackle the main event: the Stream API. In the next lesson, we will explore the core principles of streams, including lazy evaluation and the difference between intermediate and terminal operations. You will see how the functional interfaces we learned about today are the engine that powers the entire Stream API.