Lambda run Java
Yes, a lambda run Java code. It’s a key feature introduced in Java 8 that provides a concise way to represent an instance of a functional interface. You use it to write a block of Java code that can be treated as a single object and passed as an argument to a method.
Introduction to Lambda Expressions (JDK 8+)
By treating functionality as a method argument or code as data, lambda expressions make Java programming more efficient and effective. Originally included in Java SE 8 (JDK 8), they simplify the use of some standard constructions, including anonymous classes, and cut down on the quantity of the code required. Other new features, such as the Stream API and default methods in interfaces, were also sparked by this addition to Java. Lambdas are now a key component of computer language design, guaranteeing that Java will always be a dynamic and inventive language.
In essence, an anonymous (unnamed) method is a lambda expression. Instead of running independently, it is employed to carry out a method that is specified by a functional interface. Lambda expressions are therefore also frequently called closures.
General Form of a Lambda Expression
The basic structure of a lambda expression involves a new operator, ->
, often called the lambda operator or arrow operator. This operator divides the lambda expression into two parts:
- The lambda expression’s parameters are specified on the left side.
- The actions of the lambda expression are specified by the lambda body, which is located on the right.
The ->
operator can be verbalised as “becomes” or “goes to”.
Here’s the general syntax: (parameter-list) -> { lambda-body }
Parameter List Variations:
- No parameters: Use brackets that are empty ().
- With no parameters, this lambda yields the constant value 98.6.
- Single parameter: It is possible to omit the parenthesis.
- If the number
n
is even, this lambda returnstrue
. This tutorial and other programming styles may still use brackets to enclose single-parameter lists for consistency. - Multiple parameters: Multiple parameters are denoted by commas and contained with brackets.
- This lambda establishes whether
n
is a factor ofd
or not.
Type Inference: In many cases, Java’s compiler can determine a parameter’s type based on its context, so you won’t need to explicitly provide it. On the other hand, all of the parameters in the list must have specified types if you explicitly declare the type of one parameter.
Functional Interfaces
One and only one abstract method is present in a functional interface. The desired function or activity of the interface is often specified by this one abstract method. The standard Runnable
interface, for instance, defines exactly one method, run()
, making it a functional interface.
Static methods, default methods (introduced in JDK 8), and any public
methods provided by Object
(like equals()
) can all be included in functional interfaces without compromising their position as such.
Determining a lambda expression’s target type is the main purpose of a functional interface. You can only use a lambda expression when you specify a target type (a compatible functional interface), like in assignments, variable initialisation, return statements, or method arguments.
To designate an interface as functional, use the @FunctionalInterface
annotation. It is only informational and not strictly necessary (any interface that has precisely one abstract method is functional by definition), but it aids the compiler in making sure the interface complies with the functional interface contract by generating a compilation error if it doesn’t.
Example of a Functional Interface and Lambda Assignment:
// A functional interface.
interface MyValue {
double getValue();
}
class LambdaDemo {
public static void main(String args[]) {
MyValue myVal; // Declare an interface reference
// Assign a lambda expression to the functional interface reference.
// The lambda implements the getValue() method.
myVal = () -> 98.6;
// Call getValue(), which is provided by the assigned lambda expression.
System.out.println("A constant value: " + myVal.getValue());
// Assign another lambda expression, this time a more complex one.
myVal = () -> Math.random() * 100;
System.out.println("A random value: " + myVal.getValue());
}
}
Output:
A constant value: 98.6
A random value: 53.00582701784129 (This value will vary)
In this example, MyValue
is a functional interface. The lambda expressions () -> 98.6
and () -> Math.random() * 100
are compatible with its getValue()
method (no parameters, returns double
) and provide its implementation.
Expression Lambdas vs. Block Lambdas
Java is compatible with two kinds of lambda bodies:
- Expression Lambdas (Expression Bodies): There is only one expression in the body. The lambda returns the value of this expression implicitly. Frequently used for simple processes, these are brief.
- Block Lambdas (Block Bodies): Block lambdas, also known as block bodies, are made up of a block of code encased in curly braces
{}
. Loops, conditional statements, multiple statements, and variable declarations are all possible with block lambdas. Toreturn
a value, they need an explicit return statement, unlike expression lambdas.
Example: Expression Lambda vs. Block Lambda Let’s use a functional interface for a numeric function:
interface NumericFunc {
int func(int n);
}
class LambdaBodyDemo {
public static void main(String args[]) {
// Expression Lambda: Squares a number
NumericFunc square = (n) -> n * n;
System.out.println("Square of 5 (Expression Lambda): " + square.func(5));
// Block Lambda: Computes the factorial of a number
NumericFunc factorial = (n) -> {
int result = 1;
for (int i = 1; i <= n; i++) {
result = i * result;
}
return result; // Explicit return is necessary for block lambdas
};
System.out.println("Factorial of 5 (Block Lambda): " + factorial.func(5));
}
}
Output:
Square of 5 (Expression Lambda): 25
Factorial of 5 (Block Lambda): 120
The structural and functional distinctions between square
and factorial
are illustrated in this example, where square is an expression lambda and factorial is a block lambda.
Lambda Expressions and Variable Capture (Effectively Final): Local variables from the enclosing scope that are used inside a lambda must be “effectively final” that is, they cannot be changed after they have been initialised, either inside or outside the lambda while instance or static variables of the enclosing class can be changed by the lambda. Lambda Expressions and Variable Capture (Effectively Final): Variables defined in the enclosing scope of a lambda expression are accessible within the language.
Method References (::)
A method reference is a powerful feature of lambda expressions that allows you to refer to a method without actually running it; when evaluated, it creates an instance of the functional interface and requires a target type context of a compatible functional interface. Method references are frequently more self-documenting and concise than lambda expressions, especially when the lambda is just calling an existing method.
JDK 8 added a new separator for method references called ::
(double colon).
Several categories of method references exist, including:
- Making Use of a Static Method
- Syntax:
ClassName::staticMethodName
- The static method receives as an argument the instance object in the collection.
- Syntax:
- Reference to a Specific Object’s Instance Method:
- Syntax:
objectReference::instanceMethodName
- On the given
objectReference
, the method is invoked.
- Syntax:
- Invocation of a Specific Type of Arbitrary Object Instance Method:
- Syntax:
TypeName::instanceMethodName
- When a lambda receives a parameter that is an instance of the class
TypeName
and the method reference acts on that parameter, this type is utilised.
- Syntax:
Constructor References
The creation of constructor references is similar to that of method references, and any functional interface reference that provides a method compatible with the constructor can be assigned a constructor reference.
Syntax: ClassName::new
The constructor of ClassName
is referred to here, and the matching parameterised constructor will be referred to if the functional interface method accepts parameters.
- Generic Constructors: Type arguments can be included in the reference, for example
MyClass<Integer>::new
. - Array Constructors: References can also be made to array constructors using
TypeName[]::new
.
Example: Constructor Reference
// Functional interface whose method returns a MyClass reference
interface MyClassFactory {
MyClass create(String s);
}
class MyClass {
private String str;
// Parameterized constructor
MyClass(String s) { str = s; }
// Default constructor (not used by the reference below)
MyClass() { str = ""; }
String getStr() { return str; }
}
class ConstructorRefDemo {
public static void main(String args[]) {
// Create a reference to the MyClass constructor.
// Since create() in MyClassFactory takes a String argument,
// 'new' refers to the parameterized constructor in MyClass.
MyClassFactory factory = MyClass::new;
// Create an instance of MyClass via the constructor reference.
MyClass mc = factory.create("Hello from constructor reference!");
System.out.println("String in mc is: " + mc.getStr());
}
}
Output:
String in mc is: Hello from constructor reference!
To create MyClass
objects, a String
parameter is passed to the MyClass
constructor, which is referenced by the factory
variable.
Predefined Functional Interfaces
Several predefined functional interfaces are provided by the java.util.function
package, which was introduced in JDK 8 to make programming easier and eliminate the need to define custom functional interfaces for common tasks. These interfaces are used extensively throughout the Java API, especially with the Stream API and other lambda-aware features.
Here are a few examples of popular predefined functional interfaces and associated abstract techniques:
- Predicate<T>:
- Purpose: Determines if an object of type
T
fulfills a constraint. - Abstract Method:
boolean test(T val)
- Example:
Predicate<Integer> isEven = (n) -> (n % 2) == 0;
- Purpose: Determines if an object of type
- Consumer<T>:
- Purpose: Performs an operation on an object of type
T
and returns no result. - Abstract Method:
void accept(T val)
- Example:
list.forEach(item -> System.out.println(item));
- Purpose: Performs an operation on an object of type
- Function<T, R>:
- Purpose: Applies an operation to an object of type
T
and returns a result of typeR
. - Abstract Method:
R apply(T val)
- Example:
Function<String, Integer> stringLength = (s) -> s.length();
- Purpose: Applies an operation to an object of type
- Supplier<T>:
- Purpose: Supplies an object of type
T
(e.g., for lazy evaluation). - Abstract Method:
T get()
- Example:
Supplier<Double> randomNumber = () -> Math.random();
- Purpose: Supplies an object of type
- UnaryOperator<T>:
- Purpose: Applies a unary operation to an object of type
T
and returns a result of the same typeT
. It extendsFunction<T, T>
. - Abstract Method:
T apply(T val)
- Example:
UnaryOperator<Integer> increment = (n) -> n + 1;
- Purpose: Applies a unary operation to an object of type
- BiFunction<T, U, R>:
- Purpose: Applies an operation to two objects of types
T
andU
, returning a result of typeR
. - Abstract Method:
R apply(T t, U u)
- Example:
BiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b;
- Purpose: Applies an operation to two objects of types
- BinaryOperator<T>:
- Purpose: Applies an operation to two objects of type
T
and returns a result of the same typeT
. It extendsBiFunction<T, T, T>
. - Abstract Method:
T apply(T val1, T val2)
- Example:
BinaryOperator<Integer> add = (a, b) -> a + b;
- Purpose: Applies an operation to two objects of type
Example: Using Predicate and Consumer
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.function.Consumer;
class PredefinedFunctionalInterfacesDemo {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
names.add("David");
// Using Predicate to filter names longer than 4 characters
Predicate<String> isLongerThanFour = (name) -> name.length() > 4;
System.out.println("Names longer than 4 characters:");
for (String name : names) {
if (isLongerThanFour.test(name)) {
System.out.println(name);
}
}
// Using Consumer to print names with a greeting
Consumer<String> greetName = (name) -> System.out.println("Hello, " + name + "!");
System.out.println("\nGreetings:");
names.forEach(greetName); // forEach method often takes a Consumer
}
}
Output:
Names longer than 4 characters:
Alice
David
Greetings:
Hello, Alice!
Hello, Bob!
Hello, Charlie!
Hello, David!
To make the code cleaner and more useful, this illustrates how to use Predicate
to specify a test condition and Consumer
to define an action to be executed on each element.
In Conclusion
Lambda expressions, functional interfaces, method references, and constructor references mark a significant change in Java’s programming paradigm that makes it possible to write more expressive, succinct, and effective code particularly for jobs involving collections and parallel processing.