DEV Community

Mayur Bodhare
Mayur Bodhare

Posted on

From Anonymous Class to Lambda: A Java Step-by-Step

When I first saw this line of Java, I stared at it for a full minute:

names.sort((a, b) -> Integer.compare(a.length(), b.length()));
Enter fullscreen mode Exit fullscreen mode

What is that arrow? Where is the method name? What type is the thing in the brackets? And later I hit an even stranger puzzle: the same arrow syntax worked in some places and gave a confusing error in others.

The answers all come back to one idea: a lambda only works where Java expects a functional interface. Once you understand that single rule, lambdas stop feeling like magic.

In this post, we'll learn what functional interfaces and lambda expressions are, why one needs the other, what the compiler does behind the scenes and which built-in interfaces you'll use every day. If you've read about anonymous inner classes, this is the natural next step.

Unless I show a full class, the snippets go inside a main method.

The problem lambdas solve

Here's how we wrote a custom sort before lambdas, using an anonymous class:

List<String> names = new ArrayList<>(List.of("Meera", "Jo", "Ravi"));

Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return Integer.compare(a.length(), b.length());
    }
});
Enter fullscreen mode Exit fullscreen mode

The real idea is one line: "compare the lengths." But we had to write new Comparator<String>(), braces, @Override, a full method signature and more braces. Almost all of that is ceremony that carries no information.

Java 8 introduced lambda expressions to remove the ceremony. Here's the same sort:

names.sort((a, b) -> Integer.compare(a.length(), b.length()));
System.out.println(names);
Enter fullscreen mode Exit fullscreen mode

Output:

[Jo, Ravi, Meera]
Enter fullscreen mode Exit fullscreen mode

Same behavior, one line. But to understand why this is allowed, we first need to meet functional interfaces.

What is a functional interface?

A functional interface is an interface with exactly one abstract method. That's the whole definition. (You'll also see the name SAM, short for "single abstract method.")

@FunctionalInterface
interface Validator {
    boolean isValid(String text);
}
Enter fullscreen mode Exit fullscreen mode

A few details:

  • It can also have default and static methods. They have bodies, so they don't count.
  • Methods that already exist in Object, like equals, don't count either. (That's why Comparator, which re-declares equals, is still functional.)
  • @FunctionalInterface is optional, but it's a good habit. It asks the compiler to check the rule for you. If someone later adds a second abstract method, the compiler complains:
@FunctionalInterface
interface Broken {
    void first();
    void second();
}
Enter fullscreen mode Exit fullscreen mode
Unexpected @FunctionalInterface annotation
  Broken is not a functional interface
    multiple non-overriding abstract methods found in interface Broken
Enter fullscreen mode Exit fullscreen mode

You've already used functional interfaces without knowing it. Runnable (one method: run), Comparator (compare) and Comparable (compareTo) are all functional interfaces.

What is a lambda expression?

A lambda expression is a short way to write an anonymous function: some inputs, an arrow and a body. It has no name, and it can be passed around like a value.

   (parameters)   ->   body

   (a, b)         ->   a + b
     |             |      |
   inputs        arrow   what to compute (the result)
Enter fullscreen mode Exit fullscreen mode

Here are the different forms you'll see:

(a, b) -> a + b                          // two parameters, one expression
x -> x * 2                               // one parameter: brackets are optional
() -> System.out.println("Hi")           // no parameters: empty brackets needed
(String s) -> s.length()                 // parameter types are optional
(a, b) -> {                              // block body: needs braces and "return"
    int sum = a + b;
    return sum;
}
Enter fullscreen mode Exit fullscreen mode

The rules in short:

  • Parameter types are usually optional, because Java figures them out.
  • With one expression, no braces and no return are needed. The value of the expression is the result.
  • With a block body { ... }, you write return yourself (if the method returns something).

Let's use a lambda to implement our own interface, and Runnable:

Validator notEmpty = text -> !text.isEmpty();

System.out.println(notEmpty.isValid("Java"));
System.out.println(notEmpty.isValid(""));

Runnable task = () -> System.out.println("Running");
task.run();
Enter fullscreen mode Exit fullscreen mode

Output:

true
false
Running
Enter fullscreen mode Exit fullscreen mode

The significance of functional interfaces for lambdas

This is the key idea of the whole post.

A lambda has no type of its own. Look at x -> x * 2. Is it a Runnable? A Comparator? Something else? The expression alone doesn't say. Java decides by looking at where the lambda is used. That's called target typing.

When you write:

Validator notEmpty = text -> !text.isEmpty();
Enter fullscreen mode Exit fullscreen mode

