Lambdas & Functional Interfaces

Lambda Syntax

15 min Lesson 2 of 13

Lambda Syntax

In the previous lesson you saw how anonymous classes let you pass behaviour as a value. Lambdas are a cleaner spelling of the same idea. This lesson digs into every part of the syntax: how parameters are written, when you can drop parentheses and type annotations, and the difference between an expression body and a block body.

The Shape of a Lambda

A lambda expression has three parts separated by the arrow token ->:

(parameters) -> body

The left side declares what the lambda receives. The right side declares what it does. There is no return type, no access modifier, no name.

Parameter Lists

Parameter syntax is flexible. The rules are:

  • Zero parameters — write empty parentheses: () -> ...
  • One parameter — parentheses are optional if you omit the type: x -> ... or (x) -> ...
  • Two or more parameters — parentheses are required: (a, b) -> ...
  • Explicit types — parentheses are required: (String s) -> ...
// Zero parameters Runnable r = () -> System.out.println("Hello"); // One parameter — no parentheses needed java.util.function.Consumer<String> greet = name -> System.out.println("Hi " + name); // Two parameters java.util.function.BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b; // Explicit types (rarely needed, but valid) java.util.function.BiFunction<Integer, Integer, Integer> multiply = (Integer x, Integer y) -> x * y;
Consistency rule: if you provide a type for one parameter you must provide types for all of them. You cannot mix typed and untyped parameters in the same lambda.

Type Inference

In almost every real-world lambda you omit the parameter types. The compiler infers them from the target type — the functional interface the lambda is being assigned to or passed into. This is the same flow-sensitive inference Java uses for var and generic method calls.

import java.util.List; import java.util.function.Predicate; List<String> names = List.of("Alice", "Bob", "Charlie"); // The compiler knows filter() expects Predicate<String>, // so it infers that 's' is a String. // You write: s -> s.length() > 4 // Not: (String s) -> s.length() > 4 names.stream() .filter(s -> s.length() > 4) .forEach(System.out::println);

When there is genuine ambiguity — for example, when a method is overloaded with incompatible functional interfaces — the compiler asks you to add explicit types to resolve it. That situation is rare.

Expression Bodies

If the lambda body is a single expression, the result of that expression is the implicit return value. No braces, no return keyword.

import java.util.function.Function; // Expression body — clean and readable Function<String, Integer> lengthOf = s -> s.length(); System.out.println(lengthOf.apply("hello")); // 5

Expression bodies shine for small transformations, comparisons, and predicates. Keep them as expressions whenever the logic fits on one line.

Block Bodies

When you need more than one statement — local variables, conditionals, loops — wrap the body in braces. You must then use an explicit return statement for non-void lambdas.

import java.util.function.Function; Function<String, String> titleCase = s -> { if (s == null || s.isEmpty()) { return s; } return Character.toUpperCase(s.charAt(0)) + s.substring(1).toLowerCase(); }; System.out.println(titleCase.apply("jAVA")); // Java
Prefer expression bodies. A block body longer than three or four lines usually means the logic deserves a named private method. You can then reference that method with a method reference, keeping the call-site clean and the logic testable in isolation.

Return Inside a Block Body

A common mistake is forgetting that return inside a lambda exits the lambda, not the surrounding method.

import java.util.List; void printLongNames(List<String> names) { names.forEach(name -> { if (name.length() <= 3) { return; // exits the lambda for THIS element, not printLongNames() } System.out.println(name); }); System.out.println("Done"); // always reached }
Void-compatible expressions: an expression that produces a value can be used as the body of a void-returning lambda. For example, list.add(item) returns boolean, but you can use it as the body of a Consumer<T> lambda — Java simply discards the return value. The compiler calls this a void-compatible expression.

Putting It Together: Sorting Example

Comparator is one of the oldest functional interfaces in Java. Sorting a list by string length shows all syntax variants side by side:

import java.util.ArrayList; import java.util.Comparator; import java.util.List; List<String> words = new ArrayList<>(List.of("banana", "fig", "apple", "kiwi")); // Block body with explicit types words.sort((String a, String b) -> { return Integer.compare(a.length(), b.length()); }); // Expression body — inferred types words.sort((a, b) -> Integer.compare(a.length(), b.length())); // Even shorter with a factory method words.sort(Comparator.comparingInt(String::length)); System.out.println(words); // [fig, kiwi, apple, banana]

Summary

Lambda syntax revolves around three choices: how many parameters (and whether to write their types), whether to use an expression body or a block body, and whether to include parentheses around a single untyped parameter. The compiler infers types from the target functional interface, so you almost never need to spell them out. Use expression bodies for concise single-expression logic and block bodies when you need multiple statements — and consider extracting long block bodies into named methods.