Java Lambda Expressions — A Practical Guide
My View on Lambdas
Having seen the evolution of Java over the years, I can confidently say that Lambdas and Streams have revolutionized the way we write code. I thought I would share my experience. When I was working as a software engineer, after I started using Lambdas and Streams, I almost became addicted to this style of coding — building functional pipelines felt like crafting beautiful logic flows. I love lambdas; they are cute, precise, and elegant to use. Having worked on older versions of Java, I felt a huge sense of relief and joy when I adopted lambdas — it was as if Java got a fresh new life.
Java’s introduction of Lambdas marked a paradigm shift in how we write and think about Java code. It transformed Java from an imperative to a hybrid functional language, allowing developers to focus more on what to do rather than how to do it. This evolution placed Java back in the forefront of modern programming languages, alongside Python and Kotlin. The simplicity, power, and expressiveness of Lambdas and Streams inspire developers to think differently, code smartly, and love Java all over again.
“Lambdas transformed Java from verbose to versatile — a leap that made coding not just efficient but joyful.”
1. What is a Lambda Expression?
A lambda expression is a short block of code that takes parameters and returns a value. It provides a clear and concise way to represent a single-method interface (a functional interface) as an expression. Lambdas enable functional-style programming in Java and make code more compact and readable—especially when used with collections and streams.
// Conceptual form
(parameters) -> { body }
// Example: a lambda that adds two integers
(int a, int b) -> a + b
2. How to create and use Lambda expressions
To use a lambda you need a functional interface — an interface with exactly one abstract method. Java provides the @FunctionalInterface annotation to make intent explicit, though it's optional.
@FunctionalInterface
interface Operation { int apply(int a, int b); }
class LambdaExample {
public static void main(String[] args) {
Operation sum = (a,b) -> a + b; // inferred types
Operation mul = (a,b) -> { return a * b; }; // block body with return
System.out.println("Sum: " + sum.apply(5, 7)); // Sum: 12
System.out.println("Mul: " + mul.apply(5, 7)); // Mul: 35
}
}
Use lambdas with collection APIs — e.g., List.forEach, Stream.map, filter, etc.
List names = List.of("Alice","Bob","Charlie");
names.stream()
.filter(n -> n.length() > 3)
.map(n -> n.toUpperCase())
.forEach(System.out::println);
3. Passing Lambdas as parameters & returning Lambdas from methods
Lambdas are objects — you can pass them as parameters or return them from methods using functional interfaces.
// Passing a lambda
public static int operate(int a, int b, Operation op) {
return op.apply(a,b);
}
// Returning a lambda
public static Operation makeAdder(int x) {
return (a,b) -> a + b + x; // captures x (effectively final)
}
// Usage
Operation adder = makeAdder(10);
System.out.println( operate(2,3, adder) ); // prints 15
4. java.util.function — Predicate, Function, Supplier, Consumer
Java provides common functional interfaces in java.util.function. Learn their purpose with compact examples.
// Predicate - boolean test(T t)
Predicate isLong = s -> s.length() > 5;
System.out.println(isLong.test("NBKRIST")); // true
// Function - R apply(T t)
Function len = s -> s.length();
System.out.println(len.apply("Lambda")); // 6
// Supplier - T get()
Supplier random = () -> Math.random();
System.out.println(random.get()); // e.g., 0.345
// Consumer - void accept(T t)
Consumer printer = s -> System.out.println(">> " + s);
printer.accept("Learn Lambdas");
They compose nicely. Example: combine Predicates or use Function chaining.
Predicate p1 = s -> s.startsWith("A");
Predicate p2 = s -> s.endsWith("e");
Predicate combined = p1.and(p2);
System.out.println(combined.test("Alice")); // true
Function plus2 = x -> x + 2;
Function times3 = x -> x * 3;
Function pipeline = plus2.andThen(times3);
System.out.println(pipeline.apply(4)); // (4+2)*3 = 18
5. Creating Threads with Lambdas & Comparing Strings
Lambda makes creating Runnable instances concise. Also, lambdas pair nicely with Comparator for sorting.
// Runnable via lambda
Runnable job = () -> {
System.out.println("Working in: " + Thread.currentThread().getName());
};
Thread t = new Thread(job);
t.start();
// Comparator for strings (case-insensitive)
Comparator ci = (a,b) -> a.compareToIgnoreCase(b);
List s = new ArrayList<>(List.of("Venkat","Raju","Chiru","Anil"));
s.sort(ci);
s.forEach(System.out::println); //result: Anil, Chiru, Raju, Venkat
Also showing a short example of using lambda to compare by length:
// Comparator by length
Comparator byLen = (a,b) -> Integer.compare(a.length(), b.length());
List names = List.of("Rajani","Chiru","Amit","Venkat");
List sorted = new ArrayList<>(names);
sorted.sort(byLen);
// Result: [Amit, Chiru, Rajani, Venkat]
6. Best Practices & Common Pitfalls
- Prefer meaningful variable names over single letters in complex lambdas.
- Keep lambdas short—if logic grows complex, extract to a named method or class.
- Be aware of captured variables: they must be effectively final.
- Avoid side-effects inside functional pipelines; prefer pure functions where possible.
7. Quick Hands-on Exercises
1. Write a
Predicate<Integer> that checks prime numbers.
Hint: Use IntStream.rangeClosed(2, n-1) and noneMatch.
2. Create a
Function<String,String> that reverses a string.
Hint: Use new StringBuilder(s).reverse().toString().
3. Produce a
Supplier<LocalDate> that returns current date.
Hint: () -> LocalDate.now().
Key Summary
- Lambdas provide a compact way to represent functional interfaces.
- Use
java.util.function for common patterns (Predicate, Function, Supplier, Consumer).
- Pass and return lambdas from methods to write flexible APIs.
- Prefer clarity—extract complex logic out of lambdas.
- Practice with small exercises to build fluency and confidence.