Skip to content

Introduction to Lambda Expressions

TCSS 305 Programming Practicum

Lambda expressions are one of the most significant additions to Java in the language's history. They let you write cleaner, more expressive event-handling code -- and once you understand them, you'll wonder how anyone tolerated the old way. This guide introduces lambda syntax, explains functional interfaces, and shows how method references provide an even shorter notation -- all using ActionListener as our running example.


1 What Are Lambda Expressions?

Before we look at syntax, let's understand the problem lambdas solve.

In Assignment 3, you'll build a GUI application. GUI applications are event-driven: when a user clicks a button, the program needs to run a specific block of code in response. In Java Swing, you tell a button what to do by giving it an ActionListener -- an object that implements a single method, actionPerformed.

Related Guide

For the theory behind event-driven programming — why GUIs work this way — see the Event-Driven Programming guide.

Before Java 8, the only way to create an ActionListener inline was with an anonymous inner class:

// Before Java 8: Anonymous inner class (verbose!)
button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("Button clicked!");
    }
});

Look at how much ceremony is required just to say "when the button is clicked, print a message." Five lines of boilerplate for one line of real logic.

Now here is the same behavior with a lambda expression, introduced in Java 8 (2014):

// Java 8+: Lambda expression (concise!)
button.addActionListener(e -> System.out.println("Button clicked!"));

One line. Same behavior. The lambda expression is the e -> System.out.println("Button clicked!") part -- it's an anonymous function, a block of code you can pass around like a value.

Important

Lambda expressions didn't add new capabilities to Java. Anything you can do with a lambda, you could already do with an anonymous inner class. What lambdas provide is clarity -- removing the boilerplate so the intent of your code stands out.

Related Guide

For a comparison of all five event handler syntaxes — from separate classes to lambdas to method references — see the Adding Event Handlers guide.


2 What Is a Functional Interface?

Lambdas don't work with just any interface. They work with functional interfaces -- and understanding this concept is the key to understanding how lambdas work.

A functional interface is simply an interface with exactly one abstract method. This is sometimes called a SAM interface: Single Abstract Method.

That's it. One abstract method. That's the whole requirement.

ActionListener is a functional interface because it declares exactly one abstract method:

public interface ActionListener extends EventListener {
    void actionPerformed(ActionEvent e);  // The single abstract method
}

When the compiler sees a lambda expression, it matches the lambda to the single abstract method of the target functional interface. The lambda's parameters become the method's parameters, and the lambda's body becomes the method's body. There's only one method it could possibly match -- that's why exactly one abstract method is required.

2.1 The @FunctionalInterface Annotation

Java provides an optional @FunctionalInterface annotation to document this intent:

@FunctionalInterface
public interface ActionListener extends EventListener {
    void actionPerformed(ActionEvent e);
}

The annotation does two things:

  1. Documents intent -- It tells anyone reading the code that this interface is designed for use with lambdas
  2. Compiler enforcement -- If someone accidentally adds a second abstract method, the compiler produces an error

The annotation is optional. ActionListener is a functional interface whether annotated or not -- the annotation just makes it explicit and protected against accidental modification.

Note

Java's standard library includes many functional interfaces beyond ActionListener. The java.util.function package contains general-purpose ones like Predicate, Function, and Consumer. You don't need to learn those now -- for Assignment 3, ActionListener is the functional interface that matters.

Related Guide

For a concrete example of interfaces that are not functional interfaces — MouseListener has five abstract methods — see the Handling Mouse Events guide.

2.2 Why This Matters

The "single abstract method" rule is what makes the compiler's job possible. When you write:

button.addActionListener(e -> System.out.println("Clicked"));

The compiler knows:

  1. addActionListener expects an ActionListener
  2. ActionListener has one abstract method: actionPerformed(ActionEvent e)
  3. Therefore, the lambda e -> ... must be the body of actionPerformed
  4. The parameter e must be an ActionEvent

No ambiguity. No guessing. One method, one match.


3 Lambda Syntax

Now that you understand why lambdas work (functional interfaces with one abstract method), let's look at how to write them.

The basic form of a lambda expression is:

(parameters) -> expression

or, for multi-statement bodies:

