Lambda expression basics
Let’s look at a traditional method first before moved to Lambda expressions.
Without lambda
public void hello_world_method(){
System.out.println("Hello world");
}
In Lambda we could get rid of following.
Access modifiers like public. private, protected.
No need to define return type like String , int, void etc
No need to define method name since we assign to a variable -aBlockOfCode
If it’s just one line only, can get rid of {} too.
If it’s just one line only , can get rid of return statement too.
(int a, int b) -> a + b;we can omit the variable type in a Java lambda expression when there’s only one parameter or mutiple parameters with the same data type.
Adder adder = (a, b) -> a + b;However for multiple parameters with different data type, you must explicitly specify their types, e.g., (int x, String y) -> x + y.
if it’s just one variable, we can omit the () parentheses around the parameter
(a) -> a * 2; can be shortend as a -> a * 2;
The parentheses can be omitted only when there’s a single parameter. For multiple parameters, parentheses are required.
So after removing all of this , to make a lambda expression
-> add this between () amd {} so it’s a lambda.
So above code can be written after Java 8.
aBlockOfCode= () -> {Systrm.out.print(""Hello world)}
// If it is one line can remove {} too.
hello_world_Function = () -> System.out.print("Hello world");
Why no access modifiers?
Lambda expressions aren’t tied to specific classes.
They represent standalone code blocks that can be assigned to variables.
Focus on Functionality: Lambdas emphasize “what to do” rather than “who can access it,” aligning with functional programming principles.
No need to define return type
Lambda expressions always target a specific functional interface, which has a single abstract method (SAM).
The SAM already defines the return type for the method that the lambda implements.
This implicit association eliminates the need to reiterate the return type in the lambda expression itself.
Why we can omit the variable type in a Java lambda expression when there’s only one parameter or multiple parameters but the same type ?
interface Adder {
int add(int a, int b);
}
public class LambdaExample {
public static void main(String[] args) {
// add two numbers, no need variable type since both are int
Adder adder = (a, b) -> a + b;
}
The compiler relies on the context in which the lambda expression is used and looks at the signature of the functional interface’s abstract method to infer the type of the parameters.
In this case, since IntMultiplier specifies the multiply method with an int parameter, the compiler understands that the lambda expression should also take an int parameter.
No need of method name
when using lambda expressions, you often don’t need to define a separate method if the lambda expression captures the behaviour you want.
Instead, you can directly assign the lambda expression to a variable of a functional interface type, as demonstrated in the example above.
Before go ahead , be aware of below limitations and disadvantages of them.
limitations and disadvantages of Lambda expressions
Limited to functional interfaces.
lambdas require a functional interface as their target. They can’t function independently without being associated with a pre-defined interface or a new one you create.
The interface dictates the expected behaviour and signature (name, parameter types, and return type) of the lambda.
Also it has below limitations.
Readability and Understanding:Lambda expressions may reduce readability, especially for complex logic.
Debugging lambda expressions: can be challenging, with less informative stack traces.
Inability to Reuse Code:
Complex logic inside lambda expressions may hinder code reuse; separate methods are more suitable.
Capture of Variables:
Lambda expressions can only capture effectively final or final variables, limiting modification.
Performance Overheads:
While optimised, lambda expressions may incur slight performance overhead compared to traditional methods.
Compatibility:Lambda expressions are available from Java 8 onward, limiting compatibility with older Java versions.
Before learning further , let’s recap what is Functional interface means?
A functional interface is an interface that contains only one abstract method.
AKA SAM Single abstract method interface.
However, still can have static methods,default methods.
Mark the interface with @FunctionalInterface, so other developers won’t add any abstract method.
They can have only one functionality to exhibit.
From Java 8 onwards, lambda expressions can be used to represent the instance of a functional interface.
@FunctionalInterface
public interface FunctionalInterfaceExample {
// This is the only abstract method.
public void greetings();
// In interfaces, you don't need to explicitly declare methods as abstract
// static method
static void hello() {
System.out.println("Hello, New Static Method Here");
}
// default method
default void show()
{
System.out.println("Default Method Executed");
}
}
Quick recap- What is abstract method?
Abstract method is a method declared without a body — it only has a signature, specifying its name, parameters, and if needed return type.
Abstract Keyword: The abstract keyword is used to declare abstract methods.(No need to say as abstract in interfaces but in abstract classes, it is a must)
No Body: Abstract methods end with a semicolon, indicating no implementation within curly braces.
Abstract Classes: They reside within abstract classes, which cannot be instantiated directly.
abstract class MyAbstractClass {
abstract void myAbstractMethod(); // Abstract method
void myConcreteMethod() {
// Concrete method with an implementation
System.out.println("This is a concrete method.");
}
}
The type of the lambda expression
When using lambda expressions in Java, you can define the type of the lambda expression using either a custom functional interface or a built-in functional interface.
So let’s look at some of the examples
Challenge- lambda expression to int a multiplied by 2 and return the answer
// Define a functional interface (not necessary if you're using a built-in functional interface)
interface IntMultiplier {
int multiply(int a);
// Here we need to define asits acceepts a variable,name of it can be anything.
}
public class LambdaExample {
public static void main(String[] args) {
// Lambda expression to multiply an integer by 2
IntMultiplier multiplier = a -> a * 2;
// Example usage
int result = multiplier.multiply(5); // Replace 5 with your desired integer
System.out.println("Result: " + result);
}
}
Add two numbers and return the answer
// Define a functional interface (not necessary if you're using a built-in functional interface)
interface Adder {
int add(int a, int b);
}
public class LambdaExample {
public static void main(String[] args) {
// Lambda expression to add two numbers
Adder adder = (a, b) -> a + b;
// Example usage
int result = adder.add(3, 7); // Replace 3 and 7 with your desired numbers
System.out.println("Result: " + result);
}
}
The same thing is achieved using Java inbult interfaces as below.
package com.lambda.expressions;
import java.util.function.IntBinaryOperator;
import java.util.function.IntUnaryOperator;
public class LambdaWithTypeExample {
public static void main(String[] args) {
// Lambda expression using a built-in functional interface (IntBinaryOperator)
IntBinaryOperator addition =(int a,int b)-> a+b;
int result=addition.applyAsInt(3,5);
System.out.println(result+ "Two numbers were added using applyAsInt method in IntBinaryOperator ");
// Lambda expression using a built-in functional interface (IntUnaryOperator)
IntUnaryOperator multiplier = a -> a * 2;
// Example usage
result = multiplier.applyAsInt(5); // Replace 5 with your desired integer
System.out.println("Result: " + result);
}
}
What is IntBinaryOperator that we used in above code?
It’s a functional interface, meaning it has only one abstract method.
It’s located in the java.util.function package.
It represents an operation that takes two int values as input and produces an int result.
It has a single abstract method called applyAsInt(int left, int right).
@FunctionalInterface
public interface IntBinaryOperator {
int applyAsInt(int left, int right);
}
What is IntUnaryOperator?
It is designed to represent an operation that takes a single int operand and produces an int result.
The functional method of this interface is called applyAsInt
@FunctionalInterface
public interface IntUnaryOperator {
int applyAsInt(int operand);
}
Let’s create a lambda expreession using Runnable interface.
Runnable is a good candidate for lambda expressions in Java because it is a functional interface, meaning it has only one abstract method (run())
package com.lambda.expressions;
public class RunnableExample {
public static void main(String[] args) {
/*
Creating a Runnable using a lambda expression
The lambda expression represents the run() method of the Runnable interface
*/
Runnable myRunnable=new Runnable() {
@Override
public void run() {
for(int i=0;i<4;i++){
System.out.println("i is-"+i);
}
}
};
// Creating a Thread and passing the Runnable to its constructor
// The Thread class takes a Runnable as a parameter
Thread myThread = new Thread(myRunnable);
// Starting the Thread
// This invokes the run() method of the Runnable in a separate thread
myThread.start();
}
}
Consumer functional interface
The Consumer<T> functional interface in Java is specifically designed for operations that accept a single argument of type T and perform some action without returning a value.
accept(T t): This single abstract method takes an object of type T as input and performs the intended action on
it. It doesn’t return any value.
package com.lambda.expressions;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
public class ConsumerLambdaExample {
public static void main(String[] args) {
// Create a list of strings
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// Using a lambda expression with Consumer to print each name
Consumer<String> printName = name -> System.out.println("Hello, " + name);
// Applying the Consumer to each element in the list
names.forEach(printName);
// Or can directly write lambda expression.
names.forEach(name -> System.out.println("Hello, " + name));
}
}
Exception handling in Lambda expressions
For that , we can use a try catch block in lambda expression.
package com.lambda.expressions;
@FunctionalInterface
interface Divider {
double divide(int a, int b) throws ArithmeticException;
}
public class CannotDivideByZeroCustom {
public static void main(String[] args) {
Divider divider = (a, b) -> {
try {
return a/b; // If there is no error divided value will be returned.
}
catch (ArithmeticException e){
// Handle the exception (e.g., print a message)
System.err.println("Error: Division by zero");
throw e; // Propagate the exception or provide a default valu
}
};
System.out.println( divider.divide(10,0));
}
}
As you can see , in above lambda expression is long due to try , catch block.
We can wrap it in another merhod if needed like below.
public class CannotDivideByZeroCustomWrapperLambda {
public static void main(String[] args) {
Divider divider = (a, b) -> handleDivision(a, b);
// lambda expression is using the handleDivision method to perform the division operation with parameters.
System.out.println(divider.divide(10, 0)); // Example usage
}
// Wrapper method that handles division and exception
private static double handleDivision(int a, int b) {
try {
return a / b; // If there is no error, the divided value will be returned.
} catch (ArithmeticException e) {
// Handle the exception (e.g., print a message)
System.err.println("Error: Division by zero");
return Double.NaN; // Provide a default value or handle the exception accordingly
}
}
}
This reference in Lambda
this reference behavior in Java lambdas:
Inherits from Enclosing Scope: In Java, lambdas don’t create their own this context. They inherit this from the enclosing scope,
which is typically the instance of the surrounding class.
No Separate this for Lambda: There’s no separate this specifically for the lambda itself. It always refers to the instance of the class where the lambda is defined.
Implicit Capture: If a lambda needs to access members (fields or methods) of the enclosing instance, it implicitly captures this from that instance.
Inherits from Enclosing Scope:
Lambdas in Java inherit the this reference from their enclosing scope, which is typically the instance of the surrounding class.
In the given example (ThisLambdaExample class), the lambda expression inherits the this reference from the instance of ThisLambdaExample.
No Separate this for Lambda:
There is no separate this specifically for the lambda itself.
The this reference within the lambda always refers to the instance of the class where the lambda is defined. In the code,
when the lambda expression accesses this.message or this.age, it is referring to the instance variables of the ThisLambdaExample class.
Implicit Capture:
If a lambda needs to access members (fields or methods) of the enclosing instance, it implicitly captures this from that instance.
In the code, when the lambda expression accesses this.message or this.age, it is implicitly capturing the this reference from the enclosing instance of ThisLambdaExample.
package com.lambda.expressions;
// Functional Interface with a single abstract method
interface MyFunctionalInterface {
void myMethod();
}
// In Java, the "this" keyword is a reference to the current instance of the class.
// Lambdas don't create their own this reference.
public class ThisLambdaExample {
private String message = "Hello from the enclosing class!";
private int age=18;
public static void main(String[] args) {
ThisLambdaExample lambdaExample = new ThisLambdaExample();
lambdaExample.doSomething();
}
public void doSomething() {
// Lambda expression using 'this' reference
MyFunctionalInterface myFunc = () -> {
// Accessing instance variable from the enclosing class
System.out.println("Message from lambda: " + this.message);
System.out.println("Age from Lambda: "+age); // prints the lambdaExample reference
System.out.println(this); // prints the lambdaExample reference
};
// Calling the lambda expression
myFunc.myMethod();
}
}
output-
Message from lambda: Hello from the enclosing class!
Age from Lambda: 18
com.lambda.expressions.ThisLambdaExample@6e2c634b
Method references in Lambda
You can use method references in Java lambda expressions when both the lambda and the method it calls have no arguments
An explanation of method references in Java:
What They Are:
Method references are a concise way to create lambda expressions by directly referencing existing methods.
They act as shorthand for lambdas that simply call a specific method.
They improve code readability and reduce verbosity.
How They Work:
Syntax: Use the :: operator to reference a method: objectName::methodName (instance method) or ClassName::methodName (static method).
Compatibility: The method’s signature must match the functional interface’s abstract method’s signature.
Execution: When the lambda is invoked, the referenced method is called instead
Types of Method References:
//Reference to a Static Method:
ClassName::staticMethodName
//Reference to an Instance Method of a Particular Object:
objectName::instanceMethodName
// Reference to an Instance Method of an Arbitrary Object of a Particular Type:
ClassName::instanceMethodName
// Reference to a Constructor:
ClassName::new
A coding example-
package com.lambda.expressions;
// Functional Interface with no arguments
interface MyFunctionalInterfaceRefence {
void myMethod();
}
// Class with a static method and an instance method
class MyClass {
static void staticMethod() {
System.out.println("Static method of MyClass");
}
void instanceMethod() {
System.out.println("Instance method of MyClass");
}
// This is just to explain Reference to a Constructor
MyClass() {
System.out.println("Constructor invoked");
}
}
public class MethodReferenceExample {
public static void main(String[] args) {
// Method references for a static method
MyFunctionalInterfaceRefence staticMethodReference =MyClass::staticMethod;
// Create an object.
MyClass myObject = new MyClass();
// Method references for an instance method
MyFunctionalInterfaceRefence instanceMethodReference=myObject::instanceMethod;
// Calling the method references
staticMethodReference.myMethod();
instanceMethodReference.myMethod();
// Constructor reference
MyFunctionalInterfaceRefence constructorReference=MyClass::new;
}
}
Output-
Constructor invoked
Static method of MyClass
Instance method of MyClass
Good reference-https://www.youtube.com/playlist?list=PLqq-6Pq4lTTa9YGfyhyW2CqdtW9RtY-I3