Lambda expression in Java 8
-
Last Updated: July 30, 2023
-
By: javahandson
-
Series
Learn Java in a easy way
A lambda expression in Java 8 is a short block of code that you can pass around like a value. It takes parameters, runs some logic, and hands back a result. The twist is that it has no name and no class wrapped around it.
Before Java 8, passing behaviour into a method felt clumsy. You wrote a whole anonymous class just to hand over three lines of logic. The useful part hid inside a pile of braces and boilerplate.
Java 8 arrived in March 2014 and changed that. It let you write just the logic, with an arrow in the middle. Suddenly a five-line block shrank to a single readable line.
Think of a lambda as a recipe card. The card does not cook anything by itself. You hand it to someone, and they follow the steps whenever they are ready.
That small shift opened the door to the Streams API, cleaner event handlers, and far tidier collection code. Almost every modern Java library leans on lambdas somewhere.
We will start with the shape of a lambda and work steadily toward the rules that trip people up. Here is the plan:
this behaves differently in a lambda than in an anonymous classYou only need basic Java to follow along. If you have written a class and called a method, you are ready.
[IMAGE PLACEHOLDER 1: diagram showing the three parts of a lambda – parameters, arrow token, body]
A lambda expression is a method stripped down to its essentials. It keeps the parameters and the body. It drops the name, the return type, and the modifiers.
Look at an ordinary method that adds two numbers:
public int add(int a, int b) {
return a + b;
}Now strip away everything the compiler can work out on its own. The name goes. So does the return type. Even the public keyword disappears.
(a, b) -> a + b
Same logic, one line. That is a lambda expression. People sometimes call it an anonymous function, and the label fits nicely.
Java has always let you pass data into a method. An int, a String, a list of orders. All easy.
Passing behaviour was the hard part. Say you want a sort method that works for any rule. The rule itself has to travel into the method somehow.
Before Java 8, you wrapped that rule in an object. The wrapper added noise and hid the one line you actually cared about. We walk through that whole journey in passing code as a parameter.
Lambdas removed the wrapper. Now the rule travels on its own, and the reader sees the logic straight away.
A few myths follow lambdas around. Clearing them up early saves confusion later.
That last one matters. Lambdas make code shorter and clearer, and clarity is the real prize here.
Every lambda has exactly three pieces. Learn these and the rest falls into place.
-> symbol that splits inputs from logicRead the arrow out loud as “goes to” or “becomes”. So (a, b) -> a + b reads as “a and b become a plus b”.
A lambda body comes in two flavours. The choice depends on how much work you need.
With a single expression, skip the braces. The value of that expression becomes the return value automatically, and you never type return.
(a, b) -> a + b // returns a + b, no return keyword name -> name.length() // returns the length () -> "hello" // returns the String
Need several statements? Wrap them in braces. Once you open a brace, the automatic return disappears, so type return yourself.
(a, b) -> {
int sum = a + b;
System.out.println("Adding " + a + " and " + b);
return sum; // required inside a block body
}One handy exception applies. When the method returns void, drop the return entirely.
Parameter syntax bends in a few places, which confuses people at first. These four rules cover everything.
() -> 42x -> x * 2(int x) -> x * 2Types are optional because the compiler already knows them. It reads the interface method you are implementing and fills them in. We call that type inference.
// all three lines mean the same thing
Calculator c1 = (int a, int b) -> a + b; // explicit types
Calculator c2 = (a, b) -> a + b; // inferred types
Calculator c3 = (a, b) -> { return a + b; };One rule has no exceptions: never mix styles. Either every parameter carries a type or none of them do.
Since Java 11 you may write var in a lambda parameter list. It buys you nothing on its own, but it gives annotations somewhere to sit.
// Java 11 and later Calculator c = (var a, var b) -> a + b; // useful when you need an annotation Calculator d = (@NonNull var a, var b) -> a + b;
The all-or-nothing rule still applies here. Use var for every parameter or for none. Our guide to var in Java covers the keyword in depth.
Here is a question worth pausing on. If a lambda has no name and no declared type, what type does it have?
By itself, none. A lambda borrows its type from wherever you put it. The compiler looks at the target, then decides. That mechanism has a name: target typing.
The target must be an interface with exactly one abstract method. Java calls that a functional interface, and the lambda becomes the implementation of that single method.
@FunctionalInterface
public interface Calculator {
int calculate(int operand1, int operand2);
}
// the lambda supplies the body of calculate
Calculator addition = (a, b) -> a + b;
System.out.println(addition.calculate(10, 20)); // Output: 30Why does exactly one abstract method matter? With two, the compiler could not tell which one your lambda implements. One method removes the guesswork.
Java ships plenty of ready-made functional interfaces, so you rarely write your own. Have a look at the predefined functional interfaces and the two-argument versions when you need one.
Target typing leads to a neat result. Identical lambda text can produce two completely different types.
interface Adder { int apply(int a, int b); }
interface Combiner { int merge(int x, int y); }
Adder adder = (a, b) -> a + b; // type is Adder
Combiner combiner = (a, b) -> a + b; // type is CombinerBoth lambdas look the same on the page. The variable on the left decides everything, which is exactly what target typing means.
Some targets simply will not accept a lambda. Knowing them saves a puzzling compile error.
var declaration gives the compiler no target at allDefault and static methods on an interface do not count toward the total. Only abstract methods do, so an interface can hold many default methods and still work fine. Our interface in Java guide explains those method kinds.
Watching a lambda emerge from an anonymous class makes the syntax click. Start with the old way of implementing our Calculator:
package com.javahandson;
public class Test {
public static void main(String[] args) {
Calculator calculator = new Calculator() {
@Override
public int calculate(int operand1, int operand2) {
return operand1 + operand2;
}
};
System.out.println(calculator.calculate(10, 20)); // Output: 30
}
}Seven lines to say “add two numbers”. Count how much of that text carries real meaning. Only operand1 + operand2 does.
Now remove anything the compiler can figure out. The variable type on the left already says Calculator, so new Calculator() repeats itself. Delete it.
The interface holds one abstract method, so naming calculate tells us nothing new. Delete the name, the return type, and @Override as well.
// after removing the redundant parts
Calculator calculator = (int operand1, int operand2) {
return operand1 + operand2;
}; // not valid yet - nothing separates inputs from bodySomething is missing. Nothing marks where the parameters stop and the logic starts. The arrow token fills that gap.
Drop the arrow in and the code compiles.
Calculator calculator = (int operand1, int operand2) -> { return operand1 + operand2; };Two more trims remain. The compiler infers the parameter types, and a single expression needs neither braces nor return.
Calculator calculator = (operand1, operand2) -> operand1 + operand2;
Seven lines became one. Nothing meaningful vanished along the way, and every deletion removed pure repetition.
Here comes the idea that explains most lambda errors. A lambda does not open a new scope. It lives inside the scope that surrounds it.
An anonymous class behaves differently. It creates a fresh scope, so a variable declared inside it can safely reuse an outer name.
A lambda gets no such privilege. Reuse a name and the compiler treats it as a duplicate declaration.
int value = 10; // compile error: variable value is already defined Calculator bad = (value, b) -> value + b; // fine - different parameter name Calculator good = (a, b) -> a + b + value;
Java calls this lexical scoping. Read the lambda as a plain block of code sitting where you wrote it, and the behaviour stops feeling strange.
A lambda may read local variables from the method around it. One condition applies: each captured variable must be final or effectively final.
What does effectively final mean? The variable never changes after its first assignment. You did not type final, yet the variable behaves as though you had.
int base = 100; // never reassigned - effectively final Calculator ok = (a, b) -> a + b + base; // compiles fine int counter = 0; counter = counter + 1; // reassignment breaks the rule Calculator broken = (a, b) -> a + counter; // error: local variables referenced from a lambda expression // must be final or effectively final
Notice what triggers the error. Reading counter was never the problem. Changing it was.
Adding the final keyword makes the rule explicit and documents your intent. Our final keyword in Java guide goes deeper on that.
The rule looks arbitrary until you see where local variables live. Every local variable sits on the stack frame of its method.
That frame disappears the moment the method returns. A lambda, though, can easily outlive the method that created it. Store it in a field or hand it to another thread, and it keeps running long after.
So the lambda cannot point at the original variable. Java copies the value instead, right when it builds the lambda.
Now picture a variable that keeps changing. The copy inside the lambda would drift away from the real one, and two versions of the same name would disagree. Freezing the value sidesteps that whole mess.
Only local variables carry the effectively final rule. Instance fields and static fields escape it completely, and a lambda may change them freely.
package com.javahandson;
public class Counter {
private int total = 0; // instance field
public void run() {
Runnable task = () -> {
total = total + 1; // allowed - fields are fair game
System.out.println("Total is " + total);
};
task.run(); // Output: Total is 1
task.run(); // Output: Total is 2
}
}Why the difference? Fields belong to an object on the heap, not to a stack frame. The object survives as long as something references it, so the lambda reaches the real field every time.
Developers sometimes dodge the rule with a one-element array. The array reference never changes, so the compiler stays quiet while the contents move.
int[] counter = {0}; // the reference is effectively final
Runnable task = () -> counter[0]++; // compiles, but think twice
task.run();
System.out.println(counter[0]); // Output: 1Yes, it compiles. That does not make it wise. The trick hides shared mutable state behind a technicality, and it breaks badly once several threads touch the same array.
Reach for a return value instead. When you genuinely need a shared counter, pick AtomicInteger, which handles concurrent updates properly.
Scope sharing has one more consequence, and it catches almost everybody once. Inside a lambda, this points at the enclosing object.
The lambda never becomes a separate object with its own identity. Writing this inside one means exactly what it would mean just outside it.
package com.javahandson;
public class Demo {
private String name = "Demo object";
public void show() {
Runnable task = () -> System.out.println(this.name);
task.run(); // Output: Demo object
}
}Notice that this.name reads the field of Demo. The lambda contributes no identity of its own. Our guide to the this and super keywords covers the keyword itself.
Swap the lambda for an anonymous class and the meaning shifts. An anonymous class really is a new object, so this refers to that object.
package com.javahandson;
public class Demo {
private String name = "Demo object";
public void show() {
Runnable task = new Runnable() {
private String name = "Runnable object";
@Override
public void run() {
System.out.println(this.name); // the anonymous object
System.out.println(Demo.this.name); // the outer object
}
};
task.run();
// Output: Runnable object
// Output: Demo object
}
}Reaching the outer object needs the Demo.this form. That extra syntax exists precisely because the anonymous class shadows the outer this. Lambdas never need it. If inner classes are new to you, start with nested and inner classes in Java.
The lambda rule turns out to be the friendlier one. Ask what a lambda is meant to be: a chunk of behaviour, not a thing.
An anonymous class carries baggage. It holds fields, it holds identity, and it quietly changes what this means. Bugs used to hide in exactly that gap.
Java designers made lambdas transparent instead. Move a line of code into a lambda and its meaning stays put, which makes refactoring far safer.
Both tools pass behaviour around, yet they differ in ways that matter. This table sums up the whole comparison.
| Aspect | Lambda expression | Anonymous inner class |
|---|---|---|
| Valid target | Functional interface only | Any interface or abstract class |
| Abstract methods allowed | Exactly one | Any number |
| Meaning of this | The enclosing object | The anonymous object itself |
| Scope | Shares the enclosing scope | Creates a brand new scope |
| Name shadowing | Not permitted | Permitted |
| Own fields | Cannot declare any | Can declare fields |
| Extra class file | None generated | Generates Outer$1.class |
| Compiled form | invokedynamic instruction | A real separate class |
| Typical length | One line | Five to seven lines |
The last two rows deserve a note. Javac turns your lambda body into a hidden method and links it at runtime through invokedynamic. No extra class file appears on disk.
Anonymous classes work the old way. Each one compiles into its own file, so ten anonymous classes leave ten extra files in your build folder.
So which should you pick? Choose a lambda whenever the target is a functional interface. Fall back to an anonymous class only when you need fields, several methods, or an abstract class.
This one tops the list. A loop counter or an accumulator changes value, and the compiler blocks the capture.
int sum = 0; List<Integer> numbers = List.of(1, 2, 3); numbers.forEach(n -> sum += n); // error: must be final or effectively final
Do not fight it with an array. Ask the collection for a total instead, or accumulate into a field that you own.
Old habits from anonymous classes cause this. Since a lambda shares its enclosing scope, every name inside it must stay unique.
int x = 5; Calculator wrong = (x, y) -> x + y; // error: x is already defined Calculator right = (a, b) -> a + b; // pick a fresh name
Half-typed parameter lists never compile. Commit to one style across the whole list.
Calculator bad = (int a, b) -> a + b; // error Calculator ok1 = (int a, int b) -> a + b; // all typed Calculator ok2 = (a, b) -> a + b; // none typed
Adding braces quietly switches off the automatic return. Many people add a print statement, then wonder why compilation breaks.
Calculator bad = (a, b) -> {
a + b; // error: not a statement, nothing returned
};
Calculator good = (a, b) -> {
System.out.println("adding");
return a + b; // explicit return
};Remember that a lambda owns no type of its own. Declaring with var leaves the compiler nothing to infer from.
var f = (a, b) -> a + b; // error: cannot infer type Calculator f2 = (a, b) -> a + b; // give it a target type
People converting old anonymous classes hit this regularly. Inside a lambda, this jumps straight to the enclosing object.
Watch for it in listeners and callbacks. Code that once read a field on the anonymous object will now read a field on the outer class, and it may compile happily while behaving differently.
Let us pull the ideas together in one small program. A shop applies different discount rules, and each rule is just behaviour, which makes it perfect lambda territory.
package com.javahandson;
@FunctionalInterface
interface DiscountRule {
double apply(double price);
}
public class Shop {
private int rulesApplied = 0; // field, so a lambda may change it
public double checkout(double price, DiscountRule rule) {
rulesApplied++;
return rule.apply(price);
}
public static void main(String[] args) {
Shop shop = new Shop();
double threshold = 500; // effectively final, safe to capture
DiscountRule flatTen = price -> price - 10;
DiscountRule bigSpender = price -> {
if (price > threshold) {
return price * 0.8; // 20 percent off
}
return price;
};
System.out.println(shop.checkout(100, flatTen)); // Output: 90.0
System.out.println(shop.checkout(600, bigSpender)); // Output: 480.0
System.out.println(shop.checkout(300, bigSpender)); // Output: 300.0
System.out.println(shop.rulesApplied); // Output: 3
}
}Several rules from this article show up in those thirty lines. Walk through them one at a time.
flatTen uses an expression body, so its result returns on its ownbigSpender needs an if, which forces braces and an explicit returnthreshold never changes, and that makes the capture legalrulesApplied increments happily, since fields skip the effectively final rulecheckout as arguments, exactly like data wouldLook at that last point again. The checkout method knows nothing about discounts. Callers supply the rule, so adding a festive offer never touches checkout at all.
Streams push this same idea much further. Once you feel comfortable here, filtering in streams shows lambdas doing real work.
A: A lambda expression is a short block of code with parameters and a body, but no name. It supplies the implementation of the single abstract method on a functional interface, which lets you pass behaviour into a method the way you would pass data.
A: An effectively final variable gets one assignment and never changes afterwards, even though nobody typed the final keyword. A lambda may capture such a variable. Reassign it anywhere in the method and the capture stops compiling.
A: Local variables live on the stack frame of their method, and that frame vanishes when the method returns. A lambda can outlive the method, so Java copies the value rather than pointing at the original. Freezing the variable keeps the copy and the original in agreement.
A: Yes. The effectively final rule covers local variables only. Instance fields and static fields live on the heap alongside their object, so a lambda reaches the real field and may change it as often as it likes.
A: It refers to the enclosing object, the same one you would get just outside the lambda. Lambdas never gain an identity of their own, so they borrow this from the class around them.
A: An anonymous inner class creates a genuine new object, so this points at that object. Reaching the outer instance needs the Outer.this form. A lambda skips all of that because it shares the scope around it.
A: No. A lambda shares the enclosing scope instead of opening a new one, so the compiler sees a duplicate declaration and reports an error. Anonymous inner classes do open a new scope, which is why they can shadow outer names.
A: No. A lambda takes its type from the target on the left, and var offers no target to work with. Declare the variable with a functional interface type instead. Note that var may still appear inside the parameter list from Java 11 onward.
A: No. Javac places the lambda body in a hidden method and emits an invokedynamic instruction that links it at runtime. An anonymous inner class, by contrast, compiles into its own file such as Outer$1.class.
A: Pick an anonymous class when the target is an abstract class, when the interface declares more than one abstract method, or when you need fields and internal state. Everything else reads better as a lambda.
Let us wrap up what we covered. A lambda expression is a nameless method built from three parts: parameters, an arrow, and a body.
Its body takes one of two shapes. A single expression returns its value automatically, while a braced block wants an explicit return.
A lambda owns no type on its own. It borrows one from the target you assign it to, and that target must be an interface with exactly one abstract method.
Scope is where the real subtleties live. A lambda shares the scope around it, so parameter names must stay unique and captured local variables must be effectively final. Fields dodge that rule entirely, because they live on the heap.
The keyword this follows the same logic. It means the enclosing object, never the lambda, which keeps refactoring predictable.
Start using them on small things. Convert one anonymous class, then another. The syntax becomes second nature quickly, and your code gets shorter along the way.