Lambda expressions in Java 8

Lambda expressions in Java 8

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));

    }

}        

要查看或添加评论,请登录

Ankit Tripathi的更多文章

  • Optional in Java

    Optional in Java

    Every Java programmer must have dealt with NullPointerException. Null pointer exception, which represents absence of a…

  • Java Streams

    Java Streams

    Java 8 introduced a new java.util.

  • String, StringBuffer & StringBuilder : Use Cases and Differences

    String, StringBuffer & StringBuilder : Use Cases and Differences

    In the previous article we saw how String objects can be used to represent a character sequence. String objects are…

  • Strings in Java

    Strings in Java

    java.lang.

  • Root of the class hierarchy in Java : java.lang.Object

    Root of the class hierarchy in Java : java.lang.Object

    In Java, objects are often used to represent the real-world objects (that we find in our everyday life). All these…

  • Throwing and Catching, Exceptions!

    Throwing and Catching, Exceptions!

    So, What is an Exception? How is it "thrown" and "Caught"? Exception is an event, which occurs during the code…

  • Java High Level Concurrency APIs

    Java High Level Concurrency APIs

    In the previous article on Java Concurrency support, we looked into the low level APIs provided by Java platform. After…

  • Concurrent Programming in Java (Multi Threading)

    Concurrent Programming in Java (Multi Threading)

    Today, we all are used to working on systems that can do more than one things at a time. Not just systems, even a…

    1 条评论
  • Java Collection Framework : Implementations Classes

    Java Collection Framework : Implementations Classes

    In previous two articles, we had an brief overview of the Java collections framework and its interfaces. Collection…

  • Java collections framework: Interfaces

    Java collections framework: Interfaces

    In previous article, we had an brief overview of the Java collections framework. Collections framework consists of…

社区洞察

其他会员也浏览了