DEV Community

Cover image for A brief introduction to Java Lambda expressions
Aamir
Aamir

Posted on • Updated on

A brief introduction to Java Lambda expressions

Let me introduce you to a programming scenario that you are most probably already familiar with if you have been programming in Java for some time now.
Often you need to override a functional interface to provide callbacks for event listeners or to provide a custom implementation of a function, let's have a look at the following example as a refresher, where we are passing a callback method to the Greeter class which is calling our provided callback method after a 5 seconds delay an example of functional interface

interface onGreetCallback{
   public void greet(String greetMsg);
 }

class Greeter{
   private onGreetCallback callback;
   Greeter(onGreetCallback callback){
    this.callback = callback;

    //call the provided callback after 5 seconds delay
    try{
     Thread.sleep(5000);
     this.callback.greet("Hello, Pardon me, I am 5 seconds late to greet you.");
    }catch(InterruptedException  e){
      e.printStackTrace();
    }

   }
 }

 class DEMO{
   public static void main(String args[]){
    Greeter greeter =  new Greeter(new onGreetCallback(){
       @Override
       public void greet(String greetMsg){
         System.out.println(greetMsg);
       }
    });
   }
 }
Enter fullscreen mode Exit fullscreen mode

The above code is very common, and you will come across similar code on daily basis in java. As you can see, it takes some time to write the override method which is being passed into Greeter class constructor.
From Java 8, Lambda expressions have been added to simplify this part of code and make it look cleaner and elegant, not to mention shorter.
Let's write the main function again, this time with Lambda Expressions.

 public static void main(String args[]){
    Greeter greeter =  new Greeter(msg -> System.out.println(msg));
 }
Enter fullscreen mode Exit fullscreen mode

Lambda expressions just saved us at least 4 lines of code. In the following article, I am going to talk about lambda expressions in detail the syntax, where to use, and some comparisons with the older approach. So without further ado, let's get into the nitty-gritty details of Java Lambda Expressions.

In above modified code, msg -> System.out.println(msg) is the lambda expressions code where msg is the input parameter and
System.out.println(msg) is the body of the lambda expression, -> is what separates input parameters from the body of the lambda.

Above lambda expression has been written in a very condensed manner, a more expanded version would look like this.

  Greeter greeter =  new Greeter((msg) -> {
          System.out.println(msg);
  });
Enter fullscreen mode Exit fullscreen mode

If you have only one input parameter you can skip the brackets, but if you have more than one input parameter then you have to use brackets e.g.
Let's also pass in a name parameter into our example.

  Greeter greeter =  new Greeter((msg, name) -> {
     System.out.println("Hello " + name);      
     System.out.println(msg);
  });
Enter fullscreen mode Exit fullscreen mode

The curly brackets are also only needed if you have more than one line of code, that is why the first lambda expression I showed you is very compact.

 public static void main(String args[]){
    Greeter greeter =  new Greeter(msg -> System.out.println(msg));
 }
Enter fullscreen mode Exit fullscreen mode

The return keyword is also not required if there is only a single statement.
Let's see an example where we have more than one statement inside the lambda body and then see how we can write the same lambda expression with just a single line to remove the explicit return keyword.

 interface FilterInterface{
    boolean filter(int number);
}

//Class to call the user implementation of method, on given data
class Utils {
    private FilterInterface filterInterface;

    //User provided Transformation Method
    void setFilterMethod(FilterInterface filterMethod) {
     this.filterInterface = filterMethod;
    }
    //Transform method to apply user-provided Method on given data
    List transform(List data) {
        List filtered_data = new ArrayList<>();
        for (int i = 0; i < data.size(); i++) {
            //Test with user provided Filter method
            if (this.filterInterface.filter(data.get(i))) {
               filtered_data.add(data.get(i));
            }
        }
        return filtered_data;
    }
}
public class Main{
    public static void main(String[] args) {
        List data =  new ArrayList<>();
        data.add(1);
        data.add(2);
        data.add(3);
        data.add(4);
        data.add(5);
        data.add(6);

        Utils utils =  new Utils(); //Instantiate object of our custom util class
        //Let's provide a Lambda Expression to filter Even numbers
        /*
        * Because we have more than a single line of code, we have to use the return keyword
        * */
        utils.setFilterMethod(x -> {
            if (x % 2 ==0){
                return true;
            }
            return false;
        });

        //Apply the provided transformation function
        List filtered_data =  utils.transform(data);
    }
}
Enter fullscreen mode Exit fullscreen mode

I have deliberately written the above lambda expression in this long manner, let's write it with a single line and skip the return keyword.

        utils.setFilterMethod(x -> x % 2 == 0);
Enter fullscreen mode Exit fullscreen mode