the compiler thinks:

  1. "The target type here is Validator."
  2. "It's a functional interface, so it has exactly one abstract method: boolean isValid(String text)."
  3. "So this lambda must be the body of that method. The parameter text is a String, and the result must be a boolean."
   Validator notEmpty = text -> !text.isEmpty();
                         |           |
                         |           +---- becomes the body of isValid(...)
                         +---------------- becomes the parameter of isValid(...)

   Validator has ONE abstract method:   boolean isValid(String text)
Enter fullscreen mode Exit fullscreen mode

That's why the rule matters. If an interface had two abstract methods, Java couldn't tell which one the lambda is meant to implement. With exactly one, there's no doubt. A functional interface is what makes the lambda unambiguous, and it also gives the compiler all the type information it needs, so you can leave the types out.

The same lambda, different types

Because the type comes from the context, the exact same lambda text can become different things:

Function<Integer, Integer> doubler1 = x -> x * 2;
UnaryOperator<Integer> doubler2 = x -> x * 2;

System.out.println(doubler1.apply(5));
System.out.println(doubler2.apply(6));
Enter fullscreen mode Exit fullscreen mode

Output:

10
12
Enter fullscreen mode Exit fullscreen mode

And without a target type, it fails

Object o = () -> System.out.println("Hi");
Enter fullscreen mode Exit fullscreen mode
incompatible types: Object is not a functional interface
Enter fullscreen mode Exit fullscreen mode

Object has no single abstract method for the lambda to become, so Java refuses. This is also why you can't use a lambda for an abstract class or an interface with several abstract methods. For those, an anonymous class is still the tool.

Behind the scenes: what does the compiler do?

Many people say "a lambda is just an anonymous class with shorter syntax." It's a helpful way to think at first, but it isn't what really happens.

With an anonymous class, the compiler creates a separate .class file (like Main$1.class). With a lambda, it doesn't. Roughly, this happens:

   Source:     () -> System.out.println("Hi")
                         |
   javac:      1) turns the body into a private hidden method, named like  lambda$main$0
               2) at the place where the lambda is used, it writes an "invokedynamic" instruction
                         |
   Run time:   the first time that line runs, the JVM (through LambdaMetafactory)
               builds an object that implements Runnable and forwards run()
               to lambda$main$0()
Enter fullscreen mode Exit fullscreen mode

You can see this yourself with javap -p -c on a compiled class. Look for lambda$main$0 and invokedynamic.

Why did Java choose this design? Because creating the object at run time lets the JVM decide how to do it efficiently, for example reusing one object when the lambda doesn't capture anything. It also keeps your output folder free of hundreds of tiny class files.

One visible difference: this

Inside an anonymous class, this means the anonymous object itself. Inside a lambda, this means the enclosing object, because a lambda is not a new object scope:

class Greeter {
    String name = "Outer";

    void test() {
        Runnable lambda = () -> System.out.println(this.name);

        Runnable anon = new Runnable() {
            String name = "Anon";

            public void run() {
                System.out.println(this.name);
            }
        };

        lambda.run();
        anon.run();
    }
}
Enter fullscreen mode Exit fullscreen mode
new Greeter().test();
Enter fullscreen mode Exit fullscreen mode

Output:

Outer
Anon
Enter fullscreen mode Exit fullscreen mode

Using variables from outside the lambda

A lambda can read variables from the surrounding method, but with the same rule as anonymous classes: they must be final or effectively final (never changed after being set).

int bonus = 10;
Function<Integer, Integer> addBonus = score -> score + bonus;

System.out.println(addBonus.apply(90));
Enter fullscreen mode Exit fullscreen mode

Output:

100
Enter fullscreen mode Exit fullscreen mode

If you try to change bonus afterwards (even after the lambda), it won't compile:

local variables referenced from a lambda expression must be final or effectively final
Enter fullscreen mode Exit fullscreen mode

The reason is that the lambda copies the value when it's created. Allowing changes would make the copy and the original disagree. Fields (variables belonging to an object) don't have this limit, since the lambda reads them through the object.

Built-in functional interfaces

You don't have to invent a new interface for every lambda. Java ships a set of ready-made ones in the java.util.function package (add import java.util.function.*;). The most common are named after the shape of the function:

   Predicate<T>        T       ->  boolean        "is this true or false?"
   Function<T, R>      T       ->  R              "turn this into that"
   Consumer<T>         T       ->  (nothing)      "do something with this"
   Supplier<T>         ()      ->  T              "give me a value"
   BiFunction<T,U,R>   T, U    ->  R              "combine two things"
   UnaryOperator<T>    T       ->  T              "same type in and out"
   BinaryOperator<T>   T, T    ->  T              "combine two of the same type"
Enter fullscreen mode Exit fullscreen mode

Here they are in action:

Predicate<Integer> isEven = n -> n % 2 == 0;
Function<String, Integer> length = s -> s.length();
Consumer<String> printer = s -> System.out.println("Hi " + s);
Supplier<String> hello = () -> "Hello";
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;