(parameters) -> { statements; }

Expressions vs. Statements

An expression produces a value: x + 1, e.getPoint(), Math.max(a, b). A statement performs an action and ends with a semicolon: System.out.println("hello");, count++;, return x;. A method call like System.out.println(...) is technically both -- it's an expression (that evaluates to void) used as a statement. The distinction matters for lambdas because the no-braces form (-> expression) only works when the body is a single expression. If you need multiple statements, variable declarations, or if/else logic, you must use the braces form (-> { statements; }).

Let's build up from the most verbose form to the most concise, using ActionListener throughout.

3.1 Full Form

Start with every element spelled out explicitly:

button.addActionListener((ActionEvent e) -> {
    System.out.println("Button clicked!");
});

This is the most explicit lambda form:

  • Parameter type declared: ActionEvent e
  • Parentheses around parameters: (ActionEvent e)
  • Body in braces: { System.out.println("Button clicked!"); }

3.2 Type Inference

The compiler already knows addActionListener expects an ActionListener, and actionPerformed takes an ActionEvent. So the parameter type is redundant:

button.addActionListener((e) -> {
    System.out.println("Button clicked!");
});

The compiler infers that e is an ActionEvent. Less to type, same result.

3.3 Single Parameter -- No Parentheses

When a lambda has exactly one parameter (and the type is inferred), you can drop the parentheses:

button.addActionListener(e -> {
    System.out.println("Button clicked!");
});

Warning

This shortcut only works for a single parameter with an inferred type. Zero parameters require parentheses: () -> .... Two or more parameters require parentheses: (a, b) -> .... An explicit type requires parentheses: (ActionEvent e) -> ....

3.4 Single Expression -- No Braces

When the body is a single expression, you can drop the braces and the semicolon:

button.addActionListener(e -> System.out.println("Button clicked!"));

This is the most concise form and the one you'll see most often for simple event handlers. The expression is evaluated and its result (if any) becomes the return value.

3.5 Summary of Shortening Steps

The following table shows the progression from the most verbose form to the most concise. Each row removes one element of boilerplate.

Form Syntax
Full form (ActionEvent e) -> { System.out.println("Clicked"); }
Type inferred (e) -> { System.out.println("Clicked"); }
Single param, no parens e -> { System.out.println("Clicked"); }
Single expression, no braces e -> System.out.println("Clicked")

All four forms are equivalent. Use whichever is clearest for the situation -- but for simple event handlers, the shortest form is usually the most readable.

3.6 No Parameters

Some functional interfaces have a method that takes no parameters. In that case, use empty parentheses:

// Runnable has one method: void run()
Runnable task = () -> System.out.println("Running!");

This doesn't apply directly to ActionListener (which always receives an ActionEvent), but you may encounter it with other interfaces.

3.7 Multi-Statement Bodies

When the lambda body needs multiple statements, use braces:

button.addActionListener(e -> {
    String source = e.getActionCommand();
    System.out.println("Action: " + source);
    updateStatus(source);
});

With braces, you write statements just like a regular method body, including explicit return statements if the method has a return type.

Keep Lambdas Short

Prefer single-expression lambdas whenever possible. If a handler needs multiple statements, extract the logic into a named helper method and call it from the lambda: e -> handleSave(e) — or better yet, use a method reference: this::handleSave. A lambda that spans more than two or three lines is harder to read and harder to debug. The lambda should communicate what happens; the helper method handles how.

3.8 How the Lambda Maps to the Interface

Understanding the mapping between a lambda and its target interface removes any mystery about what the compiler is doing.

Lambda Part Maps To
Parameters (e) Method parameters (ActionEvent e)
Arrow (->) Separates parameters from body
Body (System.out.println(...)) Method body
Return type Inferred from the interface method's return type

When you write e -> System.out.println("Clicked"), the compiler generates something equivalent to:

new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("Clicked");
    }
}

The lambda is not literally syntactic sugar for an anonymous class (the implementation is different under the hood), but the behavior is identical.

Gen AI & Learning: Why Lambda Syntax Matters for AI-Assisted Development

