
A lambda expression is a function without a name: It consists of zero, one, or more input parameters, followed by an arrow and a body that may return a value.
An example: book -> book.year() < 1850 takes a book as its input parameter and returns whether that book was published before 1850.
A lambda is something you pass to a method that expects a definition of behavior as its parameter – to the Stream.filter() method, for example, to filter the books you want out of a stream of books.
Java has had lambda expressions since Java 8 (March 2014) – before that, you would have had to write an anonymous class for this. Don’t worry – I will show you all of it with examples in a moment.
In this article, you will find out
- what a lambda expression is and what it replaces,
- which forms the syntax allows and which one I recommend,
- how the compiler determines the type of a lambda,
- which variables a lambda can access and what “effectively final” means,
- when a method reference replaces a lambda and which four kinds there are,
- where lambdas are used beyond streams,
- how the JVM executes a lambda,
- which mistakes to avoid.
The Examples in This Article
The examples use the data model of the Java Streams article – an enum Genre, a record Book, and a small library of eleven classics:
public enum Genre {
NOVEL,
GOTHIC,
ADVENTURE,
FANTASY,
SCIENCE_FICTION
}
public record Book(String title, String author, int year, Genre genre) {}
public class Library {
public static final List<Book> BOOKS = List.of(
new Book("Pride and Prejudice", "Jane Austen", 1813, NOVEL),
new Book("Frankenstein", "Mary Shelley", 1818, GOTHIC),
new Book("Moby-Dick", "Herman Melville", 1851, ADVENTURE),
new Book("From the Earth to the Moon", "Jules Verne", 1865, SCIENCE_FICTION),
new Book("Alice's Adventures in Wonderland", "Lewis Carroll", 1865, FANTASY),
new Book("Around the World in Eighty Days", "Jules Verne", 1873, ADVENTURE),
new Book("Treasure Island", "Robert Louis Stevenson", 1883, ADVENTURE),
new Book("Kidnapped", "Robert Louis Stevenson", 1886, ADVENTURE),
new Book("The Time Machine", "H. G. Wells", 1895, SCIENCE_FICTION),
new Book("Dracula", "Bram Stoker", 1897, GOTHIC),
new Book("The War of the Worlds", "H. G. Wells", 1898, SCIENCE_FICTION));
}
You can find the complete code of all examples in the GitHub repository java-streams-examples, in the package eu.happycoders.lambdas.
What Is a Lambda Expression?
Suppose you want to sort the books by year. List.sort() expects a Comparator, and a comparator is an object with the method compare(). Before Java 8, you had to write an anonymous class for that:
List<Book> books = new ArrayList<>(BOOKS);
books.sort(new Comparator<Book>() {
@Override
public int compare(Book a, Book b) {
return Integer.compare(a.year(), b.year());
}
});
Six lines for the comparator, of which only one matters: the comparison function Integer.compare(a.year(), b.year()). With a lambda expression, you write the same thing in a single line:
books.sort((a, b) -> Integer.compare(a.year(), b.year()));
Everything the anonymous class spelled out, the compiler now derives from the context: sort() expects a Comparator<Book>, so a and b are books, and the expression to the right of the arrow is the return value of compare().
So before Java 8, we could pass behavior only indirectly – through an object with a method that implements that behavior. Since Java 8, we pass the behavior directly: instead of an anonymous class of six lines, we write a single-line lambda expression. The same method – but without the boilerplate around it.
That makes passing behavior as ordinary as passing a number, and the Stream API is built on it: methods like filter(), map(), and sorted() receive the behavior they apply as an argument. The three operations become three lines – the same pipeline with anonymous classes is one nobody would want to read.
The Syntax of Lambda Expressions
Every lambda expression has the form (parameters) -> body. To the left of the arrow stands the parameter list, to the right the body.
Parameters
The parameter list has zero, one, or more parameters:
// no parameter: empty parentheses
Supplier<Book> firstBook = () -> BOOKS.get(0);
// one parameter: the parentheses are optional
Predicate<Book> isGothic = book -> book.genre() == GOTHIC;
// several parameters: the parentheses are required
Comparator<Book> byYear = (a, b) -> Integer.compare(a.year(), b.year());
Supplier, Predicate, and Comparator are the types of these lambdas. What they are and where they come from is the subject of the chapter How the compiler determines the type of a lambda.
The Body: Expression or Block
The body is either a single expression or a block in braces:
// expression body: the value of the expression is the return value
Function<Book, String> title = book -> book.title();
// block body: statements in braces, and a `return` if a value is expected
Function<Book, String> label = book -> {
String decade = (book.year() / 10 * 10) + "s";
return book.title() + " (" + decade + ")";
};
For “Dracula” from the Library.BOOKS list, label returns Dracula (1890s).
A lambda that returns nothing – a Consumer or a Runnable, for example – has as its body either a block without a return – or an expression that could also stand as a statement: usually a method call, but an assignment or an increment like i++ as well.
Consumer<Book> print = book -> System.out.println(book.title());
As the body, I recommend the expression wherever it fits. A block body with more than two or three statements is a sign that the code belongs in a named method – more on that in the section Lambdas that are too long.
Parameter Types: Inferred, Explicit, var
In all examples so far, the compiler has inferred the parameter types – from Comparator<Book> it follows that a and b are books. Such a lambda is called implicitly typed. You may also write the types out, and then the lambda is explicitly typed:
Comparator<Book> byYear = (Book a, Book b) -> Integer.compare(a.year(), b.year());
When is that necessary? When the compiler cannot infer the type. A typical case is a chain of calls on a generic method:
// compiles
Comparator<Book> byYearAscending = Comparator.comparing(book -> book.year());
// does not compile
Comparator<Book> byYearDescending =
Comparator.comparing(book -> book.year()).reversed();
error: cannot find symbol
symbol: method year()
location: variable book of type Object
Comparator.comparing() is a generic method: the compiler infers the type of its parameter from the target type of the call – in the line without reversed(), that is the variable Comparator<Book>.
With reversed(), Comparator<Book> no longer belongs to the call of comparing() but to the result of reversed(). And that type information does not travel back to the left, to comparing(). So the compiler types comparing() on its own and substitutes Object – and an Object has no year() method.
An explicit parameter type or a method reference tells the compiler the type directly:
Comparator<Book> byYearDescending =
Comparator.comparing((Book book) -> book.year()).reversed();
Comparator<Book> byYearDescending =
Comparator.comparing(Book::year).reversed();
Since Java 11, you may write var instead of the type. That is not shorter than leaving the type out, and its purpose is a different one: var gives an annotation a place to sit without you having to write the type – (@Nonnull var book) -> …. With no type at all, you cannot write an annotation: (@Nonnull book) -> … is a syntax error.
The three forms cannot be mixed – either all parameters without a type, all with a type, or all with var.
Since Java 22, a parameter you do not use is written as an underscore:
Map<String, List<Book>> byAuthor = new HashMap<>();
for (Book book : BOOKS) {
byAuthor.computeIfAbsent(book.author(), _ -> new ArrayList<>()).add(book);
}
computeIfAbsent() passes the key to the lambda, and the lambda does not need it. The underscore says exactly that; with a name like key or ignored, you would have to check whether the parameter is used anywhere.
As a rule, I recommend the implicitly typed form, an explicit type only where the compiler cannot infer it, var only when a parameter needs an annotation, and the underscore for every parameter you do not use.
How the Compiler Determines the Type of a Lambda
On its own, a lambda expression has no type. Its type comes from the context in which it appears – the target type. There are four such contexts: an assignment, a method argument, a return statement, and a cast.
// assignment: the target type is the declared type of the variable
Predicate<Book> isGothic = book -> book.genre() == GOTHIC;
// method argument: the target type is the parameter type of filter()
Stream<Book> gothicBooks = BOOKS.stream().filter(book -> book.genre() == GOTHIC);
// return statement: the target type is the return type of the method
static Predicate<Book> publishedAfter(int year) {
return book -> book.year() > year;
}
// cast: the target type is the type in the cast
Object byYear = (Comparator<Book>) (a, b) -> Integer.compare(a.year(), b.year());
This is also why var does not work for a lambda:
var isGothic = book -> book.genre() == GOTHIC;
error: cannot infer type for local variable isGothic
(lambda expression needs an explicit target-type)
var would take the type from the lambda, and the lambda would take it from var. Neither has one.
Functional Interfaces
The target type has to be a functional interface: an interface with exactly one abstract method. An example of a functional interface is Comparator<Book>: Its abstract method is int compare(Book a, Book b). The compiler matches the lambda against that method: Accordingly, the lambda has to take two Book parameters, and its body has to produce an int.
The JDK ships with a package of general-purpose functional interfaces, java.util.function. Four of them cover most of what you will write:
| Interface | Method | The lambda … |
|---|---|---|
Predicate<T> | boolean test(T t) | takes a T and returns true or false |
Function<T, R> | R apply(T t) | takes a T and returns an R |
Consumer<T> | void accept(T t) | takes a T and returns nothing |
Supplier<T> | T get() | takes nothing and returns a T |
The package has more than 40 such interfaces – among others, with two parameters (e.g., BiFunction), with primitive types (e.g., IntPredicate), and with the same type in and out (e.g., UnaryOperator). Which one you need, how to combine them with andThen() and negate(), and how to write your own will get an article of its own.
Overloaded Methods
Suppose there are two methods named run(): One expects a Consumer<String>, the other a Function<String, String>. If you pass a lambda to run(), the compiler has to decide which of the two methods it calls. With an implicitly typed lambda (one that does not name the type), it cannot:
static void run(Consumer<String> consumer) { … }
static void run(Function<String, String> function) { … }
run(s -> s.trim()); // does not compile
error: reference to run is ambiguous
both method run(Consumer<String>) and method run(Function<String,String>) match
The lambda body s.trim() fits both: As a Function, it returns the trimmed string; as a Consumer, it calls trim() and discards the result.
Three ways of writing the lambda resolve the ambiguity:
- With an explicit parameter type, the compiler type-checks the body against both overloads and picks the one whose method returns a value:
run((String s) -> s.trim())calls theFunctionoverload. - A block without
return, likes -> { s.trim(); }, returns nothing and is therefore aConsumer. - A block with
return, likes -> { return s.trim(); }, returns a value and is therefore aFunction.
You will meet this case in the JDK at ExecutorService.submit(), which is overloaded with Runnable and Callable. submit(() -> "done") calls the Callable overload, because a string is not a statement and therefore cannot be the body of a Runnable.
Variable Access: Effectively Final, Scope, and this
Capturing Local Variables
A lambda can use the local variables and parameters of the method it appears in:
int minYear = 1890;
List<Book> recentBooks = BOOKS.stream()
.filter(book -> book.year() > minYear)
.toList();
The lambda captures minYear. The condition is that the variable is effectively final – that is, you never assign to it after its initialization. A variable that changes, on the other hand, cannot be captured by the lambda:
int count = 0;
BOOKS.stream().forEach(book -> count++); // does not compile
error: local variables referenced from a lambda expression must be final or effectively final
Why is that? A lambda may run later than the method that created it, and in another thread. The lambda therefore does not get a reference to the variable but a copy of its value, taken when the lambda is created. If the variable could change afterwards, the lambda would work with a stale value and the method with the current one. Java rules that out by forbidding the change.
You may have seen the workaround with an array of just one element or an AtomicInteger:
int[] count = {0};
BOOKS.stream().forEach(book -> count[0]++);
That compiles, because count itself is never reassigned – only its content changes. But the lambda now has a side effect on state outside the pipeline, and in a parallel stream, several threads write to that state at the same time, which could lead to race conditions.
An AtomicInteger would be thread-safe:
AtomicInteger count = new AtomicInteger();
BOOKS.stream().forEach(book -> count.incrementAndGet());
But I recommend not calling code with side effects from within a pipeline and letting the pipeline compute the value instead:
long count = BOOKS.stream().count();
No Shadowing
A lambda parameter cannot have the name of a local variable that is already in scope:
String s = "Dracula";
Function<String, String> trimmed = s -> s.trim(); // does not compile
error: variable s is already defined in method main(String[])
The body of a lambda is part of the scope it appears in, not a scope of its own. In this, a lambda differs from an anonymous class, whose method parameters may shadow the surrounding variables.
this Means the Enclosing Instance
The same rule applies to this: Inside a lambda, this is the instance of the enclosing class. Inside an anonymous class, this is the anonymous object:
public class Scope {
void run() {
Runnable lambda = () -> System.out.println(this.getClass().getName());
Runnable anonymous = new Runnable() {
@Override
public void run() {
System.out.println(this.getClass().getName());
}
};
lambda.run();
anonymous.run();
}
}
Scope
Scope$1
The lambda prints Scope, the name of the enclosing class. The anonymous class, on the other hand, prints its own class name, Scope$1, because inside it, this refers to the anonymous object.
For a lambda, that is convenient: You use the fields and methods of your class as you do anywhere else in it, and a method reference like this::describe works as well:
public class BookFilter {
private final int minYear;
public BookFilter(int minYear) {
this.minYear = minYear;
}
public List<String> recentTitles() {
return BOOKS.stream()
.filter(book -> book.year() > minYear)
.map(this::describe)
.toList();
}
private String describe(Book book) {
return book.title() + " (" + book.year() + ")";
}
}
The lambda in filter() reads the field minYear, and this::describe refers to the method describe() – both belong to the BookFilter instance on which you call recentTitles().
In an anonymous class, on the other hand, this::describe would not compile, because there the compiler looks for describe() in the anonymous object.
Method References
A lambda that does nothing but call one method – book -> book.title(), for example – can be written as a method reference: Book::title. The two colons separate the type or the object on the left from the method name on the right. A method reference leaves out the parameters, because the compiler knows them from the target type.
There are four kinds of method references, distinguished by what stands to the left of the colons and what happens with the lambda’s parameters:
| Kind | Syntax | Equivalent lambda |
|---|---|---|
| Static method | Type::staticMethod | x -> Type.staticMethod(x) |
| Instance method of a particular object | object::method | x -> object.method(x) |
| Instance method of an arbitrary object of a type | Type::method | x -> x.method() |
| Constructor | Type::new | x -> new Type(x) |
The following four sections show each of the four kinds first as a lambda and then as the method reference it becomes.
Reference to a Static Method
To the left of the colons stands a type, and the method is static. The parameters of the lambda become the arguments of that method.
In the following example, the second lambda passes each year of publication to the static method String.valueOf():
List<String> years = BOOKS.stream()
.map(book -> book.year())
.map(year -> String.valueOf(year))
.toList();
The method reference for it is String::valueOf:
List<String> years = BOOKS.stream()
.map(book -> book.year())
.map(String::valueOf)
.toList();
Reference to an Instance Method of a Particular Object
To the left of the colons stands an object, and the parameters become the arguments of the method called on that object.
In the following example, the lambda in forEach() passes each title to the method println() of the object System.out:
BOOKS.stream()
.map(book -> book.title())
.forEach(title -> System.out.println(title));
The method reference for it is System.out::println:
BOOKS.stream()
.map(book -> book.title())
.forEach(System.out::println);
The object may also be this or super: this::describe from the BookFilter example refers to a method of the current instance. With super:: instead of this::, the reference points to the superclass’s method – even if the current class overrides it.
Reference to an Instance Method of an Arbitrary Object of a Type
To the left of the colons stands a type, and the method is an instance method of that type. Unlike the two kinds before, the first parameter of the lambda is not passed as an argument here: It becomes the object the method is called on – the receiver – and only the further parameters become arguments.
In the following example, both lambdas call a method on their first parameter – title() on the book and compareToIgnoreCase() on the first of the two titles:
List<String> titles = BOOKS.stream()
.map(book -> book.title())
.sorted((a, b) -> a.compareToIgnoreCase(b))
.toList();
The method references for them are Book::title and String::compareToIgnoreCase:
List<String> titles = BOOKS.stream()
.map(Book::title)
.sorted(String::compareToIgnoreCase)
.toList();
With Book::title, the only parameter becomes the receiver. With String::compareToIgnoreCase, the first parameter becomes the receiver and the second the argument.
How do you tell this kind from a reference to a static method? Both have a type to the left of the colons; the difference is whether the method is static. String::valueOf is static, String::compareToIgnoreCase is not. The compiler checks that against the target type, and where a type has both a static and an instance method with the same name, that leads to the ambiguity described in the section Lambda or method reference.
Reference to a Constructor
Type::new stands for a lambda that creates a new object. Which constructor is meant follows from the parameters of the target type.
The following two lambdas each call a constructor – the first without a parameter, the second with one:
Supplier<List<Book>> newList = () -> new ArrayList<>();
Function<String, StringBuilder> builder = s -> new StringBuilder(s);
The method references for them are ArrayList::new and StringBuilder::new:
Supplier<List<Book>> newList = ArrayList::new;
Function<String, StringBuilder> builder = StringBuilder::new;
The same works for arrays, where the parameter is the array’s length. That is the form toArray() expects.
In the following example, a lambda creates the array that toArray() writes the titles into:
String[] titles = BOOKS.stream()
.map(book -> book.title())
.toArray(length -> new String[length]);
The method reference for it is String[]::new:
String[] titles = BOOKS.stream()
.map(book -> book.title())
.toArray(String[]::new);
Lambda or Method Reference?
A method reference fits when the lambda passes its parameters through to the method unchanged and in the same order. That is the case in all examples above, and then I recommend the reference: It names the method and nothing else, and it has no parameter names that could mislead.
A lambda fits in three cases. First, when the arguments are not just passed through – book -> book.year() > 1890 has a comparison, (a, b) -> b.compareTo(a) swaps the order. Second, when the lambda calls more than one method. And third, when the reference is ambiguous:
List<String> numbers = Stream.of(1, 2, 3)
.map(Integer::toString) // does not compile
.toList();
error: incompatible types: cannot infer type-variable(s) R
reference to toString is ambiguous
both method toString(int) in Integer and method toString() in Integer match
Integer has a static method toString(int) and an instance method toString(), and both fit a Function<Integer, String>. The lambda i -> i.toString() says which one you mean; so does String::valueOf.
Where Lambdas Are Used
The Stream API is the most visible place, but far from the only one. Wherever a method parameter has a functional interface as its type, a lambda or a method reference fits. In the Javadoc, you recognize such parameters by types like Predicate, Function, Comparator, and Runnable. The examples below are all from the JDK:
// Sorting with a Comparator
List<Book> books = new ArrayList<>(BOOKS);
books.sort(Comparator.comparing(Book::year));
// Collections: removeIf() and forEach()
books.removeIf(book -> book.year() < 1850);
books.forEach(book -> System.out.println(book.title()));
// Maps: computeIfAbsent() creates the value on the first access
Map<String, List<Book>> byAuthor = new HashMap<>();
for (Book book : BOOKS) {
byAuthor.computeIfAbsent(book.author(), _ -> new ArrayList<>()).add(book);
}
// Optional: map() and orElseGet() run only if a value is present or absent
String firstGothicTitle = BOOKS.stream()
.filter(book -> book.genre() == GOTHIC)
.findFirst()
.map(Book::title)
.orElseGet(() -> "no gothic novel found");
// Threads: Runnable
Thread thread = new Thread(
() -> System.out.println("running in " + Thread.currentThread().getName()));
thread.start();
The computeIfAbsent() example groups the books by author and creates the list for a name the first time that name appears. Its lambda uses the underscore from Java 22; on an older Java version, name the parameter author.
The same grouping is shorter with a stream and the groupingBy() collector, which creates the lists itself:
Map<String, List<Book>> byAuthor = BOOKS.stream()
.collect(Collectors.groupingBy(Book::author));
computeIfAbsent() remains the right choice when the map already exists and its entries arrive one at a time.
How the JVM Executes a Lambda
A lambda expression looks like syntax for an anonymous class, but it is compiled differently.
The following class contains a single lambda expression – we will use it to look at what the compiler makes of it:
public class LambdaDemo {
public static void main(String[] args) {
Predicate<Book> isGothic = book -> book.genre() == GOTHIC;
System.out.println(isGothic.getClass().getName());
}
}
The javac compiler generates one class file, LambdaDemo.class – no second one for the lambda, as it would for an anonymous class. javap -p shows what happened to the lambda’s body:
public class LambdaDemo {
public LambdaDemo();
public static void main(java.lang.String[]);
private static boolean lambda$main$0(Book);
}
The body became a private static method lambda$main$0() of the enclosing class, with the lambda’s parameter as its parameter. A lambda that uses this becomes an instance method instead.
And at the place where the lambda expression stood, the bytecode of main() contains a single instruction:
0: invokedynamic #7, 0 // InvokeDynamic #0:test:()Ljava/util/function/Predicate;
invokedynamic is an instruction whose target is decided at runtime, the first time it is executed. For a lambda, that decision is made by the JDK class LambdaMetafactory: It generates a class that implements Predicate, whose test() method calls lambda$main$0(), and creates an instance of it. Every further execution of the instruction reuses that class. The program above prints the generated class’s name:
LambdaDemo$$Lambda/0x000007f001040800
The class does not exist on disk and has no source file; the number changes with every run. What you see in a stack trace is lambda$main$0 – the section Reading lambdas in stack traces shows how.
That LambdaMetafactory generates this class and creates its instances has two consequences for your code.
First: A lambda that captures no variables is created only once – LambdaMetafactory returns the same instance for every execution of the invokedynamic instruction. A lambda that captures variables, on the other hand, is a new object each time, because the captured values are stored in it. In a hot loop, a capturing lambda therefore costs one small allocation per evaluation.
Second: For lambdas, equals() compares identity only. Two lambda expressions with the same body are two different objects, and equals() returns false for them.
This matters as soon as a collection has to find a lambda via equals() – when removing it from a list, for example:
Predicate<Book> after1890 = book -> book.year() > 1890;
Predicate<Book> sameCondition = book -> book.year() > 1890;
List<Predicate<Book>> filters = new ArrayList<>(List.of(after1890));
filters.remove(sameCondition); // false – same body, different object
filters.remove(after1890); // true
So you can remove a lambda only through the reference under which you added it. The same applies to listeners: If you unregister a listener with a lambda that looks the same, the one you originally registered stays active. For the same reason, lambdas are unsuitable as map keys.
Common Mistakes and Recommendations
Side Effects in Lambdas
A lambda that writes to a variable outside itself works – until the code runs in parallel. The array of just one element in the section Capturing local variables is the smallest example; filling an ArrayList from inside forEach() is the most common one I see. The Java Streams article shows the parallel case and its fix.
You are best off sticking to the following rule: A lambda computes a value from its parameters and changes nothing outside.
Checked Exceptions
A lambda may only throw the checked exceptions that the method of its functional interface declares. The interfaces in java.util.function declare none, and neither does Runnable:
Runnable pause = () -> Thread.sleep(1_000); // does not compile
error: unreported exception InterruptedException; must be caught or declared to be thrown
Catch the exception inside the lambda and wrap it in an unchecked one, or move the call into a helper method that does that. The Java Streams article shows the helper method for IOException.
Lambdas That Are Too Long
A block body that grows beyond two or three statements hides what the pipeline does:
List<String> labels = BOOKS.stream()
.map(book -> {
String decade = (book.year() / 10 * 10) + "s";
String authorInitials = Arrays.stream(book.author().split(" "))
.map(name -> name.substring(0, 1))
.collect(Collectors.joining());
return book.title() + " (" + authorInitials + ", " + decade + ")";
})
.toList();
Move the body into a named method and pass a method reference:
List<String> labels = BOOKS.stream()
.map(Library::label)
.toList();
The pipeline then stays on one level of abstraction: It no longer says how it does something, but what it does. And the method has a name and can be documented and tested on its own.
I recommend this whenever the block describes how something is done (and that is often the case from two lines on). A method name like label() says more than a block ever can.
Recursion in a Lambda
A lambda cannot call itself through the variable it is assigned to:
Function<Integer, Integer> factorial =
n -> n <= 1 ? 1 : n * factorial.apply(n - 1); // does not compile
error: variable factorial might not have been initialized
The variable has no value yet while the lambda is being created.
As a field, the lambda compiles if it calls itself through this:
private final Function<Integer, Integer> factorial =
n -> n <= 1 ? 1 : n * this.factorial.apply(n - 1);
There is no NullPointerException: The lambda does not read this.factorial when it is created, but only when it is called – and by then, the field has long held the lambda.
A named method that calls itself is simpler:
static int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
Recursion is a case for a method, not for a lambda.
Reading Lambdas in Stack Traces
An exception thrown inside a lambda shows the generated method name in its stack trace. This pipeline tries to parse every title as a number:
public class LambdaTrace {
public static void main(String[] args) {
List<Integer> numbers = BOOKS.stream()
.map(book -> Integer.parseInt(book.title()))
.toList();
}
}
Exception in thread "main" java.lang.NumberFormatException: For input string: "Pride and Prejudice"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:529)
at java.base/java.lang.Integer.parseInt(Integer.java:626)
at LambdaTrace.lambda$main$0(LambdaTrace.java:7)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:214)
…
at java.base/java.util.stream.ReferencePipeline.toList(ReferencePipeline.java:663)
at LambdaTrace.main(LambdaTrace.java:8)
The first line tells you what happened and why: a NumberFormatException, because Integer.parseInt() cannot read the string "Pride and Prejudice" as a number.
The three frames below it belong to the JDK: That is where Integer.parseInt() throws the exception. The first frame of your own class is LambdaTrace.lambda$main$0, the first lambda in main() – the number counts the lambdas of that method from zero, in the order the compiler meets them. LambdaTrace.java:7 is the line of the lambda.
The frames after that, down to main(), are the stream pipeline. To find the bug, three lines are enough: the first one with the exception and its cause, and the two frames of your own class (the fifth and the last line of the output above).
Summary
A lambda expression is a function without a name that you pass to a method: parameters, an arrow, a body. Its type is the functional interface the context expects, its parameters can be left untyped, and it can read the local variables around it as long as they are effectively final. Where a lambda only calls a method, the method reference is shorter and easier to read.
Three recommendations for everyday code:
- Keep lambdas short: As soon as a block describes how something is done, write a method and reference it.
- Keep lambdas free of side effects, so that they work sequentially and in parallel.
- Prefer a method reference wherever the lambda passes its parameters through to the method unchanged.
This article has covered the lambda expression itself. The functional interfaces it is typed with will get an article of their own, as will Optional. The most visible place for lambdas – the Stream API – is the subject of the Java Streams article.
What should I cover next? The best way for me to find out is through your feedback: a review on my ProvenExpert profile shows me which topics matter to you – and motivates me to write more articles.
Would you like to be notified when the next Java article is published? Then click here to join the HappyCoders newsletter.