System.out.println(isEven.test(4));
System.out.println(length.apply("Lambda"));
printer.accept("Asha");
System.out.println(hello.get());
System.out.println(add.apply(3, 4));
Enter fullscreen mode Exit fullscreen mode

Output:

true
6
Hi Asha
Hello
7
Enter fullscreen mode Exit fullscreen mode

Each interface has one abstract method with a different name (test, apply, accept, get). That's the one the lambda becomes.

Default methods let you combine them

Remember that functional interfaces can have default methods. The built-in ones use that to let you combine lambdas:

Predicate<String> notEmpty = s -> !s.isEmpty();
Predicate<String> shortWord = s -> s.length() < 5;
Predicate<String> both = notEmpty.and(shortWord);

System.out.println(both.test("Java"));
System.out.println(both.test("Lambda"));
Enter fullscreen mode Exit fullscreen mode

Output:

true
false
Enter fullscreen mode Exit fullscreen mode

and is a default method, so Predicate still has exactly one abstract method (test) and remains a functional interface.

Method references: an even shorter lambda

Sometimes a lambda does nothing except call one existing method. In that case, you can write a method reference with :::

List<String> names = new ArrayList<>(List.of("Meera", "Jo", "Ravi", "Asha"));
names.removeIf(n -> n.length() < 3);
names.sort(String::compareTo);

names.forEach(System.out::println);
Enter fullscreen mode Exit fullscreen mode

Output:

Asha
Meera
Ravi
Enter fullscreen mode Exit fullscreen mode
  • String::compareTo means (a, b) -> a.compareTo(b).
  • System.out::println means n -> System.out.println(n).

Both are the same lambdas, just shorter. Use them when they make the code clearer, and keep the full arrow form when it reads better.

You also saw lambdas in removeIf and forEach. Methods like these, which take a functional interface as a parameter, are everywhere in modern Java, especially in the Streams API (a topic for another day).

Which one should I use?

  • Lambda or anonymous class? Use a lambda when the interface has exactly one abstract method and the body is short. Use an anonymous class when you need to implement an interface with several methods, extend an abstract or normal class, or keep its own fields.
  • Long lambda? If the body grows past a few lines, move the logic into a named method and use a method reference. Names explain intent better than long arrows.
  • Built-in or custom functional interface? Prefer the built-in ones (Predicate, Function and so on). Write your own when a meaningful name makes the code clearer (like Validator), or when you need more than two parameters or a checked exception.
  • Do you need @FunctionalInterface? It's optional, but add it to your own interfaces. It documents your intention and protects it.

Common mistakes

  1. Using a lambda where there's no functional interface. For an abstract class, or an interface with two or more abstract methods, a lambda won't compile. Use an anonymous class instead.
  2. Changing a captured local variable. Variables used inside a lambda must be effectively final. Use a field, or an object like an AtomicInteger, if you need a counter.
  3. Writing long, multi-line lambdas. They're hard to read, test and reuse. Extract a method and use a method reference.
  4. Assuming this works like in an anonymous class. Inside a lambda, this is the enclosing object, not the lambda.
  5. Forgetting that a lambda needs a target type. Object o = () -> ... and var x = () -> ... both fail, because Java can't tell which functional interface you mean.
  6. Throwing checked exceptions inside a lambda. Runnable r = () -> Thread.sleep(100); fails with "unreported exception InterruptedException," because run() doesn't declare it. Catch the exception inside the lambda, or use an interface whose method declares it.

Quick recap

  • A functional interface has exactly one abstract method. It may also have default and static methods.
  • @FunctionalInterface is an optional check that makes the compiler enforce the rule.
  • A lambda expression is a short anonymous function: (parameters) -> body.
  • A lambda has no type of its own. Java picks it from the target type, and a functional interface makes that choice unambiguous because there's only one method to implement.
  • The compiler turns the body into a hidden method and builds the object at run time with invokedynamic. There's no extra .class file, and this means the enclosing object.
  • Captured local variables must be effectively final.
  • Predicate, Function, Consumer, Supplier, BiFunction, UnaryOperator and BinaryOperator cover most needs.
  • A method reference (String::length) is a shorthand for a lambda that just calls one method.

Try it yourself

Here's a small practice idea: take a list of numbers and use lambdas to do three things. Remove the odd numbers with removeIf, sort the rest in descending order using a lambda for the comparison, and print each one with a method reference. Then rewrite the sort using an anonymous class, and compare how much you had to type.

For a bonus experiment, write your own functional interface called Calculator with a method int calculate(int a, int b). Create an add, a multiply and a max version, each with a one-line lambda. Then try adding a second abstract method and read the compiler's error message.

If this helped, or if you spotted something I should fix, tell me in the comments. I'm still learning too.

Next in this series: enums and annotations.

Top comments (0)