When you ask an AI coding assistant to generate event-handling code, it will almost certainly produce lambdas. Understanding lambda syntax allows you to read, verify, and modify AI-generated code confidently. More importantly, when you describe event behavior to an AI tool -- "when the user clicks this button, call the updateDisplay method" -- the AI generates concise lambda code. If you understand the mapping between lambdas and functional interfaces, you can verify that the generated code does what you intended.


4 Method References

Sometimes a lambda expression does nothing more than call an existing method. In those cases, Java offers an even shorter notation: method references.

4.1 What They Are

A method reference is shorthand for a lambda that simply delegates to an existing method. Instead of writing a lambda that calls a method, you refer to the method directly.

Consider this lambda:

button.addActionListener(e -> handleClick(e));

The lambda receives e and immediately passes it to handleClick. It's just a pass-through. A method reference says this more directly:

button.addActionListener(this::handleClick);

The this::handleClick syntax means: "use the handleClick method on this object as the implementation."

4.2 When You Can Use One

A method reference works when the lambda's parameters match the method's parameters exactly. The lambda must do nothing other than forward its parameters to the method.

Method reference works:

// Lambda just passes e to handleClick -- parameters match
e -> handleClick(e)
// Becomes:
this::handleClick

// Where handleClick is:
private void handleClick(ActionEvent e) {
    // handle the click
}

Method reference does NOT work:

// Lambda does more than just call a method
e -> {
    log("Click detected");
    handleClick(e);
}

// Lambda passes different/additional arguments
e -> processAction(e, "button1")

// Lambda ignores the parameter
e -> doSomething()  // e is not passed to doSomething

In these cases, stick with a lambda.

4.3 Common Syntax Forms

Form Syntax Example
Instance method (this) this::methodName this::handleClick
Instance method (other object) object::methodName panel::repaint
Static method ClassName::methodName System::exit

For Assignment 3, the most common form is this::methodName, where you define a private method in your GUI class and reference it from the event registration code.

4.4 A Practical Example

Here's a realistic example from a GUI class. Instead of writing all the event logic inline, you define named methods and reference them:

public class MyWindow extends JFrame {

    private void createButtons() {
        JButton saveButton = new JButton("Save");
        JButton quitButton = new JButton("Quit");

        // Method references -- clean and descriptive
        saveButton.addActionListener(this::handleSave);
        quitButton.addActionListener(this::handleQuit);
    }

    private void handleSave(ActionEvent e) {
        // Save logic here
    }

    private void handleQuit(ActionEvent e) {
        dispose();
    }
}

Compare this to the anonymous inner class version -- the method reference approach is dramatically more readable. Each event handler has a descriptive name, and the registration code clearly shows what happens for each button.

Tip

Method references work best when each handler is complex enough to deserve its own named method. For simple one-liners like e -> System.out.println("Clicked"), a lambda is perfectly fine. Use the form that makes the code most readable.


Summary

Concept Key Point
Lambda expression Anonymous function: a block of code you can pass around
Functional interface An interface with exactly one abstract method (SAM)
@FunctionalInterface Optional annotation that documents intent and enables compiler checking
Lambda syntax (params) -> expression or (params) -> { statements; }
Type inference Compiler infers parameter types from the target functional interface
Method reference Shorthand (this::method) when a lambda just calls an existing method
Anonymous inner class The verbose pre-Java 8 alternative; lambdas replace most uses

The progression from anonymous inner classes to lambdas to method references is a journey toward clarity.

Approach Syntax Best For
Anonymous inner class new ActionListener() { ... } Rarely needed in modern Java
Lambda expression e -> doSomething() Inline logic, simple handlers
Method reference this::handleClick Delegating to an existing named method

Further Reading

External Resources


References

Primary Texts:

  • Bloch, J. (2018). Effective Java (3rd ed.). Addison-Wesley. Item 42: Prefer lambdas to anonymous classes; Item 43: Prefer method references to lambdas.
  • Horstmann, C. S. (2022). Core Java, Volume I: Fundamentals (12th ed.). Oracle Press. Chapter 6: Interfaces, Lambda Expressions, and Inner Classes -- Lambda Expressions.

Language Documentation:

Additional Resources:


This guide is part of TCSS 305 Programming Practicum, School of Engineering and Technology, University of Washington Tacoma.