Lambda expressions in Java 8
Ankit Tripathi
Lead Consultant @ ITC Infotech | Master of Science, Full-Stack Development
Java 8
Java 8 was released back in 2014, It was a great leap forward for Java language. There were major changes in the language. Although new versions are available, but still it's widely used.
The changes introduced include - lambda Expressions, Method references, Default methods, etc.
In this article, we will go through the lambda expressions and try to understand how to incorporate these in our day to day coding.
Lambda Expressions
Lambda expressions can be used to resolve the issues created by using anonymous classes. If the anonymous class contains only a single method, and the implementation is simple, the syntax of the anonymous class looks unnecessary and very complex.
Anonymous classes can be used to implement a base class without naming it. It is concise than the named Java classes, but, for a class with only a single method, even the anonymous class looks cumbersome.
Lambda expressions allows to write single instance methods even more compactly. Let's try to understand the use case of lambda expression using an example.
// A Student entity
public class Student {
String name;
String emailAddress;
int age;
public int getName() {
// ...
}
public void printStudentDetails() {
// ...
}
}
// static methods
public static void printStudentOlderThan(List<Student> students, int ageLimit) {
for (Student s : students) {
if (s.getAge() >= ageLimit) {
s.printStudentDetails();
}
}
}
public static void printStudentsWithinAgeRange(
List<Student> students, int low, int high) {
for (Student s : students) {
if (low <= s.getAge() && s.getAge() < high) {
s.printStudentDetails();
}
}
}
The above code is dependent on changes in the Student class. For ex : changes in the data type of any field.
So, let's try to generalize this method, even further using an CheckStudent interface.
interface CheckStudent {
boolean test(Student s);
}
public static void printStudentsAfterCheck(
List<Student> students, CheckStudent studentTester) {
for (Student s : students) {
if (studentTester.test(s)) {
s.printStudentDetails();
}
}
}
class CheckStudentNotEligibleForTrip implements CheckStudent {
public boolean test(Student s) {
return s.getAge() >= 18 &&
s.getAge() <= 25;
}
}
Although the above code is less dependent on any class implementation, but still the implementation of CheckStudent interface seems unnecessary. We can easily replace it with an anonymous class.
printStudentsAfterCheck(
students,
new CheckStudent() {
public boolean test(Student s) {
return s.getAge() >= 18
&& s.getAge() <= 25;
}
}
);
This reduces the code because you don't have to create a new class for each implementation of the 'test' method. But still, the syntax of Anonymous classes looks very bulky, especially, since the CheckStudent interface contains only a single method.
In the above case, we can use a lambda expression.
But before using the lambda expressions, its important to note (as pointed above) that the CheckStudent interface contains only a single abstract method. These interfaces are called functional interfaces.
We can replace Anonymous classes with lambda expressions for the functional interfaces.
printStudentsAfterCheck(
students,
(Student s) -> s.getAge() >= 18
&& s.getAge() <= 25
);
Interestingly, to further reduce the efforts of the developers, Java 8 introduces many standard functional interfaces in the java.util.function package.
So, we can directly use Predicate<T> functional interface.
public static void printStudentsAfterCheckWithPredicate(
List<Student> students, Predicate<Student> tester) {
for (Student s : students) {
if (tester.test(s)) {
s.printStudentDetails();
}
}
}
For method invocation :
printStudentsAfterCheckWithPredicate(
students,
s -> s.getAge() >= 18
&& s.getAge() <= 25
);
Syntax of Lambda expressions
The lambda expressions consist of - list of parameters (enclosed in parentheses and separated by commas); the arrow ->; method body
We can eliminate the data type of the parameters and the parentheses (if only single parameter) as well. For ex -
s -> s.getAge()>=18 && s.getAge()<=25
If the body contains only a single expression, then there is no need to mention the return statement, as seen in the above example. The JVM will evaluate the expression and return its value automatically.
领英推荐
Accessing the Local variables inside Lambda Expressions
Lambda expressions have the same access (like anonymous classes) to local variables which are in the enclosing scope. However, lambda expressions don't have shadowing issues. Lambda expressions are lexically scoped i.e. don't introduce a new level of scoping. In simple terms, declarations in a lambda expressions are as if these declarations were in the enclosing scope.
So, code can directly access fields, methods, and local variables of the enclosing environment. But, just like local and anonymous classes, lambda expressions can access local variables and parameters of the enclosing scope that are final or effectively final (its value is never changed). Otherwise, the compiler gives an error.
import java.util.function.Consumer;
public class LambdaScopeExample {
public int x = 0;
class FirstClassLevelScope {
public int x = 1;
void methodInFirstClassLevelScope(int x) {
int z = 2;
Consumer<Integer> myConsumer = (y) ->
{
// z = 99; // will cause compile time error
System.out.println("x = " + x);
System.out.println("y = " + y);
System.out.println("z = " + z);
System.out.println("this.x = " + this.x);
System.out.println("LambdaScopeExample.this.x = " +
LambdaScopeExample.this.x);
};
myConsumer.accept(x);
}
}
public static void main(String... args) {
LambdaScopeExample lse = new LambdaScopeExample();
LambdaScopeExample.FirstClassLevelScope fcls = lse.new FirstClassLevelScope();
fcls.methodInFirstClassLevelScope(23);
}
}
This above code gives the following output:
x = 23
y = 23
z = 2
this.x = 1
LambdaScopeExample.this.x = 0
Target type in Lambda expressions
To determine the type of the lambda expression, Java compiler uses the target type of the context of the lambda expression. Hence, lambda expressions can be used only if Java compiler is able to determine the target type.
For method arguments, the Java compiler determines the target type using two features - overload resolution and type argument interference.
Method References
In the above sections, we saw how to use lambda expressions to create anonymous "functions". However, lambda expressions sometimes just call another existing method. In these cases, method references can be used to write compact and simple lambda expressions.
Lets try to understand this using the below example.
// A Student entity
public class Student {
String name;
String emailAddress;
Integer age;
public int getName() {
// ...
}
public void printStudentDetails() {
// ...
}
// static methods
public static void compareStudentsByAge(Student a, Student b) {
return a.getAge().compareTo(b.getAge());
}
}
Suppose that the code is required to sort the Student array by age.
Arrays.sort(studentsArray,
(Student a, Student b) -> {
return a.getAge().compareTo(b.getAge());
}
);
However, this method to compare the ages of two Students, already exists as Students.compareStudentsByAge. So, we can invoke this method instead in the body of the lambda expression:
Arrays.sort(studentsArray,
(a, b) -> Student.compareStudentsByAge(a, b)
);
Because this lambda expression invokes an existing method, you can use a method reference instead of a lambda expression:
Arrays.sort(studentsArray, Student::compareStudentsByAge);
The above method reference is semantically the same as the lambda expression. Lets now see the types of Method References and their syntax.
1) Reference to a static method
syntax --> ContainingClass::staticMethodName
2) Reference to an instance method
syntax --> containingObject::instanceMethodName
(or)
syntax --> ContainingType::methodName
3) Reference to a constructor
syntax --> ClassName::new
import java.util.function.BiFunction;
public class MethodReferencesExample {
public static <T> T bifunEx(T a, T b, BiFunction<T, T, T> bifunc) {
return bifunc.apply(a, b);
}
public static String appendStringsStatic(String a, String b) {
return a + b;
}
public String appendStringsInstance(String a, String b) {
return a + b;
}
public static void main(String[] args) {
MethodReferencesExamples mre = new MethodReferencesExamples();
// Calling the method with a lambda expression
System.out.println(MethodReferencesExamples.
bifunEx("Hello ", "World!", (a, b) -> a + b));
// Reference to a static method
System.out.println(MethodReferencesExamples.
bifunEx("Hello ", "World!", MethodReferencesExamples::appendStringsStatic));
// Reference to an instance method
System.out.println(MethodReferencesExamples.
bifunEx("Hello ", "World!", mre::appendStringsInstance));
// Reference to an instance method of an arbitrary object
System.out.println(MethodReferencesExamples.
bifunEx("Hello ", "World!", String::concat));
}
}