Lambda Expression to Functional Interface mapping

To map the lambda to a functional interface, some conditions must be met.

  1. The interface must at least contain one abstract method.
  2. Lambda expression input parameters must match the input parameters of the abstract method.
  3. The return type of the lambda expression must be matched with the return type of the abstract method.

Lambda expressions also allow you to capture variables.

You can capture variables declared outside the body of the lambda function. The following variables can be captured inside the body of the lambda.

  • Local variables.
  • Instance variables.
  • Static variables.
Local variable capture
  class DEMO{
    final String name = "Aamir";
    public static void main(String args[]){
     Greeter greeter =  new Greeter((msg) -> {
       System.out.println("Hello " + name);      
       System.out.println(msg);
     });
   }
  }
Enter fullscreen mode Exit fullscreen mode

In the above example, we are capturing the name variable inside the lambda body. Local variable being captured must be declared as final, if the variable is not declared as final then the java compiler will complain.

Instance variable capture

Lambda expression also allows you to capture the Instance variables, an important difference worth noting here is that this is not possible in the classical approach where you implement an interface anonymously, this is because an anonymous interface has its own body where a lambda expression does not.

 class MyClass{
   private String name = "Aamir";
   Greeter greeter =  new Greeter((msg) -> {
     System.out.println("Hello " + this.name);      
     System.out.println(msg);
   });
 }
Enter fullscreen mode Exit fullscreen mode
Static Variables capture
 class DEMO{
    private static String name = "Aamir";
    public static void main(String args[]){
     Greeter greeter =  new Greeter((msg) -> {
       System.out.println("Hello " + name);      
       System.out.println(msg);
     });
   }
}
Enter fullscreen mode Exit fullscreen mode

Now that we have seen how the lambda expressions work under the hood, their structure, and different flavors of syntax, let's see a more practical example from Java Stream API, which allows you to write your code in a declarative fashion.

Applying a transformation/map function across an array or a list
public class Main{
    public static void main(String[] args) {
        int fav_nums[] = {1,2,3,4,5};
        //Apply the array transformation Using a Lambda Expression
        int new_fav_nums[] = Arrays.stream(fav_nums).map(x -> x *2).toArray(); // 2,4,6,8,10
    }
}
Enter fullscreen mode Exit fullscreen mode
Filter names based on starting character
public class Main{
    public static void main(String[] args) {
        String names[] = {"Aamir" ,
                            "Ali" ,
                            "Atif" ,
                            "Siraj" ,
                            "Shehriyaar" ,
                            "Shehroz" ,
                            "Hassan" ,
                            "Jibran"};
        //Applying filter to only get names starting with "A"
        String filtered_names[] = Arrays.stream(names).filter(name -> name.startsWith("A")).toArray(String[]::new);
        //Aamir
       // Ali
      //  Atif
    }
}
Enter fullscreen mode Exit fullscreen mode

Actually we can make the above code even more compact, by abstracting the logic inside the lambda expression and taking it out in its own method, by passing in a Method Reference.

 class MyFilters {
    public static boolean filterName(String name){
        return name.startsWith("A");
    }
}
public class Main{
    public static void main(String[] args) {
        String names[] = {"Aamir" ,
                            "Ali" ,
                            "Atif" ,
                            "Siraj" ,
                            "Shehriyaar" ,
                            "Shehroz" ,
                            "Hassan" ,
                            "Jibran"};
        //Applying filter to only get names starting with "A"
        //Using a method reference
        String filtered_names[] = Arrays.stream(names).filter(MyFilters::filterName).toArray(String[]::new);
        //Aamir
       // Ali
      //  Atif
    }
}
Enter fullscreen mode Exit fullscreen mode

The filter will call our supplied Method reference on each item inside the array.

For those of you who are asking what can be the use of abstracting out the lambda expression logic into its own separate class, allow me to share some use cases, this process will be very useful if you need to apply some similar kind of filter, map, reduce operations on different sets of arrays or lists. Or maybe your lambda expression body is getting too messy.
It also makes your code more readable to other developers, who will instantly get the meaning of your code by the name of the Method Reference you passed in, will be easier for them to understand your thought process. It will also stop you from going absolutely crazy if 6 months later you decide to make some changes. I know we all have been there.

:: signals the java compiler that this is a Method Reference

You can reference the following types of methods:

  • Static method.
  • Instance method on parameter objects.
  • Instance method.
  • Constructor.

Hopefully, by now you have a good understanding of what Java Lambda Expressions are, how to write them, their usage. If this is your first encounter with lambda's you may need some more time and practice to fully grasp it, but trust me once you have learned and mastered them, they will be your first choice of the tool while dealing with collections of data.

“Always focus on how far you've come, not how far you have to go.”

Latest comments (